Merge main into #664

# Conflicts:
#	crates/engine/src/code_connect.rs
#	crates/engine/src/codegen.rs
#	crates/engine/src/component_codegen.rs
#	crates/engine/src/lib.rs
#	crates/engine/src/playground.rs
#	crates/ui-egui/locales/bn.toml
#	crates/ui-egui/locales/cs.toml
#	crates/ui-egui/locales/de.toml
#	crates/ui-egui/locales/en.toml
#	crates/ui-egui/locales/es.toml
#	crates/ui-egui/locales/fi.toml
#	crates/ui-egui/locales/fr.toml
#	crates/ui-egui/locales/id.toml
#	crates/ui-egui/locales/ja.toml
#	crates/ui-egui/locales/ka.toml
#	crates/ui-egui/locales/ko.toml
#	crates/ui-egui/locales/nl.toml
#	crates/ui-egui/locales/pl.toml
#	crates/ui-egui/locales/pt-BR.toml
#	crates/ui-egui/locales/ru.toml
#	crates/ui-egui/locales/sv.toml
#	crates/ui-egui/locales/tr.toml
#	crates/ui-egui/locales/zh-Hans.toml
#	crates/ui-egui/src/dev_mode.rs
#	docs/dev-mode-parity.md
#	docs/gaps.md
#	docs/mcp.md
#	docs/target-app-parity.md
This commit is contained in:
bflatastic committed 2026-10-26 23:52:51 +07:00
commit 4176400e09
541 files changed
+53315 -4270

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
# Boards parity
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-24 · **Change:** minor (templates and tables; facilitation: a shared timer and voting sessions; the dot grid, dragging a connector's end to re-attach it, curved connectors, elbows that go around what they join) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
**Breadth ~47%, ready ~29% (estimated, erring low).** A board is a FigCraft file of its own kind
(`file.newBoard`) with a dot grid on the page, sticky notes, shapes with text, connectors that
follow what they join (straight, elbow or curved, their ends dragged onto other objects), marker,
highlighter and eraser, stamps, text and sections, five built-in templates and tables, edited
together in the existing live sessions, with a shared countdown and voting sessions to run a
workshop. Music, board widgets, mind maps and the rest of the tail are missing. Remaining:
~20–44 h. Tracked as area 22 of
[`target-app-parity.md`](target-app-parity.md) (not in its weighted sum: the target there is the
design product). The work item is gap 24 in [`gaps.md`](gaps.md).
## How boards are built
Board objects are ordinary layers with a small `board` field on the node (`figcraft_doc::BoardItem`),
so rendering, hit testing, undo, saving, SVG/PNG/PDF export, copy and paste and live sessions work
on them unchanged. The file kind (`Document.kind`) is saved in `.figcraft` and reaches people who
join a live session in their snapshot. What follows from the metadata (a sticky's text size, a
shape's text room, connector routes and their labels) is recomputed in the engine's normalize pass,
deterministically from the document, so local edits, undo and edits from others in a live session
all end with the same geometry.
## Features
| Feature | Status | Notes |
|---|---|---|
| Board files: File ▸ New board, the home screen's New board card, a light page | done | `file.newBoard`; the kind shows in `inspect_document`, tabs and file rows (a note mark; design files a pen nib) |
| Board toolbar at the bottom | done | Move/Hand, Marker/Highlighter/Eraser, Sticky (colour flyout), Shapes (flyout), Connector, Text, Section, Stamp (flyout), Comment, Resources, Actions, the Design panel's button |
| Design panel hidden by default; a contextual bar above the selection | done | colour, shape, text size (S/M/L/XL), connector straight/elbow/curved and its arrow; the Design panel opens with its button or View ▸ Design panel |
| Prototyping and Dev Mode refuse on boards | done | `proto.*`, `dev.*`, `view.prototype`, `view.devMode` say why |
| Sticky notes | done | a square frame (200 wide, growing taller through auto layout), 8 soft colours, a soft shadow, the note's text size shrinking as it grows (24 → 11, by characters and lines), the author's name at the bottom; a click places one and starts typing; Enter or a double-click edits; Shift+Cmd/Ctrl+Enter adds the next one beside it |
| Sticky options: hide the author, change size presets, sticky stacks | partial | `board.sticky {author: false}`; no size presets or stacks |
| Shapes with text | done | rectangle, rounded rectangle, ellipse, diamond, triangle, speech bubble, chevron; text centred and wrapping inside the outline; shape type and text size from the contextual bar |
| Shape auto-fit to its text, more shapes (cylinder, cross, star…), shape libraries | missing | |
| Connectors | done | from a layer's side (the facing one, or one named) to another layer or a free point, straight, elbow or curved, arrowheads (the stroke's caps), a label in the middle of the route by length (double-click to add); re-routed when either end moves or resizes; a deleted end stays where it was |
| Curved connectors, elbows around objects, dragging an end to re-attach | done | `board.setStyle {style: curved}` (and the contextual bar): one curve leaving and arriving along the sides; an elbow whose short way would cross what it joins goes around their boxes, 24 units clear; a selected connector shows its two ends as handles, and dragging one onto an object attaches it there (the object is outlined while dragging), onto empty canvas leaves it free (`board.reattach {id, end, target, side?}`, one undo step) |
| Connector waypoints placed by hand | missing | routes are computed from the ends; no bend points of your own |
| Quick-create handles (drag out a connected copy) | missing | |
| Marker and highlighter | done | the Pencil's stroke fitting with board presets: marker opaque, highlighter wide, see-through and multiplying |
| Eraser | done | drag over marker and highlighter strokes to remove them, one undo step |
| Pen colours and widths in the toolbar | partial | `tool.marker {color}`, `tool.highlighter {color}`; colour changes on a selected stroke from the contextual bar |
| Stamps | done | thumbs up, heart, plus one, star, question, check: round badges drawn as vectors, the placer's name on hover |
| Stamps sticking to the object they land on, the stamp wheel, emotes, stickers | missing | |
| Text and sections | done | the existing Text tool and sections |
| Live sessions: cursors, selections, cursor chat, spotlight | done | the existing sessions; board objects, connectors following moved objects, colours and stamps sync both ways (checked with two app instances) |
| Timer | done | a pill at the top right of the board: presets (1, 3, 5, 10, 15 min) or any whole minutes, pause, resume, a minute more, stop; red at 0:00 until stopped; no sound. `board.timer {action, minutes?}`, `board.timerState`. Synced as its end time (or the time left while paused), so every client counts down on its own clock: nothing ticks over the network. Not an undo step |
| Timer music, timer sounds | missing | |
| Voting sessions | done | one at a time: votes per person (1–99), on every object, on sticky notes or on the selection; a click on an object places a vote (several on one object allowed), a click on your badge or an Alt-click takes it back; your own votes show as badges while it runs, everyone's once it ends, with a ranked list (ties share a rank) that selects an object from its row; Clear results. `board.vote {action: start\|cast\|retract\|end\|clear}`, `board.voteResults`. Ballots sync per voter (keyed by the live-session client id, or your name offline), so people voting at once never overwrite each other. Not undo steps |
| Voting: only the facilitator ends or clears it, who voted for what | missing | anyone who can edit the board can end or clear a vote; results are counts only |
| Board widgets | missing | design-file widgets exist (`crates/engine/src/widgets.rs`), not wired to boards |
| Templates (retrospective, brainstorm, flow chart…) | partial | five built-in templates drawn up for FigCraft (retrospective, brainstorm, weekly planner, flow chart, kanban board), placed from the toolbar's templates picker (each with a picture of itself) or `board.template {name, x?, y?}` as ordinary sections, stickies, shapes, connectors and text in one undo step; `board.templates` lists them. No community templates, no saving your own |
| Tables | partial | `board.table {rows, columns, cells?, x?, y?}` (at most 50 × 20): an auto layout frame of rows of cells with text, a tinted header row and lines between rows and columns; a cell's row grows with its text; a double-click types in the cell under the pointer; add or remove a row or column from the bar over the selection or `board.tableEdit {id, action, index?}`; undoable and synced in live sessions. No column resizing by dragging, no cell colours, no Tab between cells, no CSV paste |
| Mind maps | missing | |
| Washi tape, code blocks, image cropping on boards | missing | images can be placed as in design files |
| Audio and huddles | missing | |
| AI features (summaries, sorting stickies, generating boards) | missing | |
| Dot grid on the page | done | the canvas render draws it after the page colour and before the layers (`figcraft_render::Dots`), so objects cover it; 24 units apart, doubling when zoomed out until the dots are at least 14 points apart, the dots the next doubling drops fading out first; dark dots on a light page, light ones on a dark page, from the theme's ink; written straight into the image, no allocation per dot; never in exports |
| `.jam` import and export | missing | see [`file-format-parity.md`](file-format-parity.md) |
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | M12: the dot grid on board pages (a render pass under the layers), dragging a selected connector's end onto another object or empty canvas (`board.reattach`), curved connectors, and elbows that go around the objects they join. Breadth ~35% → ~37%, ready ~20% → ~21%, ~30–60 h → ~28–56 h |
| 2026-10-24 | minor | Facilitation (M12): a shared timer and voting sessions. Breadth ~35% → ~40%, ready ~20% → ~24%, ~30–60 h → ~26–54 h |
| 2026-10-24 | minor | Templates (retrospective, brainstorm, weekly planner, flow chart, kanban board) from a toolbar picker or `board.template`, and tables (`board.table`, `board.tableEdit`): breadth ~35% → ~40%, ready ~20% → ~24%, remaining 30–60 h → 26–54 h |
| 2026-10-24 | major | Created with board files (M12): sticky notes, shapes with text, connectors, ink and eraser, stamps, the board toolbar and contextual bar, live-session sync |
+36 -9
View File
@@ -4,7 +4,13 @@ FigCraft without a window: inspect, export, convert and render documents, run co
commands, serve MCP and measure performance. Build it with
`cargo build --release -p figcraft-cli` (binary `target/release/figcraft-cli`).
Bad arguments print usage and exit with code 2; failures exit with 1. The CLI never panics on input.
`figcraft-cli --help` (or `-h`, or `help`) prints usage on stdout and exits with 0, and so does
`--help` or `-h` after a subcommand (`figcraft-cli mcp --help`). Bad arguments print usage and exit
with code 2; failures exit with 1. The CLI never panics on input.
Put options before `--` to pass a filename beginning with a dash, for example
`figcraft-cli info --json -- --drawing.figcraft`. Every argument after `--` is literal,
including a filename named `--no-system-fonts`.
## Fonts
@@ -19,6 +25,16 @@ On Linux and other Unix systems except macOS, user fonts are read from
`$XDG_DATA_HOME/fonts`, or `$HOME/.local/share/fonts` when `XDG_DATA_HOME` is unset,
empty or relative. The legacy `$HOME/.fonts` directory is also scanned.
On macOS the font folders (`/System/Library/Fonts`, `/Library/Fonts`, `~/Library/Fonts` and the
system's downloadable fonts) are read, and so is every other font file Core Text lists as
available: fonts a font manager such as RightFont, FontBase or Typeface activates from its own
library, without copying them into a font folder. Fonts activated while the app runs are found by
`text.rescanFonts`, like newly installed ones.
On macOS, fonts activated through Adobe Fonts are read where Creative Cloud keeps them
(`~/Library/Application Support/Adobe/CoreSync/plugins/livetype`), so they're available while
they're active there. Files without an extension are read when they start like a font file.
The collaboration server is a binary of its own, `figcraft-server` (`cargo build --release -p
figcraft-server`): it keeps files with share links and serves their live sessions, and its
`key`, `files`, `links`, `reports`, `block-ip` and `check` subcommands manage them (a server can
@@ -36,7 +52,8 @@ layers}]` with `--json`); their text renders in a fallback font.
Every subcommand that reads a file also takes the public REST API file JSON (the response of
`GET /v1/files/:key` or of its nodes endpoint, written to a `.json` file): it is imported on the fly.
Image fills come from `images/<imageRef>.png` (or `.jpg`, …) next to the JSON when present, else a
grey placeholder. `.fig` files are read the same way (best effort, images from the file itself);
grey placeholder. `.fig` files are read the same way (best effort, images from the file itself), and
so are `.sketch` files saved by Sketch 48 or newer;
`info` lists what wasn't imported as `note:` lines.
```text
@@ -51,6 +68,10 @@ page 1 "Screens": 380 nodes, 4 top-level
- `<out>` is a directory: one file per top-level frame of the page (default the first page), named
after the frame (`Home.png`, `Settings.png`…); with no frames, one file per top-level layer.
The name is kept as it is, except for the characters file systems refuse (`/ \ : * ? " < > |`
and control characters), which become `_`, and leading or trailing spaces and dots, which are
dropped. A name is cut at 100 characters. The app's export replaces the same characters (there a
`/` makes a folder). Two layers with the same name get `Name 2`, `Name 3`…
- `<out>` has an image extension: one file with the given `--node`, or the whole page composited.
- `--node` takes a layer id (`12` or `0:12`) or a layer name.
- `--format fig` (or an `<out>` ending in `.fig`) writes the whole file as `.fig`, experimental:
@@ -86,7 +107,7 @@ figcraft-cli export app.figcraft -o handoff.fig
### `convert <in> <out>`
By extension. Inputs: `.figcraft`, `.json` (FigCraft or REST API file JSON), `.fig` (best effort), `.svg` (an editable frame: groups,
By extension. Inputs: `.figcraft`, `.json` (FigCraft or REST API file JSON), `.fig` (best effort), `.sketch` (Sketch 48 or newer, best effort), `.svg` (an editable frame: groups,
shapes, text, gradients, masks and effects; `info` lists what it left out as `note:` lines), `.png/.jpg/.webp/.gif/.tif/.bmp`
(an image layer). Outputs: `.figcraft`, `.json` (FigCraft JSON, pretty-printed), `.svg`,
`.png/.jpg/.webp` (the current page rendered at 1x with its background), `.pdf` (the current page's
@@ -106,14 +127,18 @@ Runs commands in a headless session (a new document, or `--in`), printing one JS
```
The whole script is **one undo step**, and it is **all-or-nothing**, exactly like running the same
commands through `command.batch` (the MCP tool `batch`, the control channel, plugins): `{"$step": n}`
in a step's params is the `id` in an earlier step's result, so a step can target a layer an earlier
step of the same script created:
commands through `command.batch` (the MCP tool `batch`, the control channel, plugins). A step named
with `"as"` can be used by later steps: `{"$ref": "name"}` in a step's params is the `id` in that
step's result (`{"$step": n}` does the same by zero-based position), and `"field"` takes another key
or a slash path such as `ids/0`. So a step can target a layer an earlier step of the same script
created ([Step references](mcp.md#step-references) has the rules):
```json
[
{"command": "node.create", "params": {"type": "frame", "name": "Card", "width": 200, "height": 100}},
{"command": "node.create", "params": {"type": "text", "parent": {"$step": 0}, "text": "Hi"}}
{"command": "node.create", "as": "card", "params": {"type": "frame", "name": "Card", "width": 200, "height": 100}},
{"command": "node.create", "params": {"type": "text", "parent": {"$ref": "card"}, "text": "Hi"}},
{"command": "edit.duplicate", "as": "copy", "params": {"ids": [{"$ref": "card"}]}},
{"command": "node.set", "params": {"ids": [{"$ref": "copy", "field": "ids/0"}], "props": {"name": "Card copy"}}}
]
```
@@ -186,7 +211,9 @@ Serve MCP on stdio: headless by default, or attached to a running app started wi
`figcraft --control PORT`. See [mcp.md](mcp.md). The automation roots (each repeatable; existing
folders) confine the files the agent's calls read (inside a read or write root) and write (inside a
write root); everything else is refused ([mcp.md › Confining file access](mcp.md#confining-file-access)).
With `--attach` they are refused: start the app with them instead. The headless session can host, join and publish
They apply to the headless session only, and that is why `--attach` refuses them: an attached server
is confined by the app it attaches to, from the flags that app was started with. The headless
session can host, join and publish
live sessions (`collab.*`: without a key on a server open to the public, with `collab.serverInfo`,
`collab.manageFile` for the owner token and `collab.report`) and use plugin directories
(`plugin.directory.*`); the one-shot `run` can't (no network link).
+14 -1
View File
@@ -99,6 +99,15 @@ fit FigCraft's architecture:
collide with what a connected client makes, because a client's numbers continue above every id of
its space already in the document. Removed entries are part of the saved session state
(`SessionState::removed`).
- **Branches** of a file in a session stay with the person who made them; nothing about them goes
over the wire until a merge, which is an ordinary batch from that person (no message or format
change). So that the server takes the layers a branch made, a branch made in the session makes
its ids in its author's own client space, 2^24 above every id made there on the main file and the
author's other branches. Layers of the branch's base that main deleted during the session come
back with their ids (the server accepts re-creating deleted ids). Merging a branch whose new
layers, styles, variables or comments lie in another id space (one made offline or in an earlier
session) is refused by the app before anything is sent, since the server would refuse it (see
`crates/engine/src/branches.rs`).
- Objects inside an entry are not separate objects: modes are part of their collection, a
variable's values per mode part of the variable and replies part of their comment thread. Two
people adding a mode to the same collection, editing different modes of one variable, or replying
@@ -142,7 +151,11 @@ As built:
colour profile, image presets, annotation categories and library data (whole values); styles,
comment threads, variable collections and variables (one entry per id plus the order of ids;
the whole list when ids aren't unique); dev-mode snapshots (per layer); images (per content
hash, the encoded bytes as base64). The page order is the pages' positions.
hash, the encoded bytes as base64); a board's timer and vote (whole values, `DocProp::Timer` and
`DocProp::Vote`) and the vote's ballots (`Entry::Ballot(voter)`, one per voter, so people voting
at once write different entries). The timer travels as its end time (or, paused, the time left),
never as ticks: each client counts down on its own clock. The page order is the pages'
positions.
- **Next**: this file's published library (it embeds a snapshot document; it moves with an asset
channel, together with images, which then stop travelling inside batches); the version history
(kept beside the document, not in it) on the standalone server; finer entries for modes,
+3 -2
View File
@@ -1,6 +1,6 @@
# Collaboration parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (server message limits follow `max_file_mb`; a damaged log generation field keeps flushed edits; the web build joins from a `#join=` link; removing someone from a live session; live-session revert guard; live sessions per document; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; cursor chat and spotlight in live sessions; branch version history and finer list merges; comments and version history behaviour suite and fixes; hosting the server: `wss://` in the desktop app, server releases, deployment guide; team libraries on the server and swap library; branches: create, update from main, review and merge; wording: the standalone server with share links to edit or view; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (branches in live sessions and branch history entries; server message limits follow `max_file_mb`; a damaged log generation field keeps flushed edits; the web build joins from a `#join=` link; removing someone from a live session; live-session revert guard; live sessions per document; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; cursor chat and spotlight in live sessions; branch version history and finer list merges; comments and version history behaviour suite and fixes; hosting the server: `wss://` in the desktop app, server releases, deployment guide; team libraries on the server and swap library; branches: create, update from main, review and merge; wording: the standalone server with share links to edit or view; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7)
Figma is multiplayer first; FigCraft is local first. **Ready ~13% (estimated):** comments, version
history and branches (review and merge) work inside one file; the real-time collaboration core landed in #103 (sync model,
@@ -18,7 +18,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla
| Share links, permissions (view / edit), teams and projects | yes | **partial**: sharing with a join code from the Share popover (online: the file goes to the server in Preferences and the server names its edit and view links with short codes; local network: the app hosts and the code carries its address; one field joins with a code or a link, in the popover and on the home screen; the browser build also joins from a link in its URL's fragment, `#join=…`, which never reaches the web host's logs, capped at 6 KiB and checked like a `figcraft://` link (#712; unit-tested, not run in a browser)); `figcraft-server` share links per file with edit or view access (a viewer sees changes and presence, a "View only" badge and only the moving tools; the server refuses its edits), revocable at once, tokens stored as hashes; managed from the app's Share popover (first links), the server's command line or a managing connection. On a server open to the public, whoever published a file manages it with its owner token (links, name, removal: `collab.manageFile`), which the app remembers: Home ▸ Online lists your online files (open again as owner, start and stop sharing, copy the code, rename, remove, forget), with one request per server for what became of them; anyone in a file can report it, and the operator blocks addresses. The person hosting a session, or a file's owner, removes someone from it (right-click their avatar, `collab.removePerson`): they are disconnected and their address can't join that file again while it runs. The list is per computer: no accounts, teams or projects | 8–17 |
| Comments: pins on frames and layers, threads, reactions, @mentions, resolve, unread, search and filters, links, in the presenter | yes (shared) | **done, local**: stored in the file, your name from Preferences; a behaviour suite from the documentation (pinning and following layers, threads, own edits and deletes, mentions, reactions, C and Shift+C, stepping, filters, saving, copy and paste); restoring a version keeps them. In a live session threads sync and anyone in it can reply, react and resolve (no accounts: a name is who you are) | (shared once multiplayer lands) |
| Version history: named versions, autosave checkpoints, view before restore, restore, duplicate | yes (cloud) | **done**, saved in the `.figcraft` file: restoring keeps the replaced design as an autosave first, a titled autosave becomes a named version, merging a branch records the file before and after. Versions are per person in a live session (a shared history needs accounts); no comparing two versions (Dev Mode compares a ready-for-dev layer with its state when marked) | — |
| Branching and merging | yes (paid plans) | **done, local**: branches saved in the `.figcraft` file, each opened in its own tab; Review and merge shows both sides' changes since the branch was made and the conflicts (the same property on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merge as one undo step (the branch is archived, the main file's history gets a version for the merge), update from main, archive. Each branch keeps its own version history (saved, viewed and restored on the branch tab, kept when it is merged or archived); fills, strokes and effects merge item by item. Left: branches in live sessions; other lists (text runs, export settings) conflict as a whole | 1–2 |
| Branching and merging | yes (paid plans) | **done, also in live sessions**: branches saved in the `.figcraft` file, each opened in its own tab; Review and merge shows both sides' changes since the branch was made and the conflicts (the same property on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merge as one undo step (the branch is archived, the main file's history gets a version for the merge), update from main, archive. Each branch keeps its own version history (saved, viewed and restored on the branch tab, kept when it is merged or archived; entries for its creation and updates from main); fills, strokes, effects, layout grids, export settings, guides, interactions, annotations and Dev Mode resources merge item by item. In a live session a branch stays with its author (its ids in their own client space, far above the main file's) and merging it is one edit everyone gets; peers' edits merge against the branch's own base and conflicts resolve as offline; a view link refuses merging. Left: others in the session can't see or open your branches (the server would have to keep them); text runs conflict as a whole; a branch made offline that adds layers merges only outside a session | 1–2 |
| Team / organization libraries | yes | **partial**: a file's library is published to `figcraft-server` under the file and enabled in other files by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names ([components-parity](components-parity.md), [server.md](server.md#team-libraries)). No team-wide library list, accounts or analytics over time | 4–8 |
| Notifications, activity | yes | no | with sharing |
@@ -26,6 +26,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | Branches in live sessions (gap 17): creating, editing, reviewing, updating from main and merging a branch work while its file is in a session. The branch stays with its author and makes its ids in their own client space 2^24 above the main file's, so the server takes the layers it adds; merging is one synced edit and one step of the author's undo history; others' edits on main merge against the branch's own base and conflicts resolve as offline; a view link still refuses merging; a branch made outside the session that adds layers, styles, variables or comments is refused with a reason. A branch's history gets entries for its creation and updates from main. No wire format change. Row wording also brought up to date with #798 (export settings and guides merge item by item). Numbers unchanged |
| 2026-10-17 | minor | From community batch #735: server message limits follow `max_file_mb`, so files above 64 MiB publish and open, the client takes at most 1 GiB and the server warns about large limits with open publishing (#705); a damaged generation field no longer drops a log's flushed edits (#687); the web build joins a session from a `#join=` link in its URL (#712, unit-tested and built for wasm, not run in a browser). Percentages and hours unchanged |
| 2026-10-14 | minor | Removing someone from a live session (M9, from the owner's use: whoever joined with the invite couldn't be put out short of ending the session for everyone): right-click a person's avatar ▸ "Remove <name> from session", or `collab.removePerson {client}`, when you host the session or own the file on a server (owner token). The hub closes their connection as the session's end for them (they keep their copy and don't reconnect) and bars their address from that file while the session or server runs, so the code or link they hold no longer gets them in; everyone else stays. Protocol addition within version 2 (`Admin removePeer`; older servers say they can't). Addresses are the only identity without accounts: people behind one address are barred together. Percentages and hours unchanged |
| 2026-10-13 | minor | Live-session revert guard (M9, #198): `file.revert` is refused while its document belongs to a live session, including publishing, reconnecting and hosting before the welcome. The document, previews, branch tabs and session stay intact; leave the session before reverting a local save. Offline revert is unchanged. Scripted access and sync regressions in `live_access_tests`. Numbers unchanged. |
+6 -4
View File
@@ -1,6 +1,6 @@
# Components, variants, styles and variables parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and
variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300
@@ -29,7 +29,7 @@ accounts).
| Assets panel: folders by page, frame and slash names (fold away), thumbnails, search, drag into frames and auto layout slots | done | virtualized; `component.list` gives each one's `folder` |
| Hidden from publishing: a leading `.` or `_` in the name | done | the name decides both ways (adding the prefix takes a published component out, removing it brings it back); Hide when publishing in the Assets panel's context menu and Libraries ▸ Hide when publishing (see [ui-parity](ui-parity.md)) |
| Instances pasted into other files follow their mains | done | missing main restorable |
| Component playground (try properties without placing) | partial | variant, boolean and text properties in the Inspect tab and `component.playground`; instance swaps and slots have no control there yet (0.5–1 h) |
| Component playground (try properties without placing) | partial | variant, boolean, text and instance-swap properties in the Inspect tab (the swap menu lists the preferred components first, then every component in the file, as in the Design panel) and `component.playground`; slots have no control there yet |
| Real-world libraries QA | **not checked** | [gaps](gaps.md) #3 |
The behaviour of instances, overrides, variants and swaps is checked against the public
@@ -61,12 +61,12 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`.
| Paint, text, effect, grid styles; style picker; text styles on character ranges | done | behaviour suite: `styles_behaviour_tests.rs` |
| Editing a style updates its layers; detach and delete keep values; editing a styled property unlinks that style | done | an edited fill/stroke/effect/grid list or text chars unlink; applying a style replaces variables bound to what it sets; text styles on text layers only |
| Organising styles: folders by `/` names, order, duplicate, rename folder, Local styles list | done | `style.move` (into a folder renames), `style.duplicate` (right below), `style.renameFolder`; the page's Local styles list by type and folder (drag to reorder, right-click) |
| Variables inside styles (paint, effect, grid and text styles; each user resolves them in its own modes) | done | `style.bind`, `style_vars_tests`; the editor binds paint colours, shadow colours, blur radii and grid sizes, the rest through the command |
| Variables inside styles (paint, effect, grid and text styles; each user resolves them in its own modes) | done | `style.bind`, `style_vars_tests`; the editor binds paint colours, shadow colours, blur radii, grid sizes and a text style's font, font style, size, line height and letter spacing, the rest through the command |
| Variables: colour, number, string, boolean; collections, modes, aliases, scopes, groups | done | behaviour suite: `variables_behaviour_tests.rs`; aliases same type, no loops (also on create); scopes filter pickers only |
| Modes: add, duplicate, rename, delete, set as default / reorder | done | layers on Auto follow a new default; explicit modes stay |
| Deleting a bound variable or its collection | done | layers keep their last value and are unbound; aliases to it take its values |
| Code syntax per platform (Web, iOS, Android) | done | generated CSS uses the Web name; in the variables JSON; SwiftUI/Compose code doesn't name variables yet |
| Bindings: fills, strokes, effects, opacity, padding, gaps, sizes, radius, font size/weight, line height, letter spacing, paragraph spacing/indent, text, visibility, layout grids, instance properties | done | type-checked per field; number variables fill text content; a colour variable bound to a solid paint brings its whole colour, alpha included, once (the paint's opacity restarts at 100% on binding, an opacity set while bound multiplies on top; unbinding moves the alpha back into the opacity) |
| Bindings: fills, strokes, effects, opacity, padding, gaps, sizes, radius, font family/style, font size/weight, line height, letter spacing, paragraph spacing/indent, text, visibility, layout grids, instance properties | done | type-checked per field; number variables fill text content; the Design panel's font family and font style menus start with Apply variable (string variables), and a bound one shows its variable in place of the menu; a colour variable bound to a solid paint brings its whole colour, alpha included, once (the paint's opacity restarts at 100% on binding, an opacity set while bound multiplies on top; unbinding moves the alpha back into the opacity) |
| Explicit modes per frame, library collections' modes | done | |
| Gradient stops and variant properties bound to variables | done | a stop's binding follows it when earlier stops are removed |
| Extended collections | done | library collections, including a library's own extended collections (chains through libraries); overrides per mode with reset; follow library updates, variables new to the library collection being an update of their own; modes inherited. Checked with synthetic libraries only ([gaps](gaps.md) #14) |
@@ -77,6 +77,8 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`.
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | Variables on a text style's fields from the style editor: the font and font style menus start with Apply variable (string variables), and the size, line height and letter spacing fields show a variable button on hover (number variables). A bound field shows its variable in place of the value; a click on it picks another variable and its detach button unbinds it (`style_picker_tests`). Left: the other effect and grid fields (commands only), library variables in the picker. Numbers unchanged. |
| 2026-10-23 | minor | Instance-swap properties in the component playground: the Inspect tab's Playground shows a dropdown for each, with the same menu as the Design panel's (the property's preferred components first, then every component in the file). The component picked changes the preview and the code only, never the file. Left: slots in the playground. Numbers unchanged. |
| 2026-10-20 | minor | A main component's copy within its file is an instance of it (#892): Duplicate (`edit.duplicate`) and Alt-drag on the canvas made a second main; they now place an instance where the copy went (same offset, the instance's default name, selected), as Copy then Paste already did. A variant duplicated in its set stays a new variant. |
| 2026-10-18 | minor | A bound colour variable's alpha applies once (#802): a `#RRGGBBAA` paint keeps its alpha in the paint opacity, and binding a variable with alpha left that opacity in place, so a 50% fill bound to a 50% variable drew at 25%. Binding (`variable.bind`, `node.set` `boundVariables`, `style.bind`) now restarts the paint's opacity at 100% and unbinding (also deleting the variable) moves the colour's alpha back into the opacity; `.fig` and REST imports take the variable's alpha back out of a bound paint's opacity, so `.fig` round trips no longer square it. Colour-picker edits of a colour variable or a plugin colour field, generated CSS strokes and the document colours list counted the paint opacity twice and now count it once (`bound_alpha_tests`). Numbers unchanged |
| 2026-10-18 | minor | Variables import (#790): a `variables` map outside `meta` (or `meta.variables` next to top-level collections) is read, and an import that finds no variables, or fewer than the collections' `variableIds` list, says so in `warnings` instead of succeeding silently |
+42 -20
View File
@@ -41,11 +41,17 @@ still answers every method that acts on the app's state (`engine.execute`, `docu
`node.get`, `ui.inspect`, `ui.pointer`, `ui.key`, `ui.type`, `ui.set`, `ui.render`, `window.list`,
…), in the file that window shows, so an attached agent can edit, inspect and save while the app is
out of sight. The app is not drawn meanwhile and only wakes for requests, at most ten times a
second while minimised, so a request there takes up to about 0.1 s. What only a drawn frame can
answer (`ui.screenshot`, `ui.input`, `window.new`) is an error at once (`window 0 is minimised or
hidden: …`) instead of waiting for the window to show; `ui.render` draws the document without it.
Under Wayland the app can't tell that its window is minimised: where the compositor then stops
drawing it, a request waits for the window and gets the 60 s timeout error.
second while minimised, so a request there takes up to about 0.1 s. A request that needs a drawn
frame (`ui.screenshot`, `ui.input`) is kept queued while the window is hidden and asks it to paint
(`RequestPaintWhileHidden`; native eframe honours that). A screenshot whose window reports no
visibility at all is parked the same way with its deadline, so it cannot wait forever in the
queue. A screenshot settles on three completed, presented UI frames (a discarded sizing pass does
not count), then is issued, and expires once at its 60 s deadline if no paint arrives. `window.new`
remains an error at once (`window 0 is minimised or hidden: …`) instead of waiting for the window
to show; `ui.render` draws the document without it. Under Wayland the app can't tell that its
window is minimised: where the compositor then stops drawing it, a request waits for the window
and gets the 60 s timeout error. The browser's shared eframe runner does not implement
`RequestPaintWhileHidden` yet, so hidden-tab screenshots and injected input are unqualified there.
Coordinates are page units, y down. `x`/`y` in `node.set`/`node.create` are relative to the parent
frame (groups have no coordinate space of their own).
@@ -100,13 +106,17 @@ JSON or the gradient shorthand `{"type":"linearGradient","stops":["#hex","#hex"]
{"id":3,"method":"engine.execute","params":{"command":"command.batch","params":{"label":"Tidy","commands":[{"command":"align.left","params":{"ids":[2,3]}},{"command":"distribute.vertical","params":{"ids":[2,3]}}]}}}
```
Inside `command.batch`, `{"$step": n}` in a step's params stands for the `id` in step `n`'s result
(zero-based; `{"$step": n, "field": "…"}` for another field; any object with a `$step` key is read
as one, and a nested `command.batch` step keeps its own references to its own steps), so one batch
can create a frame and fill it:
Inside `command.batch`, a step can name itself with `"as"` and later steps use its result:
`{"$ref": "name"}` in a step's params stands for the `id` in that step's result, and `{"$step": n}`
for the `id` in step `n`'s result (zero-based). Either takes `"field"` for another key or a slash
path into the result, such as `ids/0` for the first copy `edit.duplicate` returned. Any object with a
`$ref` or `$step` key is read as a reference, names are unique within a batch, and a nested
`command.batch` step keeps its own references to its own steps (see [MCP: Step
references](mcp.md#step-references) for the rules and errors). So one batch can create a frame, fill
it and duplicate it:
```json
{"id":4,"method":"engine.execute","params":{"command":"command.batch","params":{"commands":[{"command":"node.create","params":{"type":"frame","width":200,"height":100}},{"command":"node.create","params":{"type":"rectangle","parent":{"$step":0},"width":20,"height":20}}]}}}
{"id":4,"method":"engine.execute","params":{"command":"command.batch","params":{"commands":[{"command":"node.create","as":"card","params":{"type":"frame","width":200,"height":100}},{"command":"node.create","params":{"type":"rectangle","parent":{"$ref":"card"},"width":20,"height":20}},{"command":"edit.duplicate","as":"copy","params":{"ids":[{"$ref":"card"}]}},{"command":"node.set","params":{"ids":[{"$ref":"copy","field":"ids/0"}],"props":{"name":"Card copy"}}}]}}}
```
### `engine.commands {}`
@@ -131,7 +141,8 @@ component/instance links, overrides, `slot` (a slot layer's property name), boun
`devStatus` (handoff status), `devChanged` (changed since it was marked ready for dev),
`annotations` (Dev Mode notes: `[{label, properties, category?}]`), `codeLinks` (a component's
links to code: `[{language, importPath, name, template}]`), `devResources` (links for developers,
its own and inherited: `[{name, url, index?, from?}]`), `pluginData` (layer data written with
its own and inherited: `[{name, url, index?, from?}]`), `measurements` (Dev Mode measurements
starting from the layer: `[{to} | {side}]`), `pluginData` (layer data written with
`node.set {props: {pluginData: {key: value}}}`, pages included: `{key: value}`, values as strings;
only when there is some). The auto layout props
that `node.set` accepts are listed in [mcp.md](mcp.md#conventions-agents-must-know).
@@ -166,7 +177,8 @@ far the view is panned (`pan`, `panRange`), whether a scroll is still coasting (
active screen's `hotspots` (`[{id, rect: [x0, y0, x1, y1]}]` in window points, bottom-most first),
whether a click where nothing reacts flashes them (`hotspotHints`) and they are flashing now
(`hotspotFlash`), and whether the Inspect panel shows (`inspect`) with the layer it shows
(`inspected`) (`null` when nothing plays). `presentOptions` holds the options the next presentation
(`inspected`), and the link an Open link notice waits to open (`linkNotice`, else `null`) (`null`
when nothing plays). `presentOptions` holds the options the next presentation
starts with (`scale`, `deviceFrame`, `comments`, `hotspotHints`, `inspect`).
`textPreview` is true while a live text preview shows on the canvas (a hovered text style or named
instance, a dragged axis slider; see `text.preview` in [mcp.md](mcp.md)).
@@ -256,7 +268,7 @@ separate calls. Returns `{queued}`.
Invoke a menu item exactly like clicking it (this may open a dialog). Returns `{ok, lastError}`.
### `ui.set {rightTab?, leftTab?, theme?, home?, homeSection?, homeSearch?, homeRecentsView?, homeRecentsSort?, homeRecentsOrder?, palette?, mainMenu?, focusRegion?, colorPicker?, variables?, layerFilter?, collapseLayers?, exportPreview?, preview?, presentScroll?, colorProfileDialog?, stylePicker?, stylePickerEdit?, stylePickerView?, iconGallery?, protoEditor?, contextMenu?, contextSelectLayer?, commentOpen?, commentDraft?, commentFilter?, commentSort?, commentSearch?, commentReply?, commentEdit?, presentComments?, devCompare?, devCompareMode?, devCompareSplit?, annotationFilter?, resetFrameStats?, pluginsWindow?, pluginsTab?, pluginConfirm?, pluginForm?}`
### `ui.set {rightTab?, leftTab?, theme?, home?, homeSection?, homeSearch?, homeRecentsView?, homeRecentsSort?, homeRecentsOrder?, palette?, mainMenu?, focusRegion?, colorPicker?, variables?, layerFilter?, collapseLayers?, exportPreview?, preview?, presentScroll?, colorProfileDialog?, stylePicker?, stylePickerEdit?, stylePickerView?, iconGallery?, protoEditor?, contextMenu?, contextSelectLayer?, commentOpen?, commentDraft?, commentFilter?, commentSort?, commentSearch?, commentReply?, commentEdit?, presentComments?, devCompare?, devCompareMode?, devCompareSplit?, annotationFilter?, resetFrameStats?, pluginsWindow?, pluginsTab?, pluginConfirm?, pluginForm?, devCompareVersion?}`
Switch panels and modes: `rightTab` `design|prototype|inspect`, `leftTab` `file|assets`, `theme`,
`home` (bool), `homeSection` (`recents`, `online` or `templates`: which section the home screen shows, as `ui.inspect` reports it; opening
@@ -293,21 +305,31 @@ closes it), `commentDraft` (`{x, y}`: start a new comment there), `commentFilter
text, focused with the cursor at its end: an `@` there shows the mention picker), `commentEdit` (a
message index of the open thread to edit, 0 for the first; `null` stops), `presentComments` (bool:
the presenter shows the presented frame's comments, also Shift+C there). Dev Mode: `devCompare` (a
ready-for-dev frame or section id opens the Compare changes view, like `dev.compare {id, show:
true}`; `null` closes it) with `devCompareMode` (`sideBySide|overlay`) and `devCompareSplit` (the
frame or section id opens the Compare changes view, like `dev.compare {id, show:
true}`; `null` closes it) with `devCompareVersion` (a version id to compare with; `null`: when it
was marked ready for dev), `devCompareMode` (`sideBySide|overlay`) and `devCompareSplit` (the
overlay slider, 0–1), `annotationFilter` (`all`, `none` for uncategorised, or a category id or name:
the annotations the canvas shows). `resetFrameStats` (true: start `framesDrawn` and
`repaintCauses` over). Returns `ui.inspect` (which reports `stylePicker`,
`stylePickerView`, `comments`: `open`, `draft`, `filter`, `sort`, `search`, `editing` and the
`mentions` the picker offers, `devCompare` (`{id, mode, split}` or `null`), `devFocus` (the frame the
`mentions` the picker offers, `devCompare` (`{id, mode, split, version}` or `null`), `devFocus` (the frame the
focus view shows, or `null`) and `annotationFilter`).
### `ui.screenshot {path?}`
PNG of the whole window (`window` picks one), UI included (needs a visible window; an idle one draws the frame to
capture). Returns `{path, width, height}`, without `path` `{format: "png", width, height, data}`
(base64); an error at once when the window is minimised or hidden, or after 60 s when the window
doesn't deliver.
PNG of the whole window (`window` picks one), UI included (an idle one draws the frame to capture;
a hidden one, or one whose platform visibility is not reported, is asked to paint and the request
is parked with its deadline until it does). Returns
`{path, width, height}`, without `path` `{format: "png", width, height, data}` (base64). Requests
are bounded to four pending, 128 MiB of pixels per image and 256 MiB in flight or
completed-but-undrained. The reservation is the larger of the encoder phase (readback +
straight-RGBA copy + PNG) and the reply phase (readback + PNG + base64); the encoder copy is
dropped before base64 is built, and an oversized encoded reply is refused before base64 is
allocated. The reply carries the request's own id, and the capture belongs to the document and
window it was admitted for; cancellation, a tab switch, a resize/DPI/zoom between issuance and
arrival (retried twice, then an error), an unrepresentable current surface (fail closed), and the
60 s deadline never write late files. A hidden window whose pixels arrived is asked to paint the
pass that drains them, so a completed readback is not left to time out.
### `ui.render {path?, scale?, ids?}`
+15 -9
View File
@@ -1,23 +1,26 @@
# Dev Mode parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (dev resources links; inspect behaviour suite and fixes: colour formats, rem, blend, positions from the frame, CSS for rotation, gradients and text, icons and images as assets; code connect and Dev Mode plugins) · **Target:** Figma Design Dev Mode (desktop app 126.10.7)
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-24 · **Change:** minor (measurements kept on the canvas; a read-only layers panel in Dev Mode; earlier: import lines in generated layer trees; compare with any saved version; Dev Mode behaviour suite: an inspect-only canvas, Changed in the ready list, statuses only on frames and sections; plugin panels from the Inspect tab) · **Target:** Figma Design Dev Mode (desktop app 126.10.7)
**Breadth ~96%, ready ~72% (estimated).** The inspect surface is broad, components connect
to the code components of a repository and layers carry dev resources links; left: import lines
inside generated layer trees. Remaining: 0–1 h.
**Breadth ~98%, ready ~78% (estimated).** The inspect surface is broad, components connect
to the code components of a repository (with their imports, also inside generated layer trees),
changes compare with any saved version, the canvas and the layers panel only inspect in Dev Mode,
and measurements stay on the canvas for everyone. Left: publishing mappings from the code
repository, third-party integrations. Remaining: 1–2 h.
| Feature | Status | Notes |
|---|---|---|
| Dev Mode toggle (Shift+D), Inspect panel | done | |
| Dev Mode toggle (Shift+D), Inspect panel | done | the canvas only inspects: presses select, pan and comment but never move, resize, draw or edit text and vectors; menus and shortcuts refuse design edits with a note (Dev Mode's annotations, statuses, dev resources, measurements, code connections and comments still work); a drawing tool chosen in the toolbar goes back to the design. The layers panel only selects and expands (no renaming, reordering, hiding or locking; a double click says why), and the Pages list only opens pages (no adding, renaming, reordering, duplicating or deleting). Files dropped on the canvas aren't imported (a dropped document still opens as a file of its own), and the Assets panel places, swaps and applies nothing, doesn't change Hide when publishing, and doesn't accept library updates or add, remove or swap libraries; each says why |
| Properties with their styles and variables; redlines on hover | done | positions from the frame around the layer (groups aren't frames), rotation, radius (CSS corner order), opacity, blend; colours in hex, RGB, CSS, HSL or HSB (`inspect.properties {colorFormat}`, the Inspect tab's Color format) |
| Code: CSS, iOS (SwiftUI), Android (Compose) | done | plus HTML, React, Vue and Leptos (ours; Leptos: `#[component]` functions with `#[prop]` defaults, variant enums and `view!` markup); components take properties as parameters, instances are calls; CSS in px or rem (`inspect.code {unit, rootFontSize}`), with rotation, blend modes, gradient directions and kinds (linear, radial, conic), text alignment, case and decoration |
| Assets list with export | done | layers with export settings, icons (small, vector shapes only: SVG) and images (PNG); the icon rule is ours |
| Annotations: pinned properties, categories, coloured callouts, filter | done | |
| Ready for dev list, statuses, "Changed" after edits | done | |
| Ready for dev list, statuses, "Changed" after edits | done | the list filters Ready for dev, Changed, Completed or any, and badges changed ones (`inspect.readyForDev {status: changed}`, `changed` per item); only sections and frames at the top of a page or section take a status (others are refused) |
| Focus view (one frame, previous / next) | done | |
| Compare changes (before/after side by side or overlaid, property diffs) | done | only against the snapshot taken when a frame was marked ready; not against any named version |
| Code connect (map components to repository code) | done | per language (React, Leptos, Vue, SwiftUI, Compose): import path, name and a template with `{property}` and `{property ? "a" : "b"}` placeholders; instances (also inside generated layer trees) show the snippet with their values; edited in the Inspect panel with a live preview; `figcraft.connect.json` export/import. Inside a generated layer tree, connected instances' imports aren't collected |
| Plugins in Dev Mode (Jira, GitHub, etc.) | partial | FigCraft's own WASM plugins that only read run from the Inspect tab; no plugin UI at run time, no third-party integrations |
| Compare changes (before/after side by side or overlaid, property diffs) | done | against the snapshot taken when a frame was first changed after it was marked ready, or any saved version (`dev.compare {version}`; the view's Compare with menu); a frame that isn't ready for dev compares with versions |
| Code connect (map components to repository code) | done | per language (React, Leptos, Vue, SwiftUI, Compose, ArkUI): import path, name and a template with `{property}` and `{property ? "a" : "b"}` placeholders; instances (also inside generated layer trees) show the snippet with their values; edited in the Inspect panel with a live preview; `figcraft.connect.json` export/import; generated layer trees bring the import lines of the connected components they call (also in definitions, slots and swapped-in instances) once each at the top, per language. No CLI publishing mappings from the code repository |
| Plugins in Dev Mode (Jira, GitHub, etc.) | partial | FigCraft's own WASM plugins that only read run from the Inspect tab, with their panels (callbacks can only read); no third-party integrations |
| Measurements (redlines on hover, Alt in the design) | done | distances between the selection and the hovered layer, its parent or a guide, auto layout padding and gaps (Shift+Alt); measurements kept on the canvas (`devMode.addMeasurement` between two layers, along a side or to the layer around it; stored on the layer, saved, synced, undoable) drawn in Dev Mode as labelled redlines that follow the layers, listed, placed and removed in the Inspect tab; a measurement whose other layer was deleted isn't drawn until an undo brings it back |
| VS Code extension | out of scope | FigCraft's MCP server and control channel already serve design data, code and connected snippets to agents and editors |
| Dev resources links on layers | done | a URL and a name per link on any layer (`devResource.*`, saved, synced, undoable); instances list their main component's (and its set's) read-only; URLs checked as http/https and never fetched (no page titles: a link without a name is named after its URL); the Inspect tab lists, adds, edits and removes them; `get_node` returns them |
@@ -25,6 +28,9 @@ inside generated layer trees. Remaining: 0–1 h.
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | Dev Mode refuses design edits from files dropped on the canvas (a dropped document still opens as a file of its own, and the drop overlay shows the Dev Mode note for other files), from the Pages list's add, rename, reorder, duplicate and delete, and from the Assets panel's place, drag, Alt-drop swap, style, Hide when publishing, Update, and adding, removing and swapping libraries. Percentages and hours unchanged |
| 2026-10-24 | minor | Measurements kept on the canvas (gap 16): `devMode.addMeasurement {from?, to?, side?}` places one between two layers, along one side of a layer or (neither) to the layer around it, stored on the layer it starts from (saved in `.figcraft`, synced, undoable; old files open without them); Dev Mode draws them as labelled redlines that follow the layers, the Inspect tab lists them (those of the selected layer, or every one on the page), places and removes them; `devMode.removeMeasurement` and `devMode.measurements` remove and list them; a copy keeps those to layers copied with it. The layers panel is read-only in Dev Mode: rows select and expand, but don't rename, reorder, hide or lock. Breadth 97% → 98%, ready 76% → 78%, 2–4 h → 1–2 h |
| 2026-10-15 | minor | Dev Mode tail (gap 16): generated layer trees (React, Leptos, SwiftUI, Compose) start with the import lines of the connected components they call, once each, also those in a component's definition, in slots an instance fills and placed by a connected template's instance swaps; Compare changes takes any saved version as the before (`dev.compare {version}`, the view's Compare with menu; frames that aren't ready for dev compare with versions). Behaviour suite (5 tests; 4 failed before the fixes): the Dev Mode canvas only inspects (no moving, resizing, drawing, text or vector editing; menus, shortcuts, nudges, opacity keys and paste refuse design edits with a note; toolbar drawing tools go back to the design), the ready list filters and badges Changed items, statuses are refused on layers inside frames. Read-only plugins run from the Inspect tab show their panels (the runtime's panels, reused). Breadth 96% → 97% (measurements kept on the canvas and the read-only layers panel counted as missing), ready 72% → 76%, 0–1 h → 2–4 h |
| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h |
| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes `<text>`), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Dev Mode breadth 90% → 93%, ready 64% → 70%, hours unchanged (1–3, dev resources); feature breadth and ready for real work unchanged at this rounding |
| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–720 h |
+214
View File
@@ -0,0 +1,214 @@
# egui 0.37 local implementation — stage 1 notes and follow-ups
Current candidate font pin: `37827adc41fd165f9a9234ac40f94dbad353aa69` restores the complete prior Latin catalog
alongside the migration fallbacks. Central font tests and license checks passed;
publication is pending approval. Managed run `egui037-figcraft-latin-ci-20261025-0711`
passed with required fonts: five targeted build-input tests plus full CI, including
WASM and plugin builds (3,257 passing executions, zero failures, nine ignored across
72 summaries; repeated tests are counted). This supersedes the optional-font-only
qualification below for the restored catalog.
## Current local qualification — 2026-10-25
Latest combined-source qualification includes main `2a3b7d4fa78e67ecdc7bdfe8b4781e50e5b2612e`, preserving its text-on-path, style-variable, export and Dev Mode changes. Managed full CI `egui037-figcraft-main2a-ci-20261025-0745` passed with required fonts: 3,297 passing executions, zero failures and nine ignored across 71 summaries, including WASM and plugin builds. Fresh independent review accepted the integration. The capture/IME/field-save refactor addresses the prior Sonar complexity findings; remote analysis still needs the approved update. Native/browser, platform IME and paired performance gaps below remain open.
### Earlier review-fix qualification
The published refresh preserves main `9f788753` and passed managed full CI
`egui037-figcraft-main-refresh-ci-20261025-0618`: 3,247 passing executions, zero
failures and nine ignored. The 3,242 counts below describe the earlier `10702311`
qualification, before five additional main-branch tests. These are execution counts,
not distinct regressions. The release font input is under review separately; a
passing optional-font host build does not qualify the required web font catalog.
### Earlier qualification at `10702311`
The migration preserves captured main `10702311f345393ee1d8868c3257679befcfa14d`
(53 commits and 114 paths beyond the first integration). The final review commit records
both the migration history and that upstream target as parents. The selected egui revision is
`6d518fc799681571963842f95b55a6b68431b15a`, winit is
`a517b3846ee8457d84ee4975110bfda6797c44dc`, and craft-window is
`25da1314d443171b4adeaad2cd9454b273f35c6f`. Cargo.lock SHA-256 is
`7ba8af4de70ba657a652266ddee2920bfc501c851520a49ac60c8044a5c23cc1`.
The normal UI enables `default_fonts` and `monochrome_emoji_fonts`.
Managed full CI `egui037-figcraft-final-ci-20261024-2312` passed with bundled fonts.
Managed full CI `egui037-figcraft-system-font-ci-20261025-0402` also passed without
bundled craft-fonts, using a licensed DejaVu Sans system-font fixture via XDG_DATA_HOME.
The final qualification run `egui037-figcraft-final-qualification-ci-20261025-0453`
repeated that configuration successfully after the documentation reconciliation.
Both runs completed formatting, strict lint, workspace tests, doctests, translations,
layering, assets, WASM checks, and plugin fmt/lint/tests/release-WASM builds. Each log
contains 3,242 passing executions, zero failures and nine ignored across 70 summaries;
this includes repeated translation checks and is not a count of distinct regressions.
The main doctest stage selects zero tests. Three plugin bundles were hash-verified and
preserved for each configuration.
The no-bundled-font run includes two test fixture corrections: text-fit cases explicitly
scan installed fonts, and a concurrent collaboration test explicitly selects its foreground
document after both joins complete instead of depending on network completion order.
Independent review accepted these changes with the original layout, synchronization and
isolation assertions intact. All 17 field-save regressions remain covered, including
owner-bound deferred saves, same-pass document replacement and later valid edits/saves.
Incoming IME interruption, screenshot geometry admission, and document/window ownership
are implemented. Historical notes below describe the earlier planning stages.
The final review packet binds source, lock, logs, plugin outputs and commit ancestry.
Native visible GUI/browser workflows, native/custom-editor IME, paired performance,
current-source platform runtime and actual release installation/signing remain open.
The system-font result is not a claim that missing document fonts are interchangeable.
Human review and human merge are required; no publication is authorized by this document.
## What this stage changes
- Dependency family at 0.37 (`egui`, `egui_extras`, `eframe`, the app's `clipboard` feature), with
`ui-egui` enabling the production `default_fonts` fallback and tests explicitly enabling
`default_fonts` + `monochrome_emoji_fonts` (kittest's own dev-features do not propagate to us).
- Mechanical API ports: `Id::new` → `Id::unique`, `Ui::id` → `Ui::scope_id`,
`WidgetType` → `Role`, `MouseWheel::source`, `Shape::Band`, `ScrollArea::clamped_offset`.
- Lifecycle split: `FigApp::background_tick` (logic-safe work: control queues, live polling,
background completions, autosave/recovery deadlines) and `FigApp::ui_setup` (fonts/language/
theme/UI zoom/render worker, once per real frame). Direct `run_ui` harnesses tick the
background once in `FigApp::ui` and clear the marker afterwards, so every direct frame ticks
instead of every other one; `App::logic` ticks it for native/web. Child viewports do not repeat
the root's background work, but each runs `FigApp::viewport_setup` in its own frame: theme and
`ui.dark`, language, fonts (including CJK faces for its own document) and the zoom plan.
- A monotonic `FrameClock` (`web_time::Instant`) drives autosave, auto-save and capture deadlines.
Tests inject it (`set_test_now`); UI-time (`ctx.input(i.time)`) is not used for deadlines.
- Local capture-slot adapter in `crates/ui-egui/src/control.rs` (no `craft-ui` dependency):
weak one-shot callback, per-request identity/serial, deadline/cancel, document + viewport
generation, capture-time geometry epoch (size/DPI/zoom only, not window position), retry on
geometry changes, fail-closed completion when current geometry is unrepresentable, error on
document changes, straight-RGBA PNG reply, and the 4-request / 128 MiB-pixels /
256 MiB-aggregate budget counted over in-flight and completed-but-undrained images. The budget
reserves the larger of the encoder phase (readback + straight-RGBA copy + PNG) and the reply
phase (readback + PNG + base64); the encoder copy is dropped before base64 is built, and a
post-encode reply cap refuses an oversized PNG before the base64 string is allocated. The
request's document and window are fixed at admission and never rewritten at issuance, so a tab
switch while settling is an error rather than the new tab's pixels. Admission and issuance happen
in the UI phase. A hidden request, or one whose platform visibility is not reported, is parked
with its deadline and asks for `RequestPaintWhileHidden`; a completed slot asks again for the
real pass that drains it instead of waiting for the deadline. Settlement counts completed UI
frames, so a discarded sizing pass never serves as a settled capture frame.
- Accessibility: the custom panel edges (`Id::unique(("panel_edge", …))`) no longer sense focus;
`a11y::begin` records the root `Ui::scope_id` so a real `panel.__resize` ID is derivable if a panel
ever becomes resizable. Preferences (`prefs.panel_widths`) and fixed geometry are unchanged.
- Synthetic input from the control channel: one pending batch per window, tagged with the
document it was injected for (dropped if the active file changed), queued in the live pane of the
window drawing now (a child window's batch is not written into the swapped-out default pane),
applied exactly once per raw-input frame (shortcuts return on a discarded pass's rerun instead of
inserting or running again), acknowledged only from the pass that presents it, and fenced from the
user's system clipboard in one place: `FigApp::clipboard_set_text`/`clipboard_set_png` drop every
copy path's write (copy/cut, copy-as-CSS/SVG/PNG/text/link, property/code/comment/join-code
copies) while the frame's input is synthetic. A synthetic `Cut` still deletes the text-edit
selection in the document; only its OS clipboard write is fenced. Acknowledgements belong to the
window whose batch they acknowledge, so an empty child frame cannot error the root's. Focus-epoch fencing for
text/IME batches remains part of the E21b nextstep.
- Incoming IME byte deletion is implemented for the custom text editor
(`figcraft_engine::textedit::delete_surrounding_bytes`, wired from `ImeEvent::DeleteSurroundingBytes`)
and tested for multibyte CJK, a supplementary emoji, deletion-only input and one undo step.
Partial characters are preserved (a byte count ending inside one deletes only the whole
characters it covers, matching the fork and egui's `surrounding_bytes_to_chars`). The selection
itself is kept, as in stock `TextEdit`, so the paired `Commit` replaces it; only the complete
characters before/after the selection go. A delete that finds nothing is a true no-op: it keeps
an in-progress composition (pre-edit), the revision and the document untouched. The older
character-counted `DeleteSurrounding` event follows the same rules and has an app-level test.
## Outgoing IME surrounding text (E21b/P7b)
Implemented for FigCraft's one custom IME producer, `shortcuts::ime_output`, against the reviewed
fork API. The descriptor now carries:
- a stable owner `(document uid, text node)` and the engine's `TextEdit::editing_session` as the
replacement epoch (a document switch or another node changes the owner; re-entering the same
node starts a new generation without reusing the old one);
- the tracker's viewport-scoped `context_generation`, observed exactly once per viewport pass
after the pass's IME events and engine commands. `FigApp::observe_ime` is the single call site
(editor passes, the home/present early returns and the first-font frame all pass through it);
an idle `observe(None)` followed by a focused one in the same pass would drop receipts and bump
the generation, so the early shortcut/palette idles were removed. A late `window.new` that moves
the edited file to another window makes the old viewport observe idle for that pass;
- a bounded (≤ 4000 UTF-8 bytes) committed-text snapshot from
`figcraft_engine::textedit::ime_surrounding`: logical model text, never `bidi::display`, with the
UI-only pre-edit excluded, character caret/anchor converted independently to UTF-8 byte offsets
(reversed selections stay reversed), and an explicit `Unavailable` reason when no edit/text node
exists or the selection alone exceeds the budget. Empty text is `Available("",0,0)`.
- explicit change cause: `InputMethod` only for an accepted `Commit`/`DeleteSurrounding*` receipt
captured immediately around that mutation (delete+commit chains are one receipt chain); plain
typing, paste, keys, text shortcuts, undo, control-channel and AutoCorrect/list AutoFormat
changes report `Other` and dominate the frame. No-op, failed and stale receipts earn nothing.
When the bounded snapshot is unavailable (for example a selection wider than 4000 bytes), both
the pass observation baseline and the pre/post receipt stamps carry the engine's document
revision, so an accepted commit that replaces that selection still earns `InputMethod`.
- `geometry` remains the actual caret (the composition cursor while composing).
A hidden/unfocused/no-editor pass observes `None`, and closing a child window calls
`end_viewport` (now covered by a windows regression). The outgoing text is copied only as the
bounded window; the query never clones the document. Converting the caret/anchor character
indices to UTF-8 bytes now takes one character walk (both offsets) instead of two or three full
scans. Remaining cost: the caret is a character index, so a caret near the end of a huge layer is
still O(n) per query; `ime_stamp` runs the query before and after each text-affecting event, so
such a layer pays O(caret/anchor) per event and at most 4000 copied bytes. The generic edit path
also still counts characters once to clamp the caret and clones the pre-edit document for
normalisation/undo.
Tests: engine (`ime_surrounding` Unicode/reversed selection, pre-edit exclusion, empty/bounded/
too-wide selection, stable owner and session replacement, reverse-selection byte deletion with a
preserved selection, character-counted deletion, no-op composition preservation, list AutoFormat
and normalisation classification, double-click epoch stability) and UI (`ime_tests.rs`: commit vs
typing/mixed causes, transformed commit cause, bare-newline/control no-ops, byte and character
delete+commit, pre-edit, logical UTF-8, empty vs unavailable, owner replacement, double-click
inside the edit, one observation per frame, late `window.new`, discarded pass, two viewports).
These are source tests only; the parent runs the managed build/test commands.
## Web hidden paint
`RequestPaintWhileHidden` is honoured by native eframe but not by the shared web runner, which logs
an unhandled command for hidden tabs. Hidden-browser-tab `ui.screenshot`/`ui.input` therefore remain
unqualified even though the native path now queues and settles them. This is a forkfix prerequisite,
not a passing claim.
## Dependency and lock prerequisites
- The committed root `[patch.crates-io]` table pins the egui family and winit to
immutable published fork revisions. `craft-window` is pinned to the reviewed
craft-libs revision targeting egui 0.37; its optional `egui` feature stays disabled
because FigCraft needs only the frame probe.
- `Cargo.lock` is committed and resolves the canonical Git sources without local
path overrides. Use `--locked`; the earlier parent integration config is not
required to reproduce this graph.
- Fonts remain an optional central `CRAFT_FONTS_DIR` input with native system-font
fallback; no font binaries are added here. Required release-font qualification
is separate from optional-font host CI. Color emoji still needs a separate
`epaint/color_fonts` feature check.
## Qualification gaps and baseline backports/perf operations
Synthetic `ui.input` for deferred child viewports still relies on the existing per-pane queue and
`windows::take_input`; eframe invokes `App::raw_input_hook` for the root update, so injected input
into a child window is not newly qualified here.
Surface generation is not exposed by the pinned eframe/egui API, so the capture epoch covers
logical content size, DPI and zoom but cannot distinguish a same-size surface loss/recreation; that
remains a qualification gap (reported by the sibling capture review).
Remaining lifecycle item from the plan, not changed in this stage: prototype/after-delay/animated-GIF
clocks still advance from `present::show` and the GIF drawing path (UI only). They should move to a
non-drawing step called from `background_tick` so hidden playback advances once per deadline and the
visible pass never repeats it.
Unverified here: compile/tests/clippy/fmt, native X11/Wayland/Windows/macOS, browser WebGPU/WebGL2,
screen readers, hidden-window forced paint, PRIMARY/W15P, and all performance numbers. Baseline
operations to measure once the parent can build: `#8731` text tessellation, `#8736` DX12 pipeline
startup, `#8512` font discovery, `#8763`/PR `#8764` TextEdit click-away, `#8748` hidden deferred
children. None of these were applied, removed or claimed as fixed.
## Required web font catalog
Native optional-font builds retain the system-font policy. A required web build must
embed Inter Regular and SemiBold plus Noto Sans Georgian Regular after filtering;
missing required face/style coverage fails the build, regardless of which manifest supplies it. Required mode
also rejects an unset `CRAFT_FONTS_DIR`. The migration's original `b076417` catalog
lacks Inter and is not a qualified required-web input. A combined central font pin restores the prior Latin catalog while preserving the new
core fallbacks. Source Sans 3 and Source Serif 4 are excluded on every target; no font binary belongs in this repository. Required-web, required-native
and optional-font configurations remain separate validation gates.
File diff suppressed because it is too large. Load diff
+19 -11
View File
@@ -1,6 +1,6 @@
# File format parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (REST import keeps slot layers and instances' slot content (#792); REST import reads grid track sizes (#465); `.fig` import fidelity from the community batches, budgets for large files, the import report, a `.fig` copy from Save as, paste from another design app; instance layers' size and resizing and Hug on import (#731, #732); SVG/PDF export depth refusal (#208); `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; `.fig` export: written files checked by opening them in other software; experimental `.fig` export; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; embedded image colour profiles; `.fig` import, best effort; animated GIF fills and TIFF images; last major: created, formats from the installed Figma's Info.plist and help center, ours from source) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (`.sketch` import, best effort, Sketch 48 and newer; the PNG row names ignore overlapping layers and where include bounding box is offered; the Fonts table's missing-font row describes the Missing fonts window; `.figcraft` carries the file kind (boards); `.fig` import: resized instances and variable modes by asset key; atomic saves; auto-save; SVG export clip of nested frames; SVG/PDF export depth refusal (#208); `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; `.fig` export: written files checked by opening them in other software; experimental `.fig` export; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; embedded image colour profiles; `.fig` import, best effort; animated GIF fills and TIFF images; last major: created, formats from the installed Figma's Info.plist and help center, ours from source; REST import keeps slot layers and instances' slot content (#792); REST import reads grid track sizes (#465); `.fig` import fidelity from the community batches, budgets for large files, the import report, a `.fig` copy from Save as, paste from another design app; instance layers' size and resizing and Hug on import (#731, #732)) · **Target:** Figma Design (desktop app 126.10.7)
Every format Figma reads or writes, with FigCraft's support. Sources: `Info.plist` of
`/Applications/Figma.app` (document types only; nothing inside the bundle's code was read), Figma's
@@ -18,9 +18,10 @@ and prototype interactions are still unchecked there.
| Format | Figma | FigCraft read | FigCraft write | Fidelity / testing | Hours |
|---|---|---|---|---|---|
| `.fig` (Figma Design, `com.figma.document`, `application/x-figma`) | native (cloud; "Save local copy") | **yes, best effort** (`file.open`, `file.import`, drop, CLI, MCP) | **experimental** ([below](#export-to-fig-experimental)) | `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps; outlines when the network is missing or damaged), auto layout (grid included; a flow that records no primary-axis sizing hugs on that axis, an instance that records no auto layout has its main's), constraints, ellipse arcs, boolean operations, components, sets (with their descriptions), instances (expanded from their main with overrides, derived layout and property values; the size and resizing their layers override stay overrides), styles, variables with modes and bindings, prototyping (set-variable, set-variable-mode and conditional actions included) and image fills from the archive come across; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers come in as the closest frames, text layers and vectors, with a warning; anything else is a warning. Since 2026-10-17 (community batches #727, #728): an instance's own identity and the cached library components a file carries, overrides applied by key and order, local text values and instance overrides over a shared text style, grid track sizes (fixed, content-sized, fill), slot content, labels of connectors saved with their drawn path (a static snapshot; the ends aren't live), skip ink, paragraph options and hanging lists, list start numbers, vertical trim, per-point stroke joins, editable text on a path, image adjustments and images carried in the message. Budgets come from the file's own size (one value per compressed byte, 20M–128M, lower ceilings on 32-bit targets; the layer cap follows), so a 15.4 MB synthetic file with 300,000 layers opens whole; the file's bytes stay out of the command journal; an import report lists what was left out or changed once the file opens. Not read: elbowed or curved connectors without a saved path (drawn straight) and labels of connectors without a saved snapshot, table cells beyond their own layers, filled areas with their own fills, variables bound to fields the editor can't bind, video fills. Tested on fixtures generated in code (`crates/format/src/fig/tests.rs`, `tail_tests.rs` and the per-feature `*_tests.rs` beside them, fuzzed) and by reading back what the export writes; no real-world files yet | read tail 5–10, write: checks against real files 5–15 |
| `.jam` (FigJam), `.deck` (Slides) | native to other Figma products | no | no | out of scope | — |
| `.sketch` (Sketch 48 and newer) | import (file browser ▸ Import or drop, desktop app too; converted to a Figma file, symbols as components; per the help center) | **yes, best effort** (`file.open`, `file.import`, drop, CLI, MCP; Open With from Finder, Explorer and Linux file managers once installed; recent files show its `previews/preview.png`) | no (out of scope) | zip + JSON read from the public format description; Sketch 43–47 (text as binary property lists) and older (SQLite) files are refused with a message to resave them in a newer Sketch. Pages, artboards as frames with their background, groups; Sketch 2025 and newer: frames (clipping or not) with their style, graphics, stacks as auto layout (direction, gap, alignment, padding, wrap, hug/fill sizing, items ignoring the stack), corners in the style (rounded, smooth with its smoothing; angled, inside square and inside arc as paths), pins and sizing, per-side borders, lists of blurs (progressive background blurs, glass), inner shadows in the shadow list, tints (a graphic's, group's or instance's fill colouring its layers, and colour overrides), text colour and visibility overrides, text sized by its sizing; rectangles with corner radii, ovals, stars and polygons while their points are the generated ones, paths as editable vector networks, shape groups as their shape, as one compound path (children without an operation, by the group's winding rule) or as boolean operations (mixed operations nested from the left); open paths filled as if closed, edited ovals and rectangles as paths, bitmaps (with their colour adjustments as image filters), slices, hotspots; transforms (rotation, flips after it), resizing constraints; fills (solid, gradients with Sketch's geometry, image patterns), borders (the first enabled border's width and position), shadows, inner shadows, layer and background blurs, opacity and blend modes; clipping masks (an outline mask that shows its own style draws a copy under itself, as Sketch does; alpha masks only mask; broken mask chains wrapped), progressive opacity (Fade) as an alpha mask of its gradient; text with styled runs (UTF-16 ranges), paragraph and vertical alignment, lines placed by Sketch's line spacing behaviour, box sizing, fonts found by their PostScript names among the installed ones (else the family read from the name), variable font axes, Sketch's natural line (ascent and descent rounded, no line gap, a fifth more for classic Mac fonts) for text without a fixed line height; symbols as components (library copies on a page of their own) and instances carrying their masters' layers with overrides (text, nested swaps and hiding, images, layer and text styles, nested paths), laid out for their size the way Sketch does it (proportional sharing per pins from format 110, scaling before) and still live; master backgrounds in instances only where Sketch shows them; Apple's system fonts found under the names designs use, with the platform's tracking kept as letter spacing; shared layer styles as paint (fills, border colours) and effect styles, shared text styles as text styles; Color Variables (also library ones) as colour variables with bindings; prototype links, flow starts, export options, layout and square grids (hidden, as Sketch shows them only in its layout view). Instances of a stack laid out at their size and texts an override changed measured again on open. Warnings: noise fills, motion and zoom blurs (drawn as plain blurs), fonts embedded with their data (not used), Smart Layout (kept as laid out), locked overrides, scaled instances. Tested on fixtures built in code (`crates/format/src/sketch/*_tests.rs`, fuzzed) and locally (`FIGCRAFT_SKETCH_SAMPLES`) on one real Sketch 50 file and the public Apache-2.0 IBM Carbon Design Kit saved by Sketch 68 and 83 (70,000 layers open in ~3 s, debug build; its edition saved by Sketch 47 is refused, as it should be); compared artboard by artboard with Sketch 2026's own renders (`FIGCRAFT_SKETCHTOOL`, black-box): the Sketch 50 file's 73 artboards differ in at most 2.3% of their pixels (0.8% on average) (text metrics, fonts FigCraft doesn't find, antialiasing), Carbon 11's 69 1.1% on average and at most 4.4% (text in IBM Plex, which the file embeds and FigCraft doesn't use yet), a Sketch 64 file's 65 artboards 2.1% on average, a Sketch 2026 showcase file's 16 frames 0.9% on average (at most 1.9%) | 5–10 (Smart Layout as auto layout, motion and zoom blurs, several borders of different widths; fonts embedded in the file once `.figcraft` documents can carry fonts: until then they would only last the session) |
| `.jam` (whiteboards), `.deck` (Slides) | native to other Figma products | no | no | `.jam`: not read or written (boards are FigCraft files, see [boards-parity](boards-parity.md)); `.deck` out of scope | — |
| REST API file JSON (`GET /v1/files/:key`) + local variables JSON + `images/` folder | the API's output | **yes** (`file.open`, `file.import`, CLI) | no | pages, layer tree, geometry, paints, strokes, effects, text runs, vectors, auto layout (sizing modes left out read as hug, as documented; grid track sizes from `gridColumnsSizing` / `gridRowsSizing`: px, fr, auto, `minmax(0, 1fr)`, `repeat()`), constraints, grids, components, sets, instances with overrides (sizes and resizing included), component properties, slots (`SLOT` layers bound to their slot property; an instance's slot keeps its content as the instance's own unless its property value is the main's default content; slot limits and preferred components are warnings), styles, prototyping (incl. conditionals and variable modes), variables with modes, aliases, scopes and bindings, image adjustments (the same tint sign as `.fig`); the import report after opening; warnings for what's skipped (video triggers and actions, modifier keys). Unit tests in `crates/format/src/rest/*_tests.rs` | — |
| `.figcraft` (ours: zip of `document.json` + images, versions inside) | — | yes | yes | streamed JSON, 50k layers save ~0.09 s / load ~0.16 s (`cargo xtask bench`); old files keep opening (`large_tests.rs`) | — |
| `.figcraft` (ours: zip of `document.json` + images, versions inside) | — | yes | yes | streamed JSON, 50k layers save ~0.09 s / load ~0.16 s (`cargo xtask bench`); old files keep opening (`large_tests.rs`); saves write a unique staging file and rename it into place (#284); optional auto-save of files with a path (`pref.autoSave`, #305); `kind: "board"` marks a board (left out for design files, so files from before boards are design files; the home screen reads it for the file's mark); board objects keep their `board` metadata on the layer; Dev Mode measurements are kept on the layer they start from (`measurements: [{to} | {side}]`, left out when there are none, so older files open with none) | — |
| Local library files | team libraries in the cloud | yes (enable a `.figcraft` as a library) | yes (publish) | per-asset versions, updates with overrides kept | see [collaboration-parity](collaboration-parity.md) |
## Import (place / paste / drop)
@@ -49,9 +50,9 @@ export limit, not a native document nesting limit.
| Format | Figma | FigCraft | Notes | Hours |
|---|---|---|---|---|
| PNG | yes, scales/suffixes | yes | export settings per layer, presets (`2x`, `512w`, `512h`), preview, "Export N layers"; contents only (off: every layer showing in the area) and include bounding box (the layer's own box); slashes in layer names export into folders. The exported area is what the layer paints: shadows towards their offset, outside and centre strokes, unclipped children, a mask group's mask, nothing for hidden layers; slices export everything under them; sides round up to whole pixels | — |
| PNG | yes, scales/suffixes | yes | export settings per layer, presets (`2x`, `512w`, `512h`), preview, "Export N layers"; ignore overlapping layers (on by default; off: every layer showing in the area) and include bounding box (the layer's own box; the Export section offers it for text layers, and `file.export` and `export.update` take it for any layer); slashes in layer names export into folders. The exported area is what the layer paints: shadows towards their offset, outside and centre strokes, unclipped children, a mask group's mask, nothing for hidden layers; slices export everything under them, except that with ignore overlapping layers on a slice inside a frame or group exports only that frame or group; sides round up to whole pixels | — |
| JPG | yes | yes | white where transparent; fixed quality (90) | — |
| SVG | yes (outline text option, ids) | yes | alpha/luminance masks kept; noise, texture, progressive and background blur, glass embedded as images; outline text (off: `<text>` with one `<tspan>` per line, one style per layer), unique id attributes from layer names, explicit depth-limit refusal, simplified strokes (stroke attributes for centred strokes and for inside and outside strokes on boxes and ellipses) | — |
| SVG | yes (outline text option, ids) | yes | alpha/luminance masks kept; noise, texture, progressive and background blur, glass embedded as images; outline text (off: `<text>` with one `<tspan>` per line, one style per layer), unique id attributes from layer names, explicit depth-limit refusal, nested frames clipped by their parent's clip (#327), simplified strokes (stroke attributes for centred strokes and for inside and outside strokes on boxes and ellipses) | — |
| PDF | yes | yes | one page per frame, real selectable text in subset fonts, links, vectors and gradients as vectors, effects as images; bleed, crop/bleed marks, page boxes, naive CMYK; checked by rendering the PDF back against PNG exports; explicit depth-limit refusal | CMYK output intent 2–4 |
| WebP | — | yes (ours) | | — |
| Copy as CSS / SVG / PNG / text / link | yes | yes | Copy as PNG: the whole selection at 2x | — |
@@ -115,17 +116,17 @@ each.
properties, arc fields, font weight: written as the values they give), text styles on part of a
text (written as values), variables' publishing, code syntax and inheritance settings, extended
collections (written as collections of their own), comments, Dev Mode annotations, statuses,
resources and code links, plugin data, ruler
resources, measurements and code links, plugin data, ruler
guides, prototype device settings, slot properties and slot content, brush
strokes (written plain), strokes wider on one side of their path (written as filled outlines, or
with the width on both sides when the shape has a fill too), pattern fills, WebP export settings (written as PNG),
image adjustments, glass settings other than frost, springs other than Gentle (written as ease
out), set-variable, set-variable-mode and conditional actions, scroll-to offsets, text decoration
looks, variable font axes, text on a path, nested list levels, list spacing, hanging punctuation,
leading trim, OpenType features, per-layer operations inside boolean groups, grid tracks of fixed
or content size, different stroke ends on lines. Not written at all: stroke outlines of dashed and
per-side strokes and of inside or outside strokes on polygons, stars and vectors, glyph outlines
of text (a reader lays text out with its own fonts), version history and branches.
leading trim, OpenType features, per-layer operations inside boolean groups, different stroke ends
on lines. Not written at all: stroke outlines of dashed and per-side strokes and of inside or
outside strokes on polygons, stars and vectors, glyph outlines of text (a reader lays text out
with its own fonts), version history and branches.
- **Checked by opening written files in other software:** the example documents and a small test
file open there, and layers, fills, gradients, strokes, shadows, vectors, components and text draw
as in FigCraft. A file saved again there and read back keeps the layer tree and properties; two
@@ -159,12 +160,19 @@ each.
| Local fonts | yes (font helper / desktop app) | yes (system scan) |
| Bundled/shared fonts | Google Fonts in the cloud | optional `CRAFT_FONTS_DIR` build input (storytold/craft-fonts) |
| Variable fonts | yes | yes (axes, named instances, per-range axis values) |
| Missing-font replacement on open | yes | warnings only (REST import) |
| Missing-font replacement on open | yes | listed, not replaced: in the desktop app the Missing fonts window opens after a file that uses them opens and from Text ▸ Missing fonts (`text.missingFonts`; `figcraft-cli info` prints `missing font` lines), and their text draws in a fallback font |
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | patch | PNG: the export settings were listed as "contents only" and "include bounding box (the layer's own box)"; the row now names ignore overlapping layers, on by default, and says that the Export section offers include bounding box for text layers (#1307), while `file.export` and `export.update` take it for any layer. Numbers unchanged |
| 2026-10-24 | patch | Fonts: the missing-font row said "warnings only (REST import)"; it now names the Missing fonts window (#752), `text.missingFonts` and the `missing font` lines of `figcraft-cli info`. Replacing a missing font is still missing. Numbers unchanged |
| 2026-10-24 | minor | Slices and contents only (Figma's Ignore overlapping layers): with it on, a slice inside a frame or group exports only that frame or group and the layers in it, and its Export preview shows the same; with it off, or for a slice on the page or in a section, the slice exports everything in its area as before |
| 2026-10-24 | minor | Boards (M12): `.figcraft` saves the file kind (`kind: "board"`, absent for design files) and each board object's `board` metadata; the `.jam` row points at boards |
| 2026-10-21 | minor | `.sketch` import (best effort): files saved by Sketch 48 and newer open and import like `.fig` files (open, import, drop, CLI, MCP), mapped onto the REST importer: layers, paths, shape groups and booleans, styles and effects, masks, text, symbols with overrides (live instances), shared styles, Color Variables, prototype links, export settings and grids; older files are refused with a message. Sketch 2025 and newer: frames, graphics, stacks as auto layout, corners, pins and sizing, tints, per-side borders, blur lists. Checked on fixtures and locally on real Sketch 50, 64 and 2026 files and the IBM Carbon Design Kit (Sketch 68 and 83 open; the Sketch 47 edition is refused). Numbers unchanged |
| 2026-10-15 | minor | `.fig` import (#279, #281; #266, #267): resized instances lay out their layers by their constraints, and explicit variable modes whose collection is named by its asset key come across (before, they were dropped and bound layers resolved in the default mode); fixture-tested, real files still owed. `.figcraft`: atomic saves and exports through unique staging files (#284), optional auto-save (#305). SVG export keeps a nested frame's fill and stroke inside its parent's clip (#327). Numbers unchanged |
| 2026-10-20 | minor | `.fig` export writes grid track sizes (`gridColumnsSizing` / `gridRowsSizing`): fixed px, fractional fill and content-sized tracks keep their sizes and come back through our own importer (#465). Tracks whose size the format can't hold (a negative or out-of-range value) keep their id and read back equal, with a warning. Numbers unchanged |
| 2026-10-18 | minor | REST file JSON import keeps `SLOT` layers (#792): they were dropped with everything placed in them, which emptied every instance's slot. A slot in a main component becomes a slot frame bound to its `SLOT` property (the one it references, else the one named like it, else the only one left), and an instance's slot holds the instance's own content, kept across syncs, moves of the mains and saving, unless the instance's property value is the main's default content (without values, a slot that holds layers keeps them). Instances in slot content link to their own mains. Slot content limits, preferred-only and preferred components are warnings. Synthetic files only. Numbers unchanged |
| 2026-10-17 | minor | REST file JSON import reads grid track sizes from the CSS track strings `gridColumnsSizing` / `gridRowsSizing` (fixed px, fill fr, hug `auto`, `minmax(0, 1fr)`, `repeat()`; other tracks stay equal) (#465). Numbers unchanged |
| 2026-10-17 | minor | The community batches #727 and #728 in the `.fig` row: instance identity and cached library components, overrides by key, text style precedence, grid track sizes, slot content, labels of connectors saved with their path, skip ink, paragraph options, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments (also REST), images in the message; budgets from the file's size; the journal keeps no file bytes; the import report. Not read cut to what's left. A `.fig` copy from Save as. Pasted content: layers copied from another design app, best effort (tested only with clipboard HTML made from our own export), and images under Wayland (not run there). All on synthetic files: summary and hours unchanged |
+109 -25
View File
@@ -1,6 +1,6 @@
# Where FigCraft falls short of Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (the 2026-10-17 landings: `.fig` import fidelity, budgets for large files, the import report, a `.fig` copy from Save as and paste from another design app (gap 1); menus, the Actions palette, editor fixes, Selection colors and smart-selection spacing (gap 4); catalog counts re-measured (gap 5); bounded canvas pan overscan; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (gap 17: branches in live sessions, branch history entries, the list-merge rows brought up to date; gap 6: plugin widgets take text input and a property menu (plugin API 4); gap 24: board templates and tables; gap 24: a shared timer and voting sessions; gap 24: the board dot grid, connector ends dragged to re-attach, curved connectors; egui/eframe 0.37 verified for gap 18; known gaps from the #1311 review in gaps 4, 14, 15 and 16; gap 16: measurements kept on the canvas, a read-only layers panel in Dev Mode; gap 24: boards and their tail; Dev Mode tail: import lines in layer trees, compare with versions, an inspect-only canvas; gap 5: reviewed Turkish, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail; the 2026-10-17 landings: `.fig` import fidelity, budgets for large files, the import report, a `.fig` copy from Save as and paste from another design app (gap 1); menus, the Actions palette, editor fixes, Selection colors and smart-selection spacing (gap 4); catalog counts re-measured (gap 5); bounded canvas pan overscan; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap libraryLine truncated
Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from
doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The
@@ -18,8 +18,8 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
| 2 | Multiplayer and sharing are a preview, without accounts (**beta blocker**) | Feature | 42–84 | [collaboration-parity](collaboration-parity.md) |
| 3 | Nothing checked against real Figma files | Fidelity | 15–30 | [target-app-parity](target-app-parity.md) |
| 4 | Interaction fidelity not compared tool by tool | UI/UX | 40–60 | [ui-parity](ui-parity.md) |
| 5 | Localization: English, reviewed Russian, plus unreviewed Spanish (and Latin American Spanish), French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Indonesian and Czech drafts; Turkish and Swedish catalogs | Localization | 29–51 | [localization-parity](localization-parity.md) |
| 6 | Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run | Ecosystem | 8–26 | [target-app-parity](target-app-parity.md) |
| 5 | Localization: English, reviewed Russian and Turkish, plus unreviewed Spanish (and Latin American Spanish), French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Indonesian and Czech drafts; Turkish and Swedish catalogs | Localization | 29–51 | [localization-parity](localization-parity.md) |
| 6 | Plugins: widgets without hover or timers, a self-hosted directory but no public catalogue; Figma's plugins don't run | Ecosystem | 6–22 | [target-app-parity](target-app-parity.md) |
| 7 | CPU canvas where Figma uses the GPU | Hardware/performance | 30–60 | [hardware-parity](hardware-parity.md) |
| 8 | Platform QA: Linux and the browser left (Windows pass done 2026-10-11) | Stability/platforms | 10–20 | [hardware-parity](hardware-parity.md) |
| 9 | Figma Draw: pen pressure on macOS/Linux tablets (multi-edit, brushes, pressure, text on a path and pattern fills landed) | Feature | 1–3 | [target-app-parity](target-app-parity.md) |
@@ -29,14 +29,15 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
| 13 | Vector tail: none left (boolean hit testing on the result landed 2026-10-11) | Feature | — | [target-app-parity](target-app-parity.md) |
| 14 | Variables: extended collections checked on real libraries (the tail landed 2026-10-12) | Feature | 0–1 | [components-parity](components-parity.md) |
| 15 | Prototyping: media triggers and actions; controllers on macOS, Linux and FreeBSD (controller buttons on key/gamepad triggers, widgets in prototypes landed) | Feature | 2–5 | [prototyping-parity](prototyping-parity.md) |
| 16 | Dev Mode: import lines inside generated layer trees; the editor extension is left to the MCP server (code connect, Dev Mode plugins and dev resources landed) | Feature | 0–1 | [dev-mode-parity](dev-mode-parity.md) |
| 17 | Branching: branches in live sessions, lists other than fills, strokes and effects merged whole (a branch's own version history and per-item list merges landed 2026-10-12) | Feature | 1–2 | [collaboration-parity](collaboration-parity.md) |
| 16 | Dev Mode: publishing mappings from the code repository; the editor extension is left to the MCP server (measurements kept on the canvas and a read-only layers panel landed 2026-10-24) | Feature | 1–2 | [dev-mode-parity](dev-mode-parity.md) |
| 17 | Branching: text runs merged whole, branches only their author sees in a live session (branches in live sessions, export settings and guides item by item, and history entries for creation and updates landed 2026-10-24) | Feature | 1–2 | [collaboration-parity](collaboration-parity.md) |
| 18 | Display P3 on screen: blocked upstream (egui-wgpu picks an 8-bit sRGB surface); gamut warning and soft-proof sRGB landed, as did several windows | Hardware | 4–8 | [hardware-parity](hardware-parity.md) |
| 19 | AI features | AI | 30–60 | [target-app-parity](target-app-parity.md) |
| 20 | Mobile apps (view, mirror, comment) | Platforms | 20–40 | [hardware-parity](hardware-parity.md) |
| 21 | Menu items without a command: none left (landed 2026-10-11) | Feature | — | [parity-checklist](parity-checklist.md) |
| 22 | Screen readers never run; browser has no accessibility tree | UI/UX | 4–8 | [ui-parity](ui-parity.md) |
| 23 | CMYK output intent for print PDFs | File format | 2–4 | [file-format-parity](file-format-parity.md) |
| 24 | Boards: the whiteboard tail (widgets, mind maps, waypoints…) | Feature | 20–44 | [boards-parity](boards-parity.md) |
## Entries
@@ -151,7 +152,9 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
### 4. Interaction fidelity not compared tool by tool
- **Missing:** a side-by-side pass of every tool and panel: cursors, modifier keys while dragging,
hover states, popovers, menus, snapping thresholds.
hover states, popovers, menus, snapping thresholds. Known: the Assets panel's export preview
follows a slice's first export setting, so a second setting with the other Ignore overlapping
layers value previews differently from what it exports.
- **Landed (2026-10-11):** behaviour suites written from the public documentation for auto layout,
selection and transform, text, and canvas navigation with rulers, guides, layout grids, outline
mode and measuring, then (2026-10-12) comments and version history, and the layers and Assets
@@ -178,7 +181,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
- **Impact:** Figma users notice the feel within minutes; it decides whether they stay.
- **Estimate:** 40–60 h. Detail in [`ui-parity.md`](ui-parity.md).
### 5. Localization: English, reviewed Russian, plus unreviewed drafts
### 5. Localization: English, reviewed Russian and Turkish, plus unreviewed drafts
- **Missing:** native reviews of the Spanish, French, German, Brazilian Portuguese, Japanese,
Korean, Simplified Chinese, Indonesian and Czech drafts and the Latin American Spanish
overlay, the strings added since each catalog was drafted (1–5% of each), Hindi, Arabic and
@@ -204,7 +207,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
[`localization-parity.md`](localization-parity.md); how to add one in
[`translating.md`](translating.md).
### 6. Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run
### 6. Plugins: widgets without hover or timers, a self-hosted directory but no public catalogue; Figma's plugins don't run
- **Landed (#101):** sandboxed WebAssembly plugins (wasmi, metered, memory cap, no WASI, also in
the browser) acting through the command registry, one undo step per run, manifest permissions,
Plugins menu, Run last plugin, Manage plugins, `plugin.*` commands, a Rust SDK and two examples
@@ -221,15 +224,20 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
key and checked in full (`figcraft-cli plugin publish`), browsed, installed after a permissions
confirmation and updated from Plugins ▸ Manage plugins ▸ Browse, downloads checked against their
SHA-256, `plugin.directory.*` for scripts and MCP, moderation with `figcraft-server plugins`.
Widget fields and property menus (plugin API 4, 2026-10-24): a widget's text layer can be an
editable field, edited on the canvas with a double-click like any text or with `widget.setText`
(the value, cleaned and capped, goes into the widget's state and the plugin redraws; one undo
step), and a widget can carry a property menu of actions, toggles and colour choices, shown in a
small bar while it's selected and driven with `widget.menu`; the Counter example uses both.
- **Missing:** a public catalogue and Community files, publisher accounts and signatures, ratings
and install counts, background update checks, the directory in the browser build; widgets take only clicks (no
text input, property menus, hover or timers) and draw only frames, text, shapes and images;
and install counts, background update checks, the directory in the browser build; widgets have no hover or
timers, can't open a panel and draw only frames, text, shapes and images;
no images or sliders in panels. Figma's own plugins are JavaScript against Figma's plugin API and
don't run.
- **Impact:** teams that depend on specific Figma plugins (icons, content, accessibility checks)
can't bring them. FigCraft's CLI, control channel and MCP server cover agent automation better
than Figma does.
- **Estimate:** 8–26 h for the missing pieces of FigCraft's own API. Running Figma's plugins
- **Estimate:** 6–22 h for the missing pieces of FigCraft's own API. Running Figma's plugins
needs a JS engine and an API shim: **owner decision needed**.
### 7. CPU canvas where Figma uses the GPU
@@ -308,11 +316,15 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
- **Estimate:** 5–10 h (video playback in the presenter is most of it; it can reuse the GIF clock).
### 13. Vector tail: none left
- **Landed (2026-10-22):** the transform box around points selected in several layers at once, in
the page's axes: its corners and edges scale them, outside a corner rotates them and inside drags
them, each in its own layer and one undo step for all (`transform_tests`).
- **Landed (2026-10-12):** several Bezier handles selected with Shift-click and dragged together,
sharp points beside curved segments rounded, a stroke cap and join per point (canvas, PNG, SVG,
PDF, outline stroke), the Pen outside edit mode connecting to its own points and continuing a
selected open path. Left: corner smoothing beyond rectangles and frames, the transform box around
points of several layers, per-point caps and joins on dashed, variable-width and brush strokes.
selected open path. Left: corner smoothing on vectors, corner radius and smoothing on boolean
groups that contain rectangles, per-point caps and joins on dashed, variable-width and brush
strokes.
- **Landed (2026-10-11):** boolean groups are clicked where their result is (a click in a
subtracted hole reaches what's behind; with the group or an operand selected its operands are
hit, so each can still be picked; "Select layer" lists the group only where its result is), live
@@ -337,7 +349,8 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
collection (a brand theme extending a core one) can be extended again, its values flowing down
the chain as each file accepts updates, every level's overrides kept.
- **Missing:** checked with synthetic files only; extending a collection of the same file isn't
offered (extending takes library collections).
offered (extending takes library collections). A paint style's color offers variables scoped to
any fill or stroke: FigCraft's own rule, since the help center doesn't say which scopes it takes.
- **Estimate:** 0–1 h.
### 15. Prototyping: media triggers and actions, controllers off Windows
@@ -352,11 +365,14 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
importer warns and skips them, `crates/format/src/rest/proto.rs`); controllers on macOS, Linux
and FreeBSD (the usual Rust controller library links libudev on Linux; a pure-Rust reader per
platform, or an owner decision on that dependency); PlayStation and Switch button labels.
`proto.linkNotice` from the Actions palette or the control channel changes the preference in
memory only; it reaches the preferences file with the next preferences save (the notice's own
"Don't show this again" saves at once).
- **Estimate:** 2–5 h, the media part after video fills (gap 12).
### 16. Dev Mode: the tail after dev resources
- **Landed (2026-10-11):** code connect: a component or set links to its code component per
language (React, Leptos, Vue, SwiftUI, Compose): an import path, the name and a template whose `{property}`
language (React, Leptos, Vue, SwiftUI, Compose, ArkUI): an import path, the name and a template whose `{property}`
and `{property ? "a" : "b"}` placeholders take the instance's values, escaped per language and
never executed. `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet
instead of generated code (inside generated layer trees too); the panel's Connect to code edits
@@ -371,11 +387,31 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
host, no spaces, capped) and never fetched: a link added without a name is named after its URL,
and a link opens only when clicked. The Inspect tab lists, adds, edits and removes them;
`get_node` / `inspect_document` return them as `devResources`.
- **Missing:** import lines for connected instances inside a
generated layer tree (only the selected layer's own import is shown); publishing mappings from
the code repository with a CLI. An editor extension is out of scope: the MCP server already
serves design data, code and the connected snippets to agents and editors.
- **Estimate:** 0–1 h.
- **Landed (2026-10-15):** generated layer trees (React, Leptos, SwiftUI, Compose) start with the
import lines of the connected components they call, once each, also those in a component's
definition, in the slots an instance fills and placed by a connected template's instance swaps.
Compare changes takes any saved version as the before (`dev.compare {version}`; the view's
Compare with menu), for any top-level frame or section. A Dev Mode behaviour suite: the canvas
only inspects (presses select, pan and comment; no moving, resizing, drawing, text or vector
editing), the menus, shortcuts, nudges, opacity keys and paste refuse design edits with a note
while Dev Mode's own edits still run, drawing tools from the toolbar return to the design; the
ready list filters and badges Changed items (`inspect.readyForDev {status: changed}`); statuses
are refused on layers inside frames. Read-only plugins run from the Inspect tab show their
panels.
- **Landed (2026-10-24):** measurements kept on the canvas: between two layers, along one side of
a layer or to the layer around it (`devMode.addMeasurement` / `removeMeasurement` /
`measurements`), stored on the layer they start from (saved, synced, undoable; old files open
without them), drawn in Dev Mode as labelled redlines that follow the layers, and listed, placed
and removed in the Inspect tab. The layers panel is read-only in Dev Mode (rows select and
expand; no renaming, reordering, hiding or locking).
- **Landed (2026-10-24):** files dropped on the canvas, the Pages list and the Assets panel no
longer edit the design in Dev Mode (a dropped document still opens as a file of its own, and
the Pages list only opens pages).
- **Missing:** publishing mappings from the code repository with a CLI. An editor extension is out
of scope: the MCP server already serves design data, code and the connected snippets to agents
and editors. The file name field in the left panel still renames the file in Dev Mode
(`file.rename`); whether Figma allows that in Dev Mode needs checking before it changes.
- **Estimate:** 1–2 h.
### 17. Branching and merging
- **Landed (2026-10-11):** branches of a file (`branch.*`): File ▸ Create branch… (or the Branches
@@ -397,9 +433,26 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
different items changed on each side, or items added at the end on one side, merge without a
conflict; only the same item changed both ways, an item removed on one side, or different items
added on both sides conflict (as the whole list).
- **Missing:** branches of a file in a live session are refused; other lists (text runs, export
settings, guides…) still conflict as a whole; the branch history has no entries of its own for
its creation or updates from main (only saved versions, autosaves and the merge on main).
- **Landed (2026-10-18, #798):** layout grids, export settings, guides, prototype interactions,
accessibility annotations and Dev Mode resources merge item by item like fills (code links merge
whole: one per language).
- **Landed (2026-10-24):** branches of a file in a live session: creating, editing, reviewing,
updating from main and merging work while the file is in a session. A branch stays with the
person who made it; merging it is one edit of the session document that everyone gets, and one
step of its author's own undo history. A branch made in the session makes its ids in its author's
own client space, 2^24 above every id made there on the main file, so the session server takes
the layers it adds; others' edits on main while it is open are main's changes against the
branch's own base, and conflicts with them resolve as offline (a layer a peer deleted comes back
with its id when the branch's edits win). A view link still refuses merging, and merging a branch
made outside the session that adds layers, styles, variables or comments (ids in another space)
is refused with a reason instead of being sent for the server to refuse. A branch's history gets
entries for its creation and each update from main. Engine tests through a session server with
several people, a fuzz test with random real commands (export settings and guides included,
offline and live) and a junk-parameter sweep of the branch commands in a session.
- **Missing:** text runs still conflict as a whole (runs are character ranges into the text, so
lining them up item by item would need a text diff, not positions); branches in a live session
are their author's only (others can't see or open them: that needs the server to keep them);
a branch made offline that adds layers merges only outside a session.
- **Estimate:** 1–2 h.
### 18. Display P3 on screen (several windows landed)
@@ -408,8 +461,9 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
needs to; closing one asks about unsaved changes; `window.new`, `window.list`, `window.focus` and
`window` on any control request.
- **Missing:** wide-gamut colour on the canvas (egui draws sRGB, so P3 colours outside sRGB clip on
screen; exports keep them); a screen-reader tree in windows other than the main one (eframe 0.36
gives only the main window one); window positions restored at the next start.
screen; exports keep them); a screen-reader tree in windows other than the main one (eframe 0.37
gives only the main window one; verified in the fork's viewport setup); window positions restored at
the next start.
- **Estimate:** 4–8 h.
### 19. AI features
@@ -440,12 +494,42 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
repository (licensing).
- **Estimate:** 2–4 h once an openly licensed profile is found.
### 24. Boards: the whiteboard tail
- **Landed (2026-10-24, M12):** board files (`file.newBoard`) with sticky notes, shapes with text,
connectors that re-route when what they join moves, marker, highlighter and eraser, stamps, text
and sections; a board toolbar and a contextual bar; live sessions sync it all. Then (also M12) a
timer everyone on the board sees and voting sessions with per-person caps, hidden votes and
ranked results (`board.timer`, `board.vote`), and five built-in templates (retrospective,
brainstorm, weekly planner, flow chart, kanban board) from a toolbar picker or `board.template`,
and tables (`board.table`, `board.tableEdit`, up to 50 × 20) made of auto layout frames.
- **Landed (2026-10-24, M12):** a dot grid on board pages, under the layers; a selected
connector's ends drag onto another object or empty canvas (`board.reattach`); curved connectors;
elbows that go around the objects they join.
- **Missing:** timer music, board widgets, saving your own templates, table column resizing and
cell colours, mind maps, washi tape, code blocks, audio and huddles, quick-create handles,
connector waypoints placed by hand, stamps sticking to the objects they land on, `.jam` files
([`boards-parity.md`](boards-parity.md)).
- **User impact:** boards cover a brainstorm, a retrospective, a flow sketch and a timed, voted
workshop round.
- **Estimate:** 20–44 h.
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | minor | egui/eframe 0.37 verified for gap 18: the remaining limitations are unchanged. egui-wgpu 0.37 still picks an 8-bit sRGB surface with no colour-space option, so P3 colours outside sRGB still clip on screen; eframe 0.37 still gives only the main window an AccessKit adapter, so other windows have no screen-reader tree. Numbers unchanged |
| 2026-10-25 | minor | Known gaps from the #1311 review: gap 4, the export preview follows a slice's first export setting; gap 14, the scopes a paint style's color takes are FigCraft's own rule; gap 15, `proto.linkNotice` from the palette or control channel isn't saved until the next preferences save; gap 16, the file name field still renames in Dev Mode (to check against Figma first). Ranking and hours unchanged |
| 2026-10-24 | minor | Gap 24: the dot grid on board pages, dragging a connector's end to re-attach it, curved connectors and elbows around the objects they join landed; connector waypoints placed by hand stay missing. 30–60 h → 28–56 h; the ranking is unchanged |
| 2026-10-24 | minor | Gap 16: Measurements kept on the canvas (gap 16): `devMode.addMeasurement {from?, to?, side?}` places one between two layers, along one side of a layer or (neither) to the layer around it, stored on the layer it starts from (saved in `.figcraft`, synced, undoable; old files open without them); Dev Mode draws them as labelled redlines that follow the layers, the Inspect tab lists them (those of the selected layer, or every one on the page), places and removes them; `devMode.removeMeasurement` and `devMode.measurements` remove and list them; a copy keeps those to layers copied with it. The layers panel is read-only in Dev Mode: rows select and expand, but don't rename, reorder, hide or lock. Left: publishing mappings with a CLI; 2–4 h → 1–2 h |
| 2026-10-24 | minor | Gap 24: a shared board timer and voting sessions landed (M12); 30–60 h → 26–54 h, ranking unchanged |
| 2026-10-24 | minor | Gap 24: board templates (retrospective, brainstorm, weekly planner, flow chart, kanban board; `board.template`, a toolbar picker) and tables (`board.table`, `board.tableEdit`); 30–60 h → 26–54 h, rank unchanged |
| 2026-10-24 | minor | Gap 6: plugin widgets take text input (editable fields, `widget.setText`) and a property menu (`widget.menu`, a bar over the selected widget), plugin API 4; 8–26 h → 6–22 h; ranking unchanged |
| 2026-10-24 | minor | Gap 17: branches of a file in a live session (create, edit, review, update from main and merge while the file is in a session; the branch stays with its author and makes ids in their own client space far above the main file's; merging is one synced edit and one step of the author's undo history; peers' edits merge against the branch's own base and conflicts resolve as offline; a view link still refuses merging; a branch made outside the session that adds layers is refused with a reason); a branch's history gets entries for its creation and updates from main; the list-merge wording brought up to date with #798 (export settings and guides already merged item by item); text runs still merge whole. Ranking and hours unchanged |
| 2026-10-24 | minor | Boards (M12): a file kind of its own (`file.newBoard`, saved in `.figcraft`, carried to live-session joiners) with sticky notes, shapes with text, connectors that re-route when what they join moves (an engine recompute, the same for local edits, undo and others' edits), marker, highlighter and eraser, stamps with the placer's name, a board toolbar and a contextual bar; prototyping and Dev Mode refuse on boards; design files and boards have their own marks on tabs and file rows. New gap 24 (the board tail, 30–60 h); the ranking above it is unchanged |
| 2026-10-15 | minor | Gap 5: Turkish reviewed by a native speaker (#263), Czech (#469) and Indonesian (#477) drafts, the Japanese draft caught up (#288); Indonesian is no longer missing; counts re-measured (2,177 entries) and corrected: only Spanish is held to completeness, Russian included. Estimate unchanged at this rounding |
| 2026-10-17 | minor | The 2026-10-17 landings. Gap 1: a new Landed entry (community batches #727 and #728, then #745: instance identity and cached library components, overrides by key, text style precedence, grid track sizes, slot content, labels of connectors saved with their path, skip ink, paragraph options, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments, images in the message, instance layers' size and resizing overrides, Hug where nothing is recorded; budgets from the file's size; the import report; a `.fig` copy from Save as; paste from another design app), its Missing list cut to what is still left, and the wrong "whether other apps open its files is unknown" replaced by what was checked; all on synthetic files, so the beta blocker and hours stand. Gap 2: server limits, log recovery and web join links in the evidence. Gap 4: menus, the Actions palette and the editor fixes (#733, #735, #736, #729), then Selection colors and smart-selection spacing (#753) landed, none compared side by side. Gap 5: catalog counts re-measured (2,272 entries; drafts at 95–99%; Indonesian, Czech, Turkish and Swedish listed). Gap 8: the Linux and BSD clipboard changes and Wayland window controls, not run there by the maintainers. Ranking and hours unchanged |
| 2026-10-15 | minor | Gap 7: bounded canvas overscan for smoother panning. Estimates and ranking unchanged. |
| 2026-10-15 | minor | Gap 16: import lines inside generated layer trees, compare with any saved version, an inspect-only Dev Mode canvas (menus and shortcuts refuse design edits), Changed in the ready list, statuses only on sections and top-level frames, read-only plugins' panels from the Inspect tab. Left: measurements kept on the canvas, a read-only layers panel, publishing mappings with a CLI; 0–1 h → 2–4 h |
| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. |
| 2026-10-13 | minor | Gap 1: the experimental `.fig` export writes instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes, and the import reads them back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Hours and the ranking unchanged |
| 2026-10-12 | minor | Gap 2: your online files landed (the app records the files it put on a server with their owner tokens, Home ▸ Online lists them and opens them again as their owner, with sharing, rename, removal and forget from the list; one request per server tells which are still there; tabs mark files in a live session). What is left is unchanged: accounts, teams and per-person permissions, shared comments and versions. Hours unchanged |
+7 -4
View File
@@ -1,6 +1,6 @@
# Hardware and platform parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (macOS: quitting ends the process once everything is kept, avoiding the Touch Bar teardown crash (#781); Linux and BSD clipboard: images under Wayland, reads time out; minimised windows under Wayland; window controls on Wayland without server decorations; canvas textures on device pixels; copy and paste in the browser; Windows QA pass; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (egui/eframe 0.37 dependency migration; hidden-window screenshot deadlines; zoom previews; hidden windows answer control requests; macOS file opening, the Windows start-up message, Linux packaging; copy and paste in the browser; Windows QA pass; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses; macOS: quitting ends the process once everything is kept, avoiding the Touch Bar teardown crash (#781); Linux and BSD clipboard: images under Wayland, reads time out; minimised windows under Wayland; window controls on Wayland without server decorations; canvas textures on device pixels; copy and paste in the browser; Windows QA pass; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses) · **Target:** Figma Design (desktop app 126.10.7)
How FigCraft uses hardware and which platforms it runs on, against Figma. Figma's desktop app is
Electron with a GPU helper process; its canvas renders on the GPU (WebGL, with WebGPU rolling out
@@ -12,13 +12,13 @@ since 2025). FigCraft renders the canvas on the CPU and presents it through wgpu
| Feature | Figma | FigCraft (macOS / Windows / Linux / web) | Hours |
|---|---|---|---|
| Canvas rendering | GPU | CPU (tiny-skia), banded across cores, retained (re-renders only what an edit or pan touches), levels of detail below 50% zoom; shown through wgpu (WebGPU or WebGL2 in the browser) | 30–60 for a GPU path |
| Canvas rendering | GPU | CPU (tiny-skia), banded across cores, retained (re-renders only what an edit or pan touches), levels of detail below 50% zoom; when renders from scratch are slow, a new zoom shows the whole view coarsely first, then refines it (#312); shown through wgpu (WebGPU or WebGL2 in the browser) | 30–60 for a GPU path |
| Effects (blurs, glass, noise) | GPU | CPU | in the above |
| HiDPI / Retina | yes | yes (all); a canvas image rendered at the screen's density is placed on whole device pixels, so fractional pans, zooms and display scales don't blur it (#359; geometry tests, the snapping not judged by eye) | — |
| Wide-gamut display (Display P3) | yes, on capable displays | colours stored and exported in P3; the screen shows sRGB: eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no colour space option, although wgpu 30 can present Display P3 or extended sRGB on Metal, DX12, Vulkan and WebGPU (see gaps #18). The colour picker warns about colours outside sRGB and View ▸ Soft-proof sRGB stripes them on the canvas | 2–5 |
| Wide-gamut display (Display P3) | yes, on capable displays | colours stored and exported in P3; the screen shows sRGB: eframe/egui-wgpu 0.37 configure an 8-bit sRGB surface with no colour space option, although wgpu 30 can present Display P3 or extended sRGB on Metal, DX12, Vulkan and WebGPU (see gaps #18). The colour picker warns about colours outside sRGB and View ▸ Soft-proof sRGB stripes them on the canvas | 2–5 |
| Trackpad pinch zoom and two-finger pan | yes | yes (egui zoom delta, smooth scroll) | — |
| Pen tablet pressure (pencil, Figma Draw brushes) | yes (forum reports of regressions in 2025) | Windows pens (winit pointer input) and browser styluses that send touch events with a force (an Apple Pencil in Safari); the Pencil records it as the stroke's width profile. A mouse, macOS and Linux tablets and pens sending only pointer events in a browser report none: constant width | 1–3 |
| Several windows / multi-monitor | yes (a window per file; presenter in its own window) | desktop: File ▸ New window, a tab's Open in new window or a tab dragged off the strip; each window has its own tabs, panels and canvas, draws only when it needs to and asks about unsaved changes when it closes. Only the main window has a screen-reader tree (eframe 0.36), so files stay tabs while a screen reader runs; positions aren't restored at the next start; the presenter stays inside its window; one window in the browser | 1–2 |
| Several windows / multi-monitor | yes (a window per file; presenter in its own window) | desktop: File ▸ New window, a tab's Open in new window or a tab dragged off the strip; each window has its own tabs, panels and canvas, draws only when it needs to and asks about unsaved changes when it closes; a minimised or covered window still answers control and MCP requests, while screenshots with hidden or unknown visibility request a hidden paint and expire at their 60-second admission deadline if unfinished; injected pointer/key/text input waits for a real UI frame (#313, #332, #720). Browser and Wayland hidden-paint behavior remain unqualified. Only the main window has a screen-reader tree (eframe 0.37), so files stay tabs while a screen reader runs; positions aren't restored at the next start; the presenter stays inside its window; one window in the browser | 1–2 |
| Screen readers | partial | AccessKit tree on desktop (names, roles, states), never tested with a real screen reader; none in the browser | 4–8 + human QA |
| Keyboard layouts (non-US) | yes | keymap uses logical keys; not checked on AZERTY/QWERTZ/JIS | 1–2 |
@@ -50,6 +50,9 @@ Nothing has been timed against Figma on the same document (needs `.fig` import).
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | minor | egui/eframe 0.37 dependency migration: the workspace pins the egui family at 0.37, and the wide-gamut and several-windows rows now cite 0.37. The limitations are unchanged: egui-wgpu 0.37 offers no surface colour-space option, so the screen still shows sRGB; eframe 0.37 still gives only the main window an AccessKit adapter, so files stay tabs while a screen reader runs. Numbers unchanged |
| 2026-10-25 | minor | Hidden or unknown-visibility screenshots request a hidden paint and expire at their 60-second admission deadline if unfinished; injected pointer/key/text input waits for a real UI frame. Browser and Wayland hidden-paint behavior remain unqualified. Numbers unchanged. |
| 2026-10-15 | minor | Community fixes (#290, #306, #312, #313, #328, #332, #346, #474): coarse zoom previews when renders from scratch are slow; a minimised or covered window answers control and MCP requests (screenshots and input fail at once while it is hidden); `.figcraft` files open from Finder on macOS; a message box when the window can't open on Windows; on Linux the window id matches the desktop entry, the RPM requires glibc 2.35 and user fonts under `XDG_DATA_HOME` are found. Numbers unchanged |
| 2026-10-18 | minor | macOS quit crash (#781): on Macs with a Touch Bar the app could crash as it quit (an exception in AppKit's Touch Bar support while the window's content view, which the accessibility adapter subclasses, was torn down). Quitting now keeps everything (preferences, this run's recovery copies, live sessions ended) and then ends the process before that teardown. Quit from the menu bar ends the app without the unsaved-changes question, so unsaved files' recovery copies now stay and reopen at the next start instead of being removed. Numbers unchanged |
| 2026-10-17 | minor | Linux row: clipboard reads on Linux and the BSDs time out after 2 s on a helper thread, images copied in other apps paste under Wayland (arboard's data-control feature, #693), a minimised window can't be detected under Wayland, so frame-only control requests may still wait there (#720), and Wayland compositors without server-side decorations give the window Adwaita controls (#592, checked by its author on GNOME, not by the maintainers; KDE not run); compiled in CI, not run on a Linux desktop. HiDPI row: native-density canvas images placed on whole device pixels (#359, geometry tests; not judged by eye). Numbers unchanged |
| 2026-10-14 | minor | Copy and paste in the browser: the web build never wrote to the system clipboard, so Cmd+V after Cmd+C pasted whatever text the clipboard held before (as a text layer) instead of the copied layers. Copies (layers, Copy as SVG/CSS/text/PNG, links) now go out through egui's output, which eframe writes during the copy event (`web_clipboard_tests`). Numbers unchanged. |
Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

+38 -31
View File
@@ -1,10 +1,10 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (macOS finds its downloadable CJK system fonts (#406); string counts re-measured: 2,272 entries, the drafts at 95–99%; Swedish command labels and menu wordings; Russian, Swedish, Turkish, Czech and Indonesian listed; a duplicated paragraph removed; draft catalogs catch up: every draft translates every string again; Simplified Chinese draft catalog) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (Thai draft catalog and Thai UI fonts; a missing translation fails no check: `cargo xtask i18n-report` lists them; Vietnamese draft catalog; string counts re-measured: 2,352 entries; Russian and Turkish catalogs reviewed by native speakers, Russian one/few/many plurals; Czech and Indonesian drafts; the Japanese draft completed; counts re-measured; Windows UI language from the first preferred language; draft catalogs catch up: every draft translates every string again; Simplified Chinese draft catalog; macOS finds its downloadable CJK system fonts (#406); string counts re-measured: 2,272 entries, the drafts at 95–99%; Swedish command labels and menu wordings; Russian, Swedish, Turkish, Czech and Indonesian listed; a duplicated paragraph removed; draft catalogs catch up: every draft translates every string again; Simplified Chinese draft catalog) · **Target:** Figma Design (desktop app 126.10.7)
FigCraft's interface has a localization layer with an English catalog, a Russian catalog reviewed
by a native speaker, and Spanish (with a Latin American overlay), French, German, Brazilian
Portuguese, Japanese, Korean, Simplified Chinese, Indonesian and Czech drafts (machine-drafted,
Portuguese, Japanese, Korean, Simplified Chinese, Indonesian, Vietnamese and Czech drafts (machine-drafted,
waiting for native speakers' review), plus Swedish and Turkish catalogs. This page lists the twelve key
languages of the craftrules standard, then the other languages Figma ships. How to add a
language: [`translating.md`](translating.md).
@@ -18,26 +18,27 @@ browser's page translation. The `.lproj` folders in `/Applications/Figma.app` (6
Electron/Chromium locale packs, not Figma's own UI languages. Text *content* in any script is
supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no vertical text.
## What FigCraft has (measured 2026-10-17, from source)
## What FigCraft has (measured 2026-10-22, from source)
- **Layer:** one TOML catalog per language in `crates/ui-egui/locales/`, baked into the binary by
the UI crate's build script (desktop and web; nothing read at run time). Keys are the English text
(`tr("Fill")`, `trf` for `{named}` placeholders, `trn` for counts with one/few/many/other plural forms), so
a missing translation shows English, never a raw key. One English text that needs two
translations takes a context (`"Fill|sizing"`).
- **Strings:** 1,680 UI keys in `en.toml` (41 with plural forms; ~110 are labels from engine and
document data, such as tool names, blend modes and Dev Mode properties) plus 561 command labels
and 31 menu wordings by command id: 2,272 entries (counted against `en.toml` and the complete
- **Strings:** 1,747 UI keys in `en.toml` (42 with plural forms; ~110 are labels from engine and
document data, such as tool names, blend modes and Dev Mode properties) plus 574 command labels
and 31 menu wordings by command id: 2,352 entries (counted against `en.toml` and the complete
Spanish catalog's command and menu tables). Command ids, the CLI, MCP and the control
channel stay English. Accessibility names come from the same strings.
- **Language preference:** Preferences ▸ Language (System, then each catalog by its own name:
English, Čeština, Deutsch, Español, Español (Latinoamérica), Français, Bahasa Indonesia, 日本語,
한국어, Português (Brasil), Русский, Svenska, Türkçe, 简体中文), applied at once without a
한국어, Português (Brasil), Русский, Svenska, Türkçe, Tiếng Việt, 简体中文), applied at once without a
restart; System takes the first of the OS's preferred languages that has a catalog (on Windows
only the first preferred language counts, since that list also holds the languages added for a
keyboard; macOS AppleLanguages, `LANGUAGE` then the locale variables on Linux and BSD; a regional
tag falls back to its language, pt-PT or pt to pt-BR, and es-419, es-MX, es-AR, es-CO and the
other Latin American and US Spanish tags to es-419; zh, zh-CN, zh-SG and zh-Hans read zh-Hans,
other Latin American and US Spanish tags to es-419; regional ru, tr, cs and id tags (ru-RU,
tr-TR, cs-CZ, id-ID) to their catalogs; zh, zh-CN, zh-SG and zh-Hans read zh-Hans,
while Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and reads English or the
next OS language, never Simplified) or the browser's language on the web.
`pref.language {language}` sets it from the CLI, MCP and the control channel.
@@ -45,15 +46,15 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
(`es-419.toml` over `es.toml`); its UI strings, command labels and menu wordings read the
overlay, then the base language, then English.
- **Check:** `cargo test -p figcraft-ui-egui i18n` fails on keys the code uses that `en.toml`
lacks, keys no code uses, unknown command ids, placeholders that differ from English, overlay
entries that read as in their base language and, for complete languages (Spanish), anything
untranslated. The other catalogs are not held to it: a string added later shows English there
until someone translates it. Every draft translated every string on 2026-10-12; by 2026-10-17
the strings added since left them at 95–99% (table below).
lacks, keys no code uses, unknown command ids, placeholders that differ from English and overlay
entries that read as in their base language. No language is held to full coverage: a string
added later shows English until someone translates it, and `cargo xtask i18n-report` (part of
`cargo xtask ci`) lists what each language lacks. Every draft translated every string on
2026-10-12; by 2026-10-17 the strings added since left them at 95–99% (table below).
- **Not yet localized:** messages the engine writes (errors, a few toasts, Dev Mode property values
such as "inside"), number formatting (decimal points stay `.`), and decimal plural rules
(`trn` takes integers: Czech uses one/few/other and Russian one/few/many; French and Brazilian
Portuguese take the singular for 0 as well; Japanese, Korean and Chinese have one form;
Portuguese take the singular for 0 as well; Japanese, Korean, Chinese and Vietnamese have one form;
CLDR's "many" for millions in French, Spanish and Portuguese reads as the plural).
- **Script support in documents:** Unicode bidi with Arabic shaping and mirrored brackets in text
layers; right-to-left list markers and indents; right-to-left names in the layers panel, tabs
@@ -63,7 +64,8 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
- **Script support in the UI:** menus widen to their longest label; dropdowns, the two-column
labels of the right panel and the home screen's cards keep a long translation on one line with an
ellipsis and show it in full on hover (checked in German, the longest); no mirrored
(right-to-left) layout of panels. CJK translations are shorter than English; the same ellipsis
(right-to-left) layout of panels. Cyrillic, Turkish and Czech letters draw from the UI fonts
(no font added; Russian checked in screenshots). CJK translations are shorter than English; the same ellipsis
and tooltip cover the few that don't fit (the sizing dropdowns).
- **CJK UI fonts:** egui's fonts and Inter have no CJK glyphs. Once the UI language, the open
file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han,
@@ -85,23 +87,24 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
| Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | Hours to full |
|---|---|---|---|---|---|---|---|
| English | en | 2,272 / 2,272 (100%, source) | yes | Latin | yes (source) | full | 0 |
| Simplified Chinese (Mandarin) | zh-Hans | 2,166 / 2,272 (95.3%, draft) | yes | CJK text and IME in documents; UI text in an installed Chinese face, ahead of Japanese ones (web: the `FIGCRAFT_EMBED_CJK` build); no Traditional Chinese catalog (zh-TW, zh-HK read English) | no | draft | 1–2 (after native review) |
| Spanish | es, es-419 | 2,272 / 2,272 (100%, draft); es-419 overlays 101 Latin American wordings (draft) | yes | Latin | no | draft | 0–1 (after native review) |
| English | en | 2,352 / 2,352 (100%, source) | yes | Latin | yes (source) | full | 0 |
| Simplified Chinese (Mandarin) | zh-Hans | 2,216 / 2,352 (94.2%, draft) | yes | CJK text and IME in documents; UI text in an installed Chinese face, ahead of Japanese ones (web: the `FIGCRAFT_EMBED_CJK` build); no Traditional Chinese catalog (zh-TW, zh-HK read English) | no | draft | 1–2 (after native review) |
| Spanish | es, es-419 | 2,352 / 2,352 (100%, draft); es-419 overlays 104 Latin American wordings (draft) | yes | Latin | no | draft | 0–1 (after native review) |
| Hindi | hi | 0 (0%) | no | Devanagari shaping works in documents; UI font fallback to check | no | none | 4–7 |
| Arabic | ar | 0 (0%) | no | Bidi, shaping and RTL names work; the UI does not mirror | no | none | 9–15 |
| French | fr | 2,167 / 2,272 (95.4%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| Portuguese | pt-BR | 2,167 / 2,272 (95.4%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| Indonesian | id | 2,244 / 2,272 (98.8%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| Japanese | ja | 2,254 / 2,272 (99.2%, draft) | yes | CJK text and IME in documents; UI text in an installed Japanese face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| German | de | 2,176 / 2,272 (95.8%, draft) | yes | Latin; long words elide in narrow fields | no | draft | 1–2 (after native review) |
| Korean | ko | 2,166 / 2,272 (95.3%, draft) | yes | Hangul text and IME in documents; UI text in an installed Korean face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| Vietnamese | vi | 0 (0%) | no | Latin with stacked diacritics | no | none | 3–5 |
| French | fr | 2,328 / 2,352 (99.0%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| Portuguese | pt-BR | 2,331 / 2,352 (99.1%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| Indonesian | id | 2,292 / 2,352 (97.4%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| Japanese | ja | 2,304 / 2,352 (98.0%, draft) | yes | CJK text and IME in documents; UI text in an installed Japanese face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| German | de | 2,226 / 2,352 (94.6%, draft) | yes | Latin; long words elide in narrow fields | no | draft | 1–2 (after native review) |
| Korean | ko | 2,216 / 2,352 (94.2%, draft) | yes | Hangul text and IME in documents; UI text in an installed Korean face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| Vietnamese | vi | 2,352 / 2,352 (100%, draft) | yes | Latin with stacked diacritics | no | draft | 1–2 (after native review) |
| Thai | th | 1,791 / 1,791 UI strings, 589 of 598 command labels, 31 menu wordings (draft) | yes | Thai text in documents; UI text (and layer and page names in Thai, whatever the UI language) in an installed Thai face, desktop only; the web build leaves Thai out | no | draft | 1–2 (after native review) |
Other languages (measured 2026-10-17): Russian (`ru`) 2,244 / 2,272 (98.8%), reviewed and approved
by VanTan, one/few/many integer plurals; Swedish (`sv`) 2,264 (99.6%), its command labels and menu
wordings added 2026-10-17 (#599), native review welcome; Turkish (`tr`) 2,243 (98.7%), whose header
says it was machine-drafted and reviewed by a native speaker; Czech (`cs`) 2,244 (98.8%), a draft
Other languages (measured 2026-10-22): Russian (`ru`) 2,352 / 2,352 (100%), reviewed and approved
by VanTan, one/few/many integer plurals; Swedish (`sv`) 2,314 / 2,352 (98.4%), its command labels and menu
wordings added 2026-10-17 (#599), native review welcome; Turkish (`tr`) 2,291 / 2,352 (97.4%), whose header
says it was machine-drafted and reviewed by a native speaker; Czech (`cs`) 2,292 / 2,352 (97.4%), a draft
with one/few/other plurals. Figma ships none beyond the eight above.
The layer is done. Per-language hours above are agent hours for a catalog of ~1,800 entries
@@ -113,13 +116,17 @@ right-to-left mirroring of the panels is in Arabic's hours. Total: **29–51 h**
every string adds).
Priority: native reviews of the drafts and the es-419 overlay (every one of Figma's languages
and variants has one, plus Simplified Chinese, Indonesian and Czech, which Figma doesn't ship), then
Hindi, Vietnamese and Arabic. Traditional Chinese is not planned (no catalog; its users see English).
and variants has one, plus Simplified Chinese, Indonesian, Vietnamese and Czech, which Figma doesn't ship), then
Hindi and Arabic. Traditional Chinese is not planned (no catalog; its users see English).
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | Thai (`th`) draft catalog: `th.toml` translates every UI string (1,791), 589 command labels and the 31 menu wordings, machine-drafted and marked as a draft awaiting native review (9 newer command labels read English, as in the other drafts). The desktop UI now adds an installed Thai face (Noto Sans Thai, Leelawadee UI, Tahoma, Sukhumvit Set, Thonburi, Ayuthaya, Sarabun, Loma, Garuda or Waree) when a layer or page name or the UI language needs it, so Thai names no longer show as tofu squares. No plural forms for Thai. Grade and hours unchanged |
| 2026-10-24 | minor | Vietnamese (`vi`) draft catalog (#1026), assessed as a machine draft awaiting review like the others; string counts re-measured: 2,352 entries. |
| 2026-10-23 | minor | A missing translation fails no check: Spanish is no longer held to full coverage, and the tests of one language's wording, names and plural forms are gone (the plural rules and language-tag tests stay). `cargo xtask i18n-report`, run by `cargo xtask ci`, lists each language's missing UI strings, command labels and menu wordings. Grade unchanged |
| 2026-10-15 | minor | Community catalogs (#257, #263, #469, #477, #288): Russian (`ru`, Русский, reviewed and approved by VanTan) and Turkish (`tr`, Türkçe, reviewed by a native speaker), Czech (`cs`, Čeština) and Indonesian (`id`, Bahasa Indonesia) drafts, and the Japanese draft caught up; Preferences ▸ Language lists them and System maps their regional tags. Catalogs may give few and many plural forms; Russian picks one, few or many by its integer rule (11–14 take many), the catalog supplying the text choosing the rule. Only Spanish is held to completeness. Counts re-measured: 1,587 UI keys (38 plural), 559 command labels and 31 menu wordings, 2,177 entries; Spanish 100%, Japanese 2,176, Czech and Indonesian 2,166, Russian and Turkish 2,165, the five older drafts 2,088–2,089 (95%). Localization 39% and 29–51 h unchanged at this rounding |
| 2026-10-17 | minor | Chinese showed as boxes on macOS (#406): the font scan also walks the system's downloaded font assets (`/System/Library/AssetsV2` and the older `/System/Library/Assets`, only their `com_apple_MobileAsset_Font*` folders), where current versions keep PingFang SC, and Hiragino Sans GB and Heiti SC join the Chinese UI faces and the text fallbacks. Tested with a folder laid out the same way; not yet run on a Mac |
| 2026-10-17 | minor | String counts re-measured from the catalogs: 1,680 UI keys, 561 command labels and 31 menu wordings (2,272 entries); Spanish complete, the other drafts at 95.3–99.2% (strings added since 2026-10-12 show English), es-419 101 entries. Indonesian has a draft (98.8%; it was listed as none); Russian (98.8%, reviewed), Swedish (99.6%, command labels and menu wordings from #599), Turkish (98.7%) and Czech (98.8%) listed with counts; the language picker's list updated; a paragraph repeated by a merge removed. Grade and hours unchanged (Indonesian's draft is offset by the drafts' new gaps) |
| 2026-10-14 | minor | Windows UI language (#291): the interface language comes from Windows' first preferred language alone; that list is also the input-language list (a German keyboard on a Polish system adds Deutsch), and taking the first entry with a catalog turned the interface German there. A first preferred language without a catalog reads English; macOS AppleLanguages and gettext's `LANGUAGE` still take the first language with a catalog |
+330 -50
View File
@@ -34,6 +34,10 @@ of the window itself (`screenshot` with `window`) needs it on screen: it is then
### Confining file access
The roots below belong to the **headless** side: the session `figcraft-cli mcp` runs itself, with
no window. A server attached with `--attach` runs the commands inside the app instead, so the app
confines them — from the flags it was started with, not from anything passed to the CLI.
```sh
figcraft-cli mcp --automation-read-root /work/project --automation-write-root /work/project/out
```
@@ -144,6 +148,8 @@ Or in a project's `.mcp.json`:
there is none, so they are page coordinates then). Groups and boolean groups have no coordinate
space of their own: a layer in a group is placed in the frame (or on the page) the group is in.
`inspect_document` reports `x`/`y` the same way, plus `absoluteBounds` in page coordinates.
In `props`, `fill` and `stroke` also accept `"none"` (either case) or `null` as no paint, the
same as an empty `fills`/`strokes` list, so an outline-only shape can be made in one call.
- Frames, groups, sections and masks: drawing a frame or section with the `pointer` tool around
layers puts those fully inside it in it; sections go only on the page or in sections (moves,
drags, pastes and new sections elsewhere are refused or land on the page); resizing a section
@@ -190,6 +196,12 @@ Or in a project's `.mcp.json`:
the CSS `fr` unit: a track never gets less than its content (a fixed-size child bigger than
its share keeps it; the other `fr` tracks share the rest). `inspect` reports a grid child's
resolved `gridCell`.
- Layer order: `node.reparent {ids?, parent, index?}` moves layers into `parent` (keeping their place
on the canvas, or taking their place in its auto layout flow) and `node.reorder {id, index}` moves
one within its parent. `index` counts the parent's children bottom first (in auto layout, the
first in the flow) and is where the (first) moved layer ends up after the move, in the same parent
or a new one; several layers land together in their stacking order. No `index`, or one past the
end, puts them on top (last in the flow).
- **Every menu action is a command** (`list_commands`), and most commands act on `ids` when given,
else on the selection. New layers become the selection.
- Colours are `"#RRGGBB"` or `"#RRGGBBAA"`. Inside fills and strokes, `color` may also be a hex
@@ -267,9 +279,11 @@ Or in a project's `.mcp.json`:
- **Styles stay true to their layers**: editing a styled fill, stroke, effect or layout grid list
(`node.set`, `fill.set`, `fill.add`, a bound variable…) unlinks that style (`styles.fill` etc. go
`null`), so a later `style.update` doesn't overwrite the edit; `style.apply` replaces variables
bound to what the style sets; a text style goes on text layers only. Names are trimmed per `/`
folder part. `style.move {styles, before?, folder?}` reorders (and with `folder`, renames into it),
`style.duplicate {style}` adds "<name> copy" right below, `style.renameFolder {folder, name}`.
bound to what the style sets; a text style goes on text layers only. In one call, a style link
(`fillStyle`, `textStyle`…) wins over values it sets; set a deviation in a later call, which
unlinks it. Names are trimmed per `/` folder part. `style.move {styles, before?, folder?}`
reorders (and with `folder`, renames into it), `style.duplicate {style}` adds "<name> copy" right
below, `style.renameFolder {folder, name}`.
- **Variables**: `variable.bind` refuses a variable of a type the field doesn't take (a colour on
`width`); scopes only filter pickers. `variable.delete` / `variable.deleteCollection` unbind the
layers using them (they keep their last value) and give aliases to them their values. An
@@ -294,16 +308,16 @@ Or in a project's `.mcp.json`:
| Tool | What it does |
|---|---|
| `list_commands {filter?}` | Every command: id, label, menu, shortcut, undoable, params doc. |
| `run_command {command, params}` | Run any command (e.g. `align.left`, `layout.add`, `component.create`, `instance.place` (drop an instance at a page point into the frame or auto layout slot under it), `component.exposeInstance` / `instance.exposed` (nested instance properties on instances), `component.makeSlot` / `instance.slotInsert` / `instance.resetSlot` (slots, below), `inspect.code` (CSS, HTML + CSS for a whole frame, React, Leptos, Vue, SwiftUI or Jetpack Compose for a layer; components take their properties as parameters and instances are calls, see Dev Mode below), `inspect.properties` (layout, auto layout, typography, fills, strokes and effects with their style and variable names; `colorFormat`: hex, rgb, css, hsl or hsb), `inspect.assets` (layers under the selection with export settings, icons and images, each with its `kind`), `inspect.code` takes `unit: rem` and `rootFontSize` for CSS, HTML, React, Leptos and Vue, `style.create`, `variable.bind`, `variable.update` (rename, describe, scope), `variable.set` with `{alias}`, `variable.renameMode` / `variable.removeMode`, `variable.deleteCollection`, `proto.interaction.add`, `codeConnect.set` (components linked to real code, see Dev Mode below)). |
| `batch {commands: [{command, params}], label?}` | Several commands as one undo step (`command.batch`); all-or-nothing for open documents and their document state. Failed inner commands are removed from the journal; preferences, clipboard, tool state and UI requests are not rolled back. A step can use what an earlier step returned: `{"$step": n}` anywhere in its params becomes the `id` in step n's result (zero-based), and `{"$step": n, "field": "…"}` another field of it (see Step references below). |
| `run_command {command, params}` | Run any command (e.g. `align.left`, `layout.add`, `component.create`, `instance.place` (drop an instance at a page point into the frame or auto layout slot under it), `component.exposeInstance` / `instance.exposed` (nested instance properties on instances), `component.makeSlot` / `instance.slotInsert` / `instance.resetSlot` (slots, below), `inspect.code` (CSS, HTML + CSS for a whole frame, React, Leptos, Vue, SwiftUI, Jetpack Compose or ArkUI for a layer; components take their properties as parameters and instances are calls, see Dev Mode below), `inspect.properties` (layout, auto layout, typography, fills, strokes and effects with their style and variable names; `colorFormat`: hex, rgb, css, hsl or hsb), `inspect.assets` (layers under the selection with export settings, icons and images, each with its `kind`), `inspect.code` takes `unit: rem` and `rootFontSize` for CSS, HTML, React, Leptos and Vue, `style.create`, `variable.bind`, `variable.update` (rename, describe, scope), `variable.set` with `{alias}`, `variable.renameMode` / `variable.removeMode`, `variable.deleteCollection`, `proto.interaction.add`, `codeConnect.set` (components linked to real code, see Dev Mode below)). |
| `batch {commands: [{command, params}], label?}` | Several commands as one undo step (`command.batch`); all-or-nothing for open documents and their document state. Failed inner commands are removed from the journal; preferences, clipboard, tool state and UI requests are not rolled back. A step can use what an earlier step returned: name it with `"as": "card"` and `{"$ref": "card"}` anywhere in a later step's params becomes the `id` in its result (`{"$step": n}` does the same by zero-based position); `"field"` takes another key or a slash path such as `ids/0` (see Step references below). |
| `create_node {type, x, y, width, height, parent, index, name, text, path, props}` | Create a frame, rectangle, ellipse, polygon, star, line, arrow, text, vector, section, component or slice; returns `{id, node}`. |
| `set_properties {ids, props}` | `node.set` on layers (geometry, fills, strokes, effects, corner radius, auto layout, sizing, constraints, masks (`isMask`, `maskType: alpha\|vector\|luminance`), handoff status (`devStatus: none\|readyForDev\|completed`; marking again clears "Changed"), a section's `sectionContentsHidden`, text…); returns the updated layers. |
| `inspect_document {depth, allPages}` | The document as JSON (layer tree, styles, selection, history). |
| `get_node {id, depth}` | One layer as JSON; `pluginData` (`{key: value}`, values as strings) when the layer or page has layer data written with `set_properties {props: {pluginData}}`. |
| `select {ids}` | Replace the selection (`[]` deselects). |
| `export {ids, format, scale \| size, path?, bleed?, cropMarks?, bleedMarks?, cmyk?, contentsOnly?, includeBoundingBox?, svgOutlineText?, svgIdAttribute?, svgSimplifyStroke?}` | png, jpg, svg, webp, pdf, or `fig`: the whole file as `.fig`, experimental (see Exporting `.fig` files below); `size` is `2x`, `512w` or `512h`. The image covers what the layers paint (shadows towards their offset, strokes, unclipped children, a mask group's mask; hidden layers take no room; a slice is its area with everything under it); `contentsOnly: false` also draws every other layer showing in that area, `includeBoundingBox` exports the layers' own boxes; SVG: `svgOutlineText: false` keeps `<text>`, `svgIdAttribute` writes unique ids from layer names, `svgSimplifyStroke: false` writes every stroke as an outline. The same options go in export settings (`export.add` / `export.update`). PDF: frames one page each (sized in points), anything else together on one page; print options: `bleed` (points or a length like `"3mm"`, `"0.125in"`), crop and bleed marks, CMYK colour (see Exports below), also taken by `file.export`, `file.exportLayers`, `file.exportFramesPdf` and stored per export setting by `export.add` / `export.update`; `run_command` `file.exportFramesPdf {page?, path?}` writes every top-level frame of a page. Without `path`, images come back as MCP image content, SVG as text. Layers' own export settings: `run_command` with `export.add` / `export.update {index, size?, suffix?, format?}` / `export.remove` and `file.exportLayers {ids?, dir?, assets?}` (every layer once per setting, named `<layer><suffix>.<format>`, a slash in a layer name making a folder; `assets: true` exports the Inspect tab's assets instead: icons as SVG, images as PNG). |
| `export {ids, format, scale \| size, path?, bleed?, cropMarks?, bleedMarks?, cmyk?, contentsOnly?, includeBoundingBox?, svgOutlineText?, svgIdAttribute?, svgSimplifyStroke?}` | png, jpg, svg, webp, pdf, or `fig`: the whole file as `.fig`, experimental (see Exporting `.fig` files below); `size` is `2x`, `512w` or `512h`. The image covers what the layers paint (shadows towards their offset, strokes, unclipped children, a mask group's mask; hidden layers take no room; a slice is its area with everything under it, or only the frame or group it sits in while `contentsOnly` is on); `contentsOnly: false` also draws every other layer showing in that area, `includeBoundingBox` exports the layers' own boxes; SVG: `svgOutlineText: false` keeps `<text>`, `svgIdAttribute` writes unique ids from layer names, `svgSimplifyStroke: false` writes every stroke as an outline. The same options go in export settings (`export.add` / `export.update`). PDF: frames one page each (sized in points), anything else together on one page; print options: `bleed` (points or a length like `"3mm"`, `"0.125in"`), crop and bleed marks, CMYK colour (see Exports below), also taken by `file.export`, `file.exportLayers`, `file.exportFramesPdf` and stored per export setting by `export.add` / `export.update`; `run_command` `file.exportFramesPdf {page?, path?}` writes every top-level frame of a page. Without `path`, images come back as MCP image content, SVG as text. Layers' own export settings: `run_command` with `export.add` / `export.update {index, size?, suffix?, format?}` / `export.remove` and `file.exportLayers {ids?, dir?, assets?}` (every layer once per setting, named `<layer><suffix>.<format>`, a slash in a layer name making a folder; `assets: true` exports the Inspect tab's assets instead: icons as SVG, images as PNG). |
| `screenshot {ids?, scale?, maxSize?, window?}` | PNG of the page (with background) or of layers, scaled to fit `maxSize` (default 1600). `window: true` captures the app window in front when attached, a window id (see Windows below) that window. |
| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` (best effort, see below), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. `open_file` returns `opened` (for a file opened anew, a `summary` of `name`, `pages`, `layers` and `images`, and `warnings` or `repairs`; a file that was already open just gives its `index`) and the document's `document` outline. |
| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` (best effort, see below), `.sketch` (Sketch 48 and newer, best effort), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. `open_file` returns `opened` (for a file opened anew, a `summary` of `name`, `pages`, `layers` and `images`, and `warnings` or `repairs`; a file that was already open just gives its `index`) and the document's `document` outline. |
| `undo` / `redo` | Step through history. |
| `pointer {kind, x, y, to?, steps?, tool?, shift, alt, cmd, pressure?, pressureTo?}` | Mouse gestures in page coordinates with the current tool: `click`, `double`, `drag`, `down`, `move`, `up`, `hover`. A dragged layer drops into the frame or auto layout slot under the pointer, except one inside a group, a boolean group or an instance (outside its slots), which only moves. `pressure` (0–1) is pen pressure; a drag goes from `pressure` to `pressureTo` (see Brushes and pen pressure). |
| `key {key, shift, alt, cmd}` | A shortcut or key as if typed on the canvas (every default shortcut, tool keys, Escape, Delete, arrows, number keys for opacity, arrows that pan the view when nothing is selected; while editing text, the text shortcuts and then text editing keys). Keys are names like `A`, `0`, `Enter`, `ArrowLeft`, `=`, `/`, or egui's (`Plus`, `Questionmark`); `cmd` is Cmd on macOS and Ctrl elsewhere. |
@@ -330,6 +344,137 @@ tips; headless, it only sets the preference.
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"export","arguments":{"format":"png","path":"dashboard.png"}}}
```
### Filling copies with data
`data.fill {id, rows, gap?, formats?, dir?, format?, scale?}` repeats a frame once per row (a carousel, a set
of posts, a card per item) in one undo step: each key of a row names layers in the copy, and a
string (or number) sets their text, `{text?, fill?, visible?}` sets more; `name` names the copy
(default "<frame> <n>"). Copies go in a row to the right, `gap` apart (default 100). With `dir`, each
copy is exported there as `<name>.<format>` (png by default) after the edits land. Returns `{ids,
missing (keys no layer is named), files?, errors?}`. `errors` lists the exports that failed as
`[{id, path, error}]`; the copies stay, and the files that were written are in `files`.
Copy that changes length per row can size itself: `{"Headline": {"text": "…", "fit": {"maxLines": 2}}}` runs
`text.fit` on those layers after the text is set.
`formats: [{name?, width, height}]` makes every row once per size: the copy is resized so its auto layout
reflows, named `<name>-<format name>` (default `<width>x<height>`), and placed under the row's previous
format. Text `fit` runs after the resize. Use case: one call turns a 1080×1350 post into feed, story
(1080×1920) and square (1080×1080) files for each slide of a carousel; the frame must be auto layout (fixed-
position children keep their place).
`text.fit {ids?, maxLines?, minSize?, orphans?}` gives a text layer with a fixed width (auto height or fixed size)
the largest font size, down to `minSize` (default 60% of the current), at which it stays within `maxLines` lines
and its box's height. Every style run scales by the same factor. A one-word last line (an orphan) is avoided by
shrinking up to 8% more, else by keeping the last two words together with a no-break space. A one-word paragraph after a hard line break is not an orphan. Text never
grows, text that already fits keeps its exact size, and sizes round to a tenth without going below `minSize`.
Returns `{fitted: [{id, fontSize, lines, fits, orphan}]}`. Use case: a carousel slide "Lighthouse 100. Loads in under
1s." at 96px broke into three lines with "1s." alone; with `fit: {maxLines: 3}` it is two balanced lines.
```json
{"command": "data.fill", "params": {"id": 80, "dir": "out/carousel", "scale": 1, "rows": [
{"name": "01-cover", "Headline": "Your website and AI chat assistant in 7 days", "Badge": {"visible": true}},
{"name": "02-proof", "Headline": "Lighthouse 100, loads in under a second", "Badge": {"visible": false}}
]}}
```
### Boards
A board is a file of its own kind for thinking together: `file.newBoard {name?}` opens one (its
page is light; `inspect_document` says `kind: "board"`). Board objects are ordinary layers with a
`board` entry `get_node` shows (`{type: sticky|shape|connector|label|stamp|ink, …}`), so the usual
commands move, resize, recolour, copy, delete and undo them. On a design file the board commands
refuse; on a board `proto.*`, `dev.*`, `view.prototype` and `view.devMode` refuse. Each of these is
one undo step and returns the ids it made:
- `board.sticky {x, y, text?, color?, author?, parent?}` → `{id, text}`: a sticky note (a square
frame whose height grows with its note; its text size shrinks as the note gets longer). `color`
is a board colour (`yellow orange red pink violet blue green gray`) or a hex colour; `author`
defaults to your name (`pref.userName`, or the one a live session gave you; `false` for none).
- `board.nextSticky {id?, side?: right|below|left|above, edit?}` → `{id, text}`: a sticky of the
same colour beside the selected (or edited) one; with `edit: true` the app goes on typing in it
(`Shift+Cmd/Ctrl+Enter` in the app does both).
- `board.shape {shape, x, y, width?, height?, text?, color?, parent?}` → `{id, text}`: a shape with centred,
wrapping text: `rectangle roundedRectangle ellipse diamond triangle speechBubble chevron`.
- `board.connector {from, to, fromSide?, toSide?, style?: straight|elbow|curved, startCap?, endCap?, label?}`
→ `{id, label?}`: `from`/`to` are layer ids or `{x, y}` page points; sides are `auto` (the side
facing the other end) or `top right bottom left`; caps are stroke caps (default an arrow at the
end). An elbow goes around the objects it joins when the short way would cross them; a curved
one leaves and arrives along the sides. The connector re-routes itself whenever what it joins
moves or resizes, also when someone else in a live session moves it; a deleted end stays where it
was as a point. `board.label {id, text}` sets its label (empty removes it).
- `board.reattach {id?, end: from|to, target, side?}` → `{id}`: moves one end of a connector
(default the selected one; its label stands for it) onto `target`, a layer id or an `{x, y}`
page point where the end stays free. Refused when both ends would be on one layer or the target
is a connector. Dragging a selected connector's end handle in the app does the same.
- `board.stamp {x, y, stamp: thumbsUp|heart|plusOne|star|question|check}` → `{id}`: a round badge
centred there, with your name on it for hover.
- `board.ink {path | points, highlighter?, color?, select? (default true)}` → `{id}`: a marker (or
wide, see-through highlighter) stroke.
- `board.setColor {ids?, color}`: stickies and shapes take it as their fill, connectors and ink as
their line. `board.setStyle {ids?, shape?, fontSize?, style?: straight|elbow|curved, startCap?,
endCap?}`: a shape's outline and text size, a connector's routing and arrowheads.
- `board.templates` → `[{id, name, description, width, height}]` lists the built-in templates:
`retrospective brainstorm weeklyPlanner flowChart kanban`. `board.template {name, x?, y?}` →
`{name, ids}` places one (its top-left at `x`, `y`; centred in the view without them) as ordinary
sections, stickies, shapes, connectors and text, selected, in one undo step.
- `board.table {rows?, columns?, cells?, x?, y?}` → `{id, rows, columns, cells}`: a table (default
3 × 3, at most 50 × 20; `cells` is `[["Name", "Role"], …]` row by row and sets the size when
`rows`/`columns` are left out). It is an auto layout frame of row frames holding cell frames, each
with one text layer; `cells` in the answer holds those text layers' ids row by row, so
`node.set {id, props: {characters}}` changes a cell. A cell's row grows with its text. `board.tableEdit {id?, action: addRow|addColumn|removeRow|removeColumn,
index?}` → the same answer: `index` is where the new row or column goes, or which one goes
(default the last); `id` may be the table or any layer in it.
- Tools for `pointer`: `tool.sticky {color?}`, `tool.shape {shape?}`, `tool.connector {style?: straight|elbow|curved}`
(drag from one object to another), `tool.marker {color?}`, `tool.highlighter {color?}`,
`tool.eraser` (drag over strokes), `tool.stamp {stamp?}`; Text and Section are the usual tools.
```json
{"command": "file.newBoard", "params": {"name": "Retro"}}
{"command": "board.sticky", "params": {"x": 0, "y": 0, "text": "Went well", "color": "green"}}
{"command": "board.sticky", "params": {"x": 240, "y": 0, "text": "To improve", "color": "pink"}}
{"command": "board.connector", "params": {"from": 2, "to": 4, "style": "elbow", "label": "leads to"}}
{"command": "board.stamp", "params": {"x": 200, "y": 0, "stamp": "plusOne"}}
```
`from` and `to` are the `id`s the two `board.sticky` calls returned (here `{"id": 2, "text": 3}` and
`{"id": 4, "text": 5}` in a new board without an author name; a sticky with its author's name takes
one more id). In a `batch`, step references (`{"from": {"$step": 1}, "to": {"$step": 2}}`, the `id`s steps 1 and 2 returned) avoid counting.
Running a session on a board: a countdown and votes. Both are part of the board, so everyone in a
live session sees the same, and neither is an undo step (undo leaves them as they are). On a
design file they refuse.
- `board.timer {action: start|pause|resume|add|stop, minutes?}` → `{state, left, text, ends}`: one
countdown per board. `start` takes `minutes` (default 5) and `add` adds them (default 1), from
one second up to 1440 minutes in all; `pause` keeps the time left, `resume` counts it down again.
The timer keeps its end time (milliseconds since 1970) or, paused, the time left, and each
client counts down on its own clock. `board.timerState {}` reads it: `state` is `off`,
`running`, `paused` or `done` (it ran out: 0:00 until someone stops it), `left` milliseconds,
`text` as `m:ss` (or `h:mm:ss`).
- `board.vote {action: start|cast|retract|end|clear, …}` → the vote as `board.voteResults` gives
it. One vote at a time: `start {votes? (per person, 1–99, default 3), on?: all|stickies|selection
(default all), ids? (with selection; default the selected layers)}` (a vote that ended may be
replaced; one still running is refused), `cast {id}` puts one of your votes on an object (several
on the same object are fine; once yours are used up it refuses), `retract {id}` takes one back,
`end` shows everyone's votes, `clear` removes the vote and its results. `voter` (any action)
votes as someone else, for scripts and tests; by default you are your live-session client, or
your name (`pref.userName`) when the file isn't shared. In the app a click on an object votes
while a vote runs, and a click on your badge (or an Alt-click) takes the vote back.
- `board.voteResults {voter?}` → `{state: off|running|ended, votesPerPerson, on, by, left, mine:
[{id, votes}], voters, results?}`: while it runs you see your own votes only (`mine`, `left`) and
how many have voted; once it ends `results` lists `[{id, name, votes, rank}]`, most votes first
(ties share a rank), with `name` the sticky's text or the layer's name. Votes on deleted objects
don't count.
```json
{"command": "board.timer", "params": {"action": "start", "minutes": 10}}
{"command": "board.vote", "params": {"action": "start", "votes": 2, "on": "stickies"}}
{"command": "board.vote", "params": {"action": "cast", "id": 2}}
{"command": "board.vote", "params": {"action": "end"}}
{"command": "board.voteResults", "params": {}}
```
### Sections, handoff and arrow keys
`run_command` with `object.wrapInSection {ids?, name?}` (Wrap in new section: layers on the page or
@@ -364,6 +509,10 @@ on; `key` resolves keys the way the canvas does. Commands behind keys:
- `view.designPrototype` (`Shift+E`), `view.devMode` (`Shift+D`) and `view.leftSidebar`
(`Shift+Cmd+\`) switch the right panel's tab or hide the left sidebar in the app (no-ops headless).
- `doc.nextTab` / `doc.previousTab` (`Ctrl+Tab` / `Ctrl+Shift+Tab`): the next or previous open file.
- Board tools (boards only): `tool.sticky` (`Q`), `tool.shape` (`B`), `tool.connector` (`X`),
`tool.marker` (`M`), `tool.highlighter` (`U`), `tool.eraser` (`E`), `tool.stamp` (`W`), and
`board.nextSticky` (`Shift+Cmd/Ctrl+Enter`, also while typing in a sticky). Keys no design-file
shortcut uses; on a design file they refuse.
- `view.pan {dx, dy, big?}`: the arrow keys with nothing selected (`key ArrowRight` does the same)
pan the view by 20 screen pixels a step, 200 with `big` (Shift).
- `view.multiplayerCursors {on?}` (`Alt+Cmd+\`): show or hide others' cursors in a live session.
@@ -436,7 +585,7 @@ on; `key` resolves keys the way the canvas does. Commands behind keys:
- `pref.startOnHome {on?}`: the desktop app starts on the Home screen instead of in a new file
(off by default; files named on the command line still open, and `--control` starts in a new
file anyway, so control callers have a document).
- `pref.language {language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|sv|zh-Hans|…}`: the app's interface language, applied live ("system":
- `pref.language {language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|sv|vi|zh-Hans|…}`: the app's interface language, applied live ("system":
the OS or browser language; a language without a catalog shows English). Command ids, labels and
every result stay English.
@@ -448,6 +597,17 @@ its proportion). A path or point set that draws nothing is refused instead of ma
0×0 layer, as is a size on an axis the geometry has none of (except 0, such as a horizontal line's
height) and a size of 0 or less on an axis it has, which would flatten it.
`node.set {path}` (or `set_properties`) replaces a vector's geometry with new SVG path data and
keeps the layer: its id, name, place in the layer list, paints, effects, styles, variable bindings
and constraints. The path is read as `vector.create` reads it, and `width`/`height` in the same call
scale it the same way. The path's own box goes at the layer's current x/y (rotation and flips
stay); with `x`/`y` in the same call it goes at `x` + its left edge and `y` + its top, as
`vector.create` places it. Points' own settings (per-point corner radius, caps, joins) go with the
old geometry. On several layers, layers that aren't vectors are skipped with a warning; on a
single one, or when none is a vector, the call is refused (convert shapes with
`vector.edit.begin` or Flatten first). A layer inside an instance is refused too: its geometry
comes from its main component, so edit that or detach the instance. Undo brings the old path back.
Vector edit mode is driven with `run_command` (or `pointer`/`key` for gestures):
`vector.edit.begin {id? | ids?}` (default the selection; converts shapes to vectors; several layers
are edited at once, the first active; `key` Enter with shapes selected does the same),
@@ -584,7 +744,8 @@ rotation?, color?, width?, height?, responsive?}` (`None`, `iPhone 16`, `iPhone
`Black`, `White`, `Pink`, `Teal`, `Blue` for the iPhone 16 (an error lists them), `""` for the
default; `width` / `height` set the custom size's screen, 1–10000 points, and pick `Custom size`,
which is a bare screen without a body). On a device the presented frame is sized to the device's
screen: what doesn't fit is clipped, or scrolls when the frame's `overflow` allows it, while `fixed`
screen: a frame bigger than the screen scrolls through its whole size whatever its `overflow`, content
past the frame's own edges scrolls only as its `overflow` allows (else it's clipped), and `fixed`
layers stay on screen (pinned to the edges their constraints name). With `responsive: true` (saved
in the file) the presented frame is instead resized to the device's screen, or without a device to
the presenter's window at 100%, and its constraints and auto layout respond as when it is resized
@@ -597,7 +758,10 @@ device, whether a click where nothing reacts briefly flashes the screen's hotspo
interactions, on by default; also in the presenter's Options menu) and whether its Inspect panel
shows the size, colours and typography of the layer under the pointer (also the Inspect button in
its bar); the options are kept for later presentations (in the presenter Z cycles them and R
restarts). The player's state is
restarts). An Open link to an `http` or `https` address first shows a notice that the link leaves
the prototype, with Open link, Cancel and "Don't show this again"; `proto.linkNotice {show?}`
turns the notice on (the default) or off, kept with the desktop app's preferences (the web build
keeps it until the page reloads). The player's state is
in `inspect_ui` (`presenter`, `preview`: screen, scroll, `scale`, `zoom`, `pan`, the screen's
`hotspots` and whether they are flashing, the `inspected` layer) when attached, and
`ui.set {presentScroll: [x, y]}` scrolls its screen (see [control-protocol.md](control-protocol.md)).
@@ -713,6 +877,14 @@ most when it can't be reached).
moves the chosen variant there, `{name, index}` reorders variant properties (the variants'
names follow). `component.properties` on a set reports `conflicts`: variants sharing one
combination of values.
- **Boolean properties**: on the main component, a boolean property's value is the visibility of
the layers bound to it. `component.addProperty {type: "boolean", default, layer}`,
`component.bindProperty {field: "visible"}` and `component.editProperty {name, default}` show or
hide the main's bound layers to match the default (without `default`, `addProperty` takes the
bound layer's visibility). Showing or hiding a bound layer on the main sets the default, and the
property's other bound layers (in every variant of a set) follow. New instances start from the
default; existing instances keep the values they have. Files open as saved: a main whose bound
layer disagrees with the default stays that way until one of them is edited.
- `component.pushChanges {id?}` copies the instance's overrides into its main (from a layer of a
nested instance too: always into the outer instance's main).
@@ -720,7 +892,9 @@ most when it can't be reached).
- `instance.swap {ids?, component}` shows another component in the instances (overrides on layers
with matching names stay; property values with the same names too). Nested instances can be
swapped on their own (an override of the instance they're in).
swapped on their own (an override of the instance they're in); a swapped nested instance keeps
what its outer main overrode on matching layers as its own, so later edits of those in the main
don't reach it.
- `instance.swapAt {component, x, y}` swaps the instance at a page point: of the instances under
it (innermost first), the first showing a variant of the same component set, else the innermost
(never one inside the dropped component); only text layers keep their overrides. The Assets
@@ -798,6 +972,8 @@ click or drag across regions with `pointer` to merge, add `alt` to remove; `shap
- **Text on a path**: `text.onPath {text?, path?, start?, flip?}` (default: the selected text layer
and the other selected layer) puts a text layer on a vector's or shape's path: the text takes the
path layer's place, transform and box, and the path layer is removed (`edit.undo` brings it back).
The text also takes the path layer's effects when it has any, and its fills when one of them
shows, each with its style and variable bindings; otherwise the text keeps its own.
The baseline follows the path from `start` (0..1 of its length); the text's horizontal alignment
places it there (left starts, centre centres, right ends at it); `flip` runs it on the other side
the other way. Glyphs past an open path's end are hidden; a closed path (ellipse, rectangle) wraps
@@ -805,7 +981,12 @@ click or drag across regions with `pointer` to merge, add `alt` to remove; `shap
them, `text.removeFromPath {ids?}` makes it plain auto-width text again. Resizing the layer
scales the path. The text stays editable (`text.edit.*`, `type_text`); `get_node` shows
`textPath {d (SVG path data in the layer's space), start, flip}`. Exports draw it as outlines
(PDF too: not selectable); underline and strikethrough aren't drawn on a path.
(PDF too: not selectable); underline and strikethrough aren't drawn on a path. With the tool
`tool.textOnPath`, a `pointer` press within 6 screen pixels of a shape's or vector's first
subpath puts a new, empty text layer on it, starting at the press, and starts typing: the text
also takes the path layer's constraints and effects, and its fills when one of them shows. Ending
the edit with nothing typed puts the path layer back with no undo step. Locked layers and layers
inside instances are skipped.
- **Pattern fills**: `fill.pattern {source (layer id), ids?, index?, tile? (grid|horizontalHex|
verticalHex), scale?, spacing? [x, y], align? (start|center|end), opacity?}` makes the fill at
`index` (default the top one) the source layer repeated, at `scale` × its own size with `spacing`
@@ -903,7 +1084,9 @@ frame back to Auto, inheriting its parent's mode).
The same JSON carries variables between FigCraft files: `variable.export {collections?, format?,
path?}` writes this file's collections (default: all but library copies) as a local variables
response (`format: "json"`, the default) or as CSS custom properties (`format: "css"`: the default
mode on `:root`, each other mode on `[data-<collection>="<mode>"]`, aliases as `var(--name)`), and
mode on `:root`, each other mode on `[data-<collection>="<mode>"]`, aliases as `var(--name)`, numbers
in px unless the layers bound to them, their scopes or the variables aliasing them make them an
opacity or a font weight, so `border-radius: var(--radius)` reads `12px`), and
returns `{text}` (or writes `path` and returns `{path}`). `variable.import {variables}` (an object,
JSON text or a path; `variableCollections` and `variables` under `meta` or at the top level) reads
one back: a collection joins this file's collection of the same name
@@ -985,7 +1168,8 @@ branches). Driven with `run_command`:
- `branch.create {name?}` copies the file into a new branch tab and returns `{id, name, index}`.
The branch remembers the file as it is now (its base) and makes new ids in its own id space, so
layers, styles and comments made on both sides never clash.
layers, styles and comments made on both sides never clash. Its history starts with a `Branch
created` version.
- `branch.list` lists the branches of the file shown (or of the file a branch tab belongs to):
`[{id, name, time, state: active|merged|archived, index?}]` (`index`: its open tab).
- `branch.open {id}` opens a branch in a tab (or switches to it). Edit it like any document.
@@ -1007,15 +1191,26 @@ branches). Driven with `run_command`:
conflict key to `"branch"` or `"main"`; the merge is refused while a conflict has none, so
neither side's work is dropped without a choice.
- `branch.updateFromMain {id?, resolutions?}` brings the main file's changes since the base into the
branch the same way (the new base is the main file as it is now). It is not undoable: the branch
tab's undo history starts over.
branch the same way (the new base is the main file as it is now) and adds an `Updated from main`
version to the branch's history. It is not undoable: the branch tab's undo history starts over.
- `branch.archive {id?}` sets a branch aside without merging (it stays in the file and can still be
opened, but not merged).
Each branch keeps its own version history: on a branch tab, `file.saveVersion`, `version.autosave`
and the `version.*` commands (list, preview, restore, rename, delete, duplicate) act on the
branch's versions, which are saved in the file with the branch and stay with it when it is merged
or archived. Branches aren't available in a live session.
or archived.
In a live session branches work the same way and stay with the person who made them (others
don't see them). A branch made in the session makes its ids in your own client space, far above
the ids the main file makes, so the session server takes them. Everyone's edits on main while the
branch is open are main's changes in the review (against the branch's own base), and conflicts
with them are resolved as offline. `branch.merge` is one edit of the session document: everyone
gets it, and it is one step of your own undo history. It is refused on a view link, and for a
branch made outside the session (or in an earlier one) whose new layers, styles, variables or
comments lie in another id space, which the server would refuse; merge those once the file has
left the session. While a session is still starting, branch commands that need your client id
wait for it.
### Collaboration changes
@@ -1246,17 +1441,24 @@ Plugin settings are kept per plugin with the preferences (in a headless session,
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"plugin.event","params":{"id":"org.figcraft.examples.swatch-grid","control":"generate"}}}}
```
Widgets (API 3, layers on the canvas a plugin draws) are driven the same way: `widget.insert
Widgets (API 3 and 4, layers on the canvas a plugin draws) are driven the same way: `widget.insert
{plugin, x?, y?}` puts one on the page (centred in the view without a position) and returns `{id,
plugin, state, targets, output, logs, calls, fuel}`; `widget.click {id, target}` clicks one of its
`targets` as a click on the canvas would (the plugin redraws it; one undo step; returns the same);
`widget.list` lists every widget in the file with its plugin, page, state and targets. A widget's
state is layer data, so it is saved, undone and synced with the file.
`widget.list` lists every widget in the file with its plugin, page, state and targets. With plugin
API 4 a widget can have editable text fields and a property menu (both listed with it, as `fields`
and `menu`): `widget.setText {id, field, value}` sets a field as typing in it would (the value,
at most 1,000 characters, goes into the state under the field's name and the plugin redraws; one
undo step), and `widget.menu {id, item, value?}` uses a menu item (a toggle's `value` is `true` or
`false`, by default the opposite of now; a colour's is one of its options). A widget's state is
layer data, so it is saved, undone and synced with the file.
```json
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"widget.insert","params":{"plugin":"org.figcraft.examples.counter"}}}}
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"widget.click","params":{"id":12,"target":"increment"}}}}
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"widget.list","params":{}}}}
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"widget.setText","params":{"id":12,"field":"label","value":"Votes"}}}}
{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"widget.menu","params":{"id":12,"item":"big"}}}}
```
Plugins from a `figcraft-server`'s plugin directory ([`plugins.md`](plugins.md#plugin-directories)):
@@ -1275,15 +1477,33 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
### Dev Mode
- **Component code:** `inspect.code {id, language: react|leptos|vue|swiftui|compose}` on a component,
- **Pages as React + Tailwind:** `inspect.code {id, language: "tailwind"}` writes a layer tree as a
React function component styled with Tailwind CSS v4 utilities and no stylesheet. Auto layout
becomes `flex`/`flex-col`/`gap-*`/`p-*` on the 4px spacing scale, fixed-width auto layout frames
and text become fluid (`w-full max-w-[960px]`; as wide as the frame: `w-full`) and a frame's
fixed height a minimum, per-side strokes become `border-b` and the like, and instances are
inlined with their overrides (component code is `language: react`), except instances whose component
is connected to code (`codeConnect.set`/`codeConnect.import`, React): those are their snippet, e.g.
`<Button variant="outline">{"See examples"}</Button>`, with each import once at the top. A design
system mapped to shadcn/ui this way exports pages that use the project's own components. Bound variables become theme
utilities (`bg-accent`, `rounded-card`, `text-muted`) that `variable.export {format: "tailwind"}`
declares: `@import "tailwindcss";` and an `@theme static` block (every token is emitted, also those only read through `var()`) named by namespace (`--color-*`,
`--radius-*`, `--text-*`, `--spacing-*`, `--font-*`; a number's namespace and unit come from what
layers bind it to), other modes on `[data-<collection>="<mode>"]`. With `mobile: <frame id>` (the
same layer names, mobile layout) the code is mobile first: the mobile frame's classes, the
desktop frame's that differ behind `lg:`, and layers only on desktop `hidden` plus `lg:flex`
(auto layout frames) or `lg:block` (other layers).
- **Component code:** `inspect.code {id, language: react|leptos|vue|swiftui|compose|arkui}` on a component,
a component set or a variant writes the component as a reusable unit: React (TSX) with a props
interface, Leptos with a `#[component]` function (`#[prop]` defaults, `view!` markup), Vue as a
single-file component (`<template>`, a `<script setup>` with `defineProps` and its defaults, a
`<style scoped>`; a slot layer is Vue's own `<slot>`, so it takes no prop), SwiftUI
with a `View` struct, Compose with a `@Composable` function. Its component properties are the
with a `View` struct, Compose with a `@Composable` function, ArkUI with a `@Component` ArkTS
struct (`@Prop` parameters, `@BuilderParam` for swaps and slots). Its component properties are the
parameters, with their defaults: variant properties become string unions (React, Vue) or enums,
booleans become flags around the layers they show, text properties strings, instance swaps views
(`React.ReactNode`, Leptos `ViewFn`, `AnyView`, a composable lambda) and slots optional content.
(`React.ReactNode`, Leptos `ViewFn`, `AnyView`, a composable lambda, an ArkUI builder) and slots
optional content.
Every variant of a set shares one definition; in React, Vue and Leptos the other variants'
differences become CSS rules on `data-*` attributes. A Vue template holds no arbitrary
expressions, so what React writes as a braced expression has its own spelling there: text is
@@ -1292,13 +1512,14 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
instances) each instance is a call passing only the values it changes, e.g. `<Button
state="Hover" label="Buy" />`, `<Button state=ButtonState::Hover label="Buy" />`,
`<Button state="Hover" />`, `Button(state:
.hover, label: "Buy")`, `Button(state = ButtonState.Hover, label = "Buy")`. Names are escaped and
.hover, label: "Buy")`, `Button(state = ButtonState.Hover, label = "Buy")`, `Button({ state:
ButtonState.Hover, label: "Buy" })`. Names are escaped and
unique (keywords get a suffix, `default` → `defaultValue`, Rust's `type` → `type_`; Leptos names
are snake_case; names that would shadow framework types get one too, `Text` → `TextComponent`). `inspect.componentCode
{id?, language?}` returns the parts: `{component, name, set, properties: [{name, param, type,
default, value? (instances)}], definition, usage? (instances)}`.
- **Code connect:** link a component (a variant links its set) to the code component it is built
as, per language (`react`, `leptos`, `vue`, `swiftui`, `compose`): `codeConnect.set {id?, language?, importPath?,
as, per language (`react`, `leptos`, `vue`, `swiftui`, `compose`, `arkui`): `codeConnect.set {id?, language?, importPath?,
name?, template?}` (undoable; fields left out keep their value; the defaults are the generated
name and a call passing every property) → `{component, language, code, unknown}`. Templates are
text, never executed: `{property}` (its name or parameter name) is the instance's value escaped
@@ -1314,7 +1535,9 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
strings, any other `{…}` stays as written (`unknown` lists placeholder names that aren't
properties). Then `inspect.code` on an instance or the component returns the import line and the
snippet instead of generated code (`connected: true`), connected instances inside a generated
layer tree are their snippet, and `inspect.componentCode` returns `connected: {importPath, name,
layer tree (also in a component's definition, in the content an instance puts in its slots, and
the components a connected template's instance swaps place) are their snippet with their import
lines once each at the top of the code, per language, and `inspect.componentCode` returns `connected: {importPath, name,
template, import, code}` with the snippet as `usage`. Example: template `<Button
variant="primary" icon="{Has icon ? "cart" : "none"}">{label}</Button>` with import path
`@acme/ui` gives `import { Button } from "@acme/ui";` then `<Button variant="primary" icon="none">{"Buy
@@ -1327,7 +1550,8 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
warning). `get_node` lists a component's links as `codeLinks`. In the app, the Inspect tab's
Connect to code section edits them in the Code section's language, with a live preview and a
Connected badge. An editor extension isn't needed: agents and editors read the same snippets
through this server. Plugins whose manifest asks only for `read` also run from the Inspect tab.
through this server. Plugins whose manifest asks only for `read` also run from the Inspect tab,
with their panels (`plugin.ui`); their callbacks can only read.
- **Dev resources:** links for developers (docs, code, tickets) on any layer, each a URL and a
name. `devResource.add {id?, url, name?}` → `{id, index}` (no name: the URL without its scheme,
`www.` and a trailing slash; no page title is looked up), `devResource.edit {id?, index, url?,
@@ -1339,6 +1563,22 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
are clipped to 200 characters; a layer holds at most 32 links. Links are data: never fetched or
opened by the engine (the app opens one when it is clicked). `get_node` and `inspect_document`
list them as `devResources`; the Inspect tab lists, adds, edits and removes them.
- **Measurements kept on the canvas:** distances a developer leaves on the canvas for everyone.
`devMode.addMeasurement {from? (default the first selected layer), to?, side?}` → `{from, index,
lines}` places one between `from` and `to` (a layer on the same page), along one side of `from`
(`side`: `top`, `right`, `bottom` or `left`: that side's length) or, with neither, to the layer
around `from`; `devMode.removeMeasurement {from?, index?}` → `{removed}` (no index: every one
starting from the layer); both undoable. `devMode.measurements {page?, id?}` → `[{from, fromName,
index, to?, toName?, side?, labels, lines: [{x0, y0, x1, y1, label, dashed}]}]` lists those on a
page (default the current one; `id`: only those from or to that layer), with their lines in page
space measured from the layers' current bounds, so they follow the layers. A measurement is
stored on the layer it starts from (`get_node` lists them as `measurements: [{to} | {side}]`),
saved with the file and synced; the same two layers are measured once (either way round), a
layer starts at most 64. One whose other layer was deleted or moved to another page isn't listed
or drawn (an undo brings it back); a copy keeps the side measurements and those to layers copied
with it. In the app, Dev Mode draws them with the hover redlines, and the Inspect tab lists them
(with nothing selected, every one on the page), selects a measurement's layer on a click, removes
one with −, and places new ones from its Add measurement menu.
- **Annotations:** notes for developers on any layer, each with optional pinned properties that
Dev Mode shows with their current values (and the style or variable behind them).
`annotation.add {id?, label?, properties?}` (a note, pinned properties or both; returns `{id,
@@ -1366,17 +1606,23 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
tab filters the callouts by category and edits the categories (colour, name, delete, add); each
note has a category menu.
- **Ready for dev:** frames at the top of a page or section, and sections, carry a handoff status
(`node.set {devStatus: none|readyForDev|completed}`). `inspect.readyForDev {status?:
readyForDev|completed|any, page?}` lists them across pages (or on one page) in page and layer
order: `{items: [{id, name, page, status}]}`. The Inspect tab sets the status of the selected
frames and, with nothing selected, lists them with a status filter (click one to go to it, or its
focus button to open it in the focus view).
(`node.set {devStatus: none|readyForDev|completed}`; other layers are refused a status).
`inspect.readyForDev {status?: readyForDev|changed|completed|any, page?}` lists them across pages
(or on one page) in page and layer order: `{items: [{id, name, page, status, changed}]}`
(`changed`: ready for dev and edited since). The Inspect tab sets the status of the selected
frames and, with nothing selected, lists them with a status filter (Ready for dev, Changed,
Completed, Any status; changed ones carry a Changed badge; click one to go to it, or its focus
button to open it in the focus view). In the app's Dev Mode the canvas only inspects: presses
select (and pan, and comment), never move, resize, draw or edit text and vectors, and the menus
and shortcuts refuse commands that edit the design (Dev Mode's own annotations, statuses, dev
resources, measurements, code connections and comments still work); choosing a drawing tool in
the toolbar goes back to the design, and the layers panel only selects and expands. Commands sent over MCP or the control channel aren't affected.
- **Compare changes:** when a layer marked Ready for dev first changes (it turns "Changed"), the
file keeps a copy of it and everything in it as it was just before that edit (saved in the
`.figcraft` file only while there is a change; resolving the change, marking it again, another
status or deleting it drops the copy, so the next change compares with that state).
`dev.compare {id?, show?}` → `{id, name, changed, snapshot, added, removed, modified, truncated,
changes: [{id, name, type, change: added|removed|changed, depth, layers, properties?: [{section,
`dev.compare {id?, version?, show?}` → `{id, name, changed, snapshot, added, removed, modified,
truncated, version: {id, name, time} | null, changes: [{id, name, type, change: added|removed|changed, depth, layers, properties?: [{section,
property, before, after, text}]}]}`: layers matched by id, added and removed ones listed as a
whole (`layers` counts what's inside), changed ones with their properties formatted like Inspect
(`width 120 → 136`, `fill #FF0000 → #E03030`, `text “Welcome” → “Hello”`, `parent Badge → Screen`,
@@ -1384,7 +1630,11 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
opacity, visibility, name), nested layers and instance contents included. `snapshot: false` means
the frame changed before copies were kept (an older file). `show: true` opens the compare view in
the app: before and now side by side (or overlaid with a slider), changed layers outlined (added
green, changed orange, removed red), the list of changes and Resolve change.
green, changed orange, removed red), the list of changes and Resolve change. With `version` (an id
from `version.list`) any top-level frame or section, ready for dev or not, is compared with that
saved version instead (a layer the version doesn't have was added as a whole); the compare view
picks it at the bottom (Compare with: When marked ready, or a version; a frame that isn't ready
for dev compares with the newest version), and `ui.set {devCompareVersion}` sets it.
- **Focus view:** `dev.focus {id? | direction: next|previous}` → `{id, name, page, index, count}`
shows one top-level frame or section alone on the canvas (fitted, selected, other layers neither
drawn nor hit), switching page when needed; `direction` steps through the ready-for-dev frames
@@ -1473,6 +1723,14 @@ Pass the returned target list to:
- `selection.applyColorStyle {targets, style}` to apply a document paint style. As with `style.apply`,
this replaces the matching node's entire fill/stroke list and links the style. Text ranges copy
resolved paints and keep their typography, without a paint-style link.
- `selection.bindColorVariable {targets, variable}` to bind a document color variable (id or name)
to those solid fill and stroke slots, as `variable.bind` does with `fills/<i>/color` or
`strokes/<i>/color`. A color style on the list and an earlier binding of the slot are removed.
Text ranges with fills of their own take the variable's value in the layer's modes, without a
binding. Gradients and other variable types are refused; scopes only filter what the Selection
colors menu offers, as for `variable.bind`.
- `selection.detachColor {targets}` to unlink the color style and the color variable of those
slots, keeping their values.
Keep the targets fixed throughout a live interaction. A color becoming equal to another color
must not add slots to the gesture. Invalid or missing targets fail atomically. Direct paint edits
@@ -1667,27 +1925,49 @@ Align and distribute as one undo step:
### Step references
A step in a `batch` can use what an earlier step returned. `{"$step": n}` anywhere in a step's
params (a value, an item in `ids`, a nested field) is replaced by the `id` in the result of step `n`
(zero-based) before the step runs; `{"$step": n, "field": "name"}` takes another field of that
result. This builds a nested tree in one call and one undo step, without guessing ids:
A step in a `batch` can use what an earlier step returned. Give the step a name with `"as"` (next to
`command` and `params`), and `{"$ref": "name"}` anywhere in a later step's params (a value, an item
in `ids`, a nested field) is replaced by the `id` in that step's result before the later step runs.
This builds a nested tree in one call and one undo step, without guessing ids or counting steps:
```json
{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"batch","arguments":{"label":"Card","commands":[
{"command":"node.create","params":{"type":"frame","name":"Card","props":{"layout":"vertical","gap":12}}},
{"command":"node.create","params":{"type":"text","parent":{"$step":0},"text":"Ada Lovelace"}},
{"command":"node.create","params":{"type":"frame","parent":{"$step":0},"name":"Button"}},
{"command":"node.create","params":{"type":"text","parent":{"$step":2},"text":"Follow"}}]}}}
{"command":"node.create","as":"card","params":{"type":"frame","name":"Card","props":{"layout":"vertical","gap":12}}},
{"command":"node.create","params":{"type":"text","parent":{"$ref":"card"},"text":"Ada Lovelace"}},
{"command":"node.create","as":"button","params":{"type":"frame","parent":{"$ref":"card"},"name":"Button"}},
{"command":"node.create","params":{"type":"text","parent":{"$ref":"button"},"text":"Follow"}}]}}}
```
A reference to the same or a later step, a negative or non-integer index, a missing `field` or any
other key next to `$step` fails the whole batch, which is rolled back. Only an object whose keys are
`$step` (and optionally `field`) is a reference: text such as `"$step 0"` is left as written. The
`$step` key is reserved in step params: an object holding it anywhere (in plugin data or a variable
value too) is read as a reference. A step that is itself a `command.batch` is passed on as written,
so the references in its steps are to its own steps, not the outer batch's.
`{"$step": n}` refers to a step by its zero-based position instead, and works in the same places;
the two can be mixed. Names are any non-empty string and must be unique within the batch.
The end-to-end tests in `crates/mcp/tests/agent_tasks.rs` are worked examples of realistic tasks
`"field"` takes something other than the `id`: a key of the result (`{"$ref": "card", "field":
"name"}`) or a path into it, with `/` between the parts and list items by zero-based index, the way
`variable.bind` names `fills/0/color`. `edit.duplicate` returns `{"ids": [...]}`, so its first copy is
`ids/0`:
```json
{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"batch","arguments":{"label":"Copy","commands":[
{"command":"edit.duplicate","as":"copy","params":{"ids":[5]}},
{"command":"node.set","params":{"ids":[{"$ref":"copy","field":"ids/0"}],"props":{"name":"Card copy"}}}]}}}
```
A path may start with `/`, as in JSON Pointer (`/ids/0`). A key that itself holds `/` is matched as
written before it is read as a path. A path ends at whatever value it reaches, so `"field": "ids"` is
the whole list.
These fail the whole batch, which is rolled back, with an error naming the step: two steps with the
same `as` (before any step runs); a `$ref` to a name no step has (the error lists the names there
are) or to the same or a later step; a `$step` to the same or a later step, or that is negative or
not an integer; a `field` the result doesn't have (the error names the full path and, for a list,
how many items it has); any key next to `$ref` or `$step` other than `field`. Only an object holding
a `$ref` or `$step` key is a reference: text such as `"$ref card"` is left as written. Both keys are
reserved in step params: an object holding either anywhere (in plugin data or a variable value too)
is read as a reference. A step that is itself a `command.batch` is passed on as written, so the
references in its steps are to its own steps and names, not the outer batch's; the outer batch can
reach into its result list with a path (`{"$ref": "inner", "field": "1/id"}`).
The end-to-end tests in `crates/mcp/tests/it/agent_tasks.rs` are worked examples of realistic tasks
(auto layout, a card built in one batch with step references, components and overrides, gradients
and shadows, paint styles, Light/Dark variables, align/distribute/group, boolean subtract + flatten,
export, undo/redo, drawing with tools).
+2 -2
View File
@@ -1,8 +1,8 @@
# Parity checklist
> **Generated:** 2026-10-17 by `cargo xtask parity`. Do not edit by hand.
> **Generated:** 2026-10-21 by `cargo xtask parity`. Do not edit by hand.
FigCraft commands registered for the incumbent's main menu and toolbar (561 commands in the engine).
FigCraft commands registered for the incumbent's main menu and toolbar (575 commands in the engine).
**212 / 212 menu items (100.0%)**
+75 -16
View File
@@ -18,7 +18,8 @@ permissions its manifest asks for, and nothing more.
plugin can also keep settings from one run to the next.
- **Widgets (API 3).** A plugin can put a layer of its own on the canvas: it describes the layer
(frames with auto layout, text, shapes, images), FigCraft builds it, and a click on one of its
parts calls the plugin back to redraw it from its state, which is kept in the document.
parts calls the plugin back to redraw it from its state, which is kept in the document. Since
API 4 a widget can also have editable text fields and a small property menu.
- **Permissions.** The manifest says whether the plugin only reads the document or may edit it, and
can narrow the commands it may call. FigCraft enforces both.
- **Distribution.** A team's own `figcraft-server` can keep a plugin directory: plugins are
@@ -91,6 +92,17 @@ and its state (a small JSON object) in its own layer data, so a saved file, a co
a live session carries the widget as it is, and a copy is a widget of its own. A widget whose
plugin isn't installed stays as it was drawn; clicking it says the plugin is missing.
**Fields and menus (API 4).** A text layer of a widget can be an editable field: double-click it
with the Move tool and it is edited like any text (all of it selected first). When editing ends
(Escape, or a click elsewhere), the typing is put back and the new value goes to the widget
instead: it is cleaned (line breaks kept, tabs become spaces, other control characters dropped),
refused when longer than 1,000 characters, written into the widget's state under the field's
name and into the field's text, and the plugin is called back to redraw, all as one undo step
(none when the text didn't change). The value lives in the state, so it syncs and saves like the
rest of it. A widget can also carry a property menu: while it is the only layer selected, a
small bar over it shows its actions and toggles as buttons and its colour choices as swatches;
using one calls the plugin back, one undo step. The labels are the plugin's own text.
In a presented prototype (the presenter or the preview pane) clicks on a widget's clickable parts
call its plugin back the same way, with the same limits, but on the prototype's own copy of the
file: the widget changes in the prototype only, the file and its undo history stay as they are,
@@ -115,6 +127,8 @@ From scripts, the CLI and MCP the same commands apply, panels included:
{"command": "widget.insert", "params": {"plugin": "org.figcraft.examples.counter", "x": 0, "y": 0}}
{"command": "widget.list", "params": {}}
{"command": "widget.click", "params": {"id": 12, "target": "increment"}}
{"command": "widget.setText", "params": {"id": 12, "field": "label", "value": "Votes"}}
{"command": "widget.menu", "params": {"id": 12, "item": "color", "value": "#14AE5C"}}
```
`figcraft-cli run script.jsonl --in design.figcraft --out tidy.figcraft` runs that headless; over
@@ -132,9 +146,11 @@ plugins: import them in the script.
| `plugin.close` | `{id}` | Closes plugin `id`'s panel and ends its session. `{closed}` says whether one was open. |
| `plugins.runLast` | `{}` | Runs the last plugin command again. |
| `plugins.manage` | `{}` | Opens the Plugins window; returns the list like `plugin.list`. |
| `widget.insert` | `{plugin, x?, y?, fuel?}` | Puts widget plugin `plugin`'s widget on the current page, its top-left at `x`, `y` (default: centred in the view), and selects it; the plugin draws it. One undo step. Returns `{id, plugin, state, targets, output, logs, calls, fuel}`: `targets` are the names of its clickable parts. |
| `widget.insert` | `{plugin, x?, y?, fuel?}` | Puts widget plugin `plugin`'s widget on the current page, its top-left at `x`, `y` (default: centred in the view), and selects it; the plugin draws it. One undo step. Returns `{id, plugin, state, targets, fields, menu, output, logs, calls, fuel}`: `targets` are the names of its clickable parts, `fields` of its editable fields, `menu` its property menu items. |
| `widget.click` | `{id, target, fuel?}` | Clicks part `target` of widget `id` (or of the widget a layer `id` is in), as a click on the canvas would: the plugin is called back and redraws it. One undo step; returns like `widget.insert`. |
| `widget.list` | `{}` | `{widgets: [{id, plugin, name, page, state, targets}]}`: every widget in the file. |
| `widget.setText` | `{id, field, value, fuel?}` | Sets editable field `field` of widget `id` (or of the widget a layer `id` is in) to the text `value`, as editing it on the canvas would: the value (at most 1,000 characters, cleaned) goes into the state under the field's name and into the field's text, and the plugin is called back. One undo step; returns like `widget.insert`. |
| `widget.menu` | `{id, item, value?, fuel?}` | Uses item `item` of widget `id`'s property menu, as its bar would: an action takes no `value`; a toggle takes `true` or `false` (default: the opposite of now); a colour takes one of its options (hex, any case). The plugin is called back. One undo step; returns like `widget.insert`. |
| `widget.list` | `{}` | `{widgets: [{id, plugin, name, page, state, targets, fields, menu}]}`: every widget in the file. |
| `plugin.directory.list` | `{server?, query?}` | The plugin directory at `server` (`ws://host:port/`; default: the one browsed last): `{server, plugins: [{id, name, version, description, author, api, access, commands, widget, bytes, sha256, published, versions, installed, update}]}`, the latest version of each plugin whose id, name, description or author contains `query`. `installed` is the installed version (or null), `update` whether the listed one is newer. |
| `plugin.directory.install` | `{id, version?, confirm, server?, sha256?}` | Downloads version `version` (default: the latest) and installs it, or updates the installed one. `confirm` must be `true` (installing runs the plugin's code: show its permissions first); without it nothing is downloaded. The download must match the SHA-256 the directory lists (and `sha256`, the one shown, when given). Returns `{installed, name, version, permissions, sha256, path, source, replaced}`. |
| `plugin.directory.publish` | `{path \| plugin, server}` | Publishes a plugin folder (or its `figcraft-plugin.json`), or an installed plugin, as a new version. `server` carries the server's key: `ws://host:port/?key=…`. Returns `{published, server}`. |
@@ -224,7 +240,8 @@ plugin directory requests (`plugin.directory.*` says so). Import plugins with `p
| `version`, `description` | Optional text. A plugin published to a [directory](#plugin-directories) needs a `version` of one to four numbers (`1.2.0`), each version newer than the last. |
| `author` | Optional: who made it, as you type it (64 characters at most); plugin directories show it. |
| `main` | Required. The module, a `.wasm` path relative to the manifest, inside its folder. |
| `api` | The plugin API version the plugin was written for: `1` (the default), `2` (panels and settings) or `3` (widgets). A manifest asking for a newer one than FigCraft runs is refused, so a plugin that needs panels or widgets fails clearly on an older FigCraft. |
| `api` | The plugin API version the plugin was written for: `1` (the default), `2` (panels and settings), `3` (widgets) or `4` (saving files, widget fields and property menus). A manifest asking for a newer one than FigCraft runs is refused, so a plugin that needs panels, widgets or `saveFile` fails clearly on an older FigCraft. |
| `permissions.saveFile` | API 4: `true` lets the plugin offer a file to save (an exporter's output). FigCraft asks the user where it goes with its own Save dialog (a download in the browser); the plugin never gets a path. Manage plugins lists it ("can save files"). A plugin directory's entries don't carry it (their wire format stays as it is), so installing any API 4 plugin from one says it may offer files to save. |
| `widget` | API 3: `true` when the plugin draws a widget (it's listed in Plugins ▸ Widgets). |
| `permissions.access` | `read` (the default): commands that leave the document as it is; such plugins also run from Dev Mode's Inspect tab. `write`: edits too. |
| `permissions.commands` | Optional allowlist: exact command ids or prefixes ending in `*` (`node.*`). Empty: any command the access allows. |
@@ -236,6 +253,9 @@ plugin directory requests (`plugin.directory.*` says so). Import plugins with `p
Whatever its manifest says, FigCraft refuses these calls (the plugin gets the refusal as an error
it can handle):
- read or write files: a plugin never gets a path or the disk. With `permissions.saveFile` it can
only hand FigCraft a file (one per run, 16 MB at most, a plain file name) for the user to save
through FigCraft's own dialog; the user can cancel it;
- files and the app: `file.*`, `app.*`, `pref.*`, `help.*`, `library.add`, `libraries.*`,
`image.place` with a `path` (pass the image as base64 `data` instead), `variable.export`,
`codeConnect.export` and `component.playground` with a `path` (leave it out to get the result
@@ -259,17 +279,22 @@ WebAssembly in a debug build, far less in a release build), 64 MB of memory, 100
16 MB. A panel spec is at most 64 KB with 64 controls; settings are at most 64 KB per plugin (keys
and JSON values), keys 1–128 characters. A widget callback gets the same limits; its description is
at most 4 MB with 256 layers nested 16 deep, its state at most 16 KB (keys 1–128 characters), and
its images 1 MB each, 2 MB together and 4096 pixels on a side.
its images 1 MB each, 2 MB together and 4096 pixels on a side. A property menu has at most 8
items (labels up to 32 characters), a colour item 1–8 colours; a field's value is at most 1,000
characters.
Copying a byte across the guest-memory boundary costs one additional fuel, reserved before the
allocation or copy. This also applies when the configured per-call overhead is zero. A reply
read is charged for the bytes actually returned, not the requested buffer size. `calls` counts
every ABI import, including calls which only query the reply length.
## ABI (version 3)
## ABI (version 4)
Version 2 adds panels and settings to version 1, and version 3 adds widgets; a version 1 or 2
module runs unchanged and gets the input it always got. Strings are UTF-8 `(ptr, len)` pairs in the plugin's linear memory; all values
Version 2 adds panels and settings to version 1, version 3 adds widgets, and version 4 adds
`save_file` and widget fields and property menus (new keys in the widget description and new
widget events); a module written for an older version runs unchanged and gets the input it always
got. A widget description with a field or a menu from a plugin whose manifest says `3` is refused,
and a FigCraft that runs an older API refuses a manifest that asks for `4` when it's imported. Strings are UTF-8 `(ptr, len)` pairs in the plugin's linear memory; all values
are `i32`.
The module **exports** `memory` and `figcraft_run(input_len) -> status`. When `figcraft_run` starts,
@@ -295,6 +320,15 @@ for a new widget) and what was clicked (`event` is null when it's inserted):
{"api": 3, "command": "widget", "params": {}, "selection": [], "widget": {"id": 12, "state": {"count": 2}, "event": {"target": "increment"}}}
```
With API 4 the event can also be an edited field (its value is already in `state` under the
field's name) or a property menu item (`value`: null for an action, the new `true`/`false` for a
toggle, the colour picked for a colour item):
```json
{"api": 4, "command": "widget", "params": {}, "selection": [], "widget": {"id": 12, "state": {"count": 2, "label": "Votes"}, "event": {"field": "label", "value": "Votes"}}}
{"api": 4, "command": "widget", "params": {}, "selection": [], "widget": {"id": 12, "state": {"count": 2}, "event": {"menu": "color", "value": "#14AE5C"}}}
```
It returns `0` for success; anything else fails the run (and rolls it back), with the text passed
to `set_output` as the error message.
@@ -315,6 +349,7 @@ included, is refused when the plugin is imported):
| `setting_get` | `(key_ptr, key_len) -> status` | API 2. `0`: the reply holds the setting's JSON. `1`: there is none. |
| `setting_set` | `(key_ptr, key_len, value_ptr, value_len) -> status` | API 2. Stores JSON under the key, for this plugin only, kept with the preferences. `1`: refused (not JSON, a bad key, over 64 KB), the reply says why. |
| `setting_delete` | `(key_ptr, key_len)` | API 2. Forgets a setting. |
| `save_file` | `(name_ptr, name_len, mime_ptr, mime_len, data_ptr, data_len) -> status` | API 4, needs `permissions.saveFile`. Offers a file to save; FigCraft asks the user where it goes once the run succeeds. `1`: refused (no permission, a bad file name, over 16 MB, a second file in one run), the reply says why. |
| `widget_render` | `(ptr, len) -> status` | API 3. Draws the widget being inserted or clicked (the widget description below, as JSON). `1`: the description is invalid, the reply says why. A widget call that doesn't draw keeps the widget as it was (inserting one must draw it). |
A pointer or length outside the plugin's memory traps, which fails the run. Host functions can only
@@ -364,16 +399,29 @@ ignored in auto layout), `width` and `height`, `fill` and `stroke` (hex colours)
`vertical`), `gap`, `padding`, `justify` and `align` (`min`, `center`, `max`: along and across the
layout) and `children`;
with auto layout it hugs its contents on an axis without a size. A text has `text`, `size`,
`color` and `bold` (with a `width` it wraps). An image has `data`, the image's bytes (PNG, JPEG…)
`color` and `bold` (with a `width` it wraps), and with API 4 may have a `field` (a name like a
target's): the text is then an editable field showing its current value. An image has `data`, the image's bytes (PNG, JPEG…)
as base64, usually carried in the module itself; without a size it takes the image's.
FigCraft checks the whole description (kinds, sizes, numbers, colours, lengths, counts, depth,
image sizes) and refuses it if anything is off.
With API 4, `menu` beside `root` is the widget's property menu (left out: none), for example:
```json
"menu": [{"item": "reset", "label": "Reset"},
{"item": "big", "label": "Steps of 10", "type": "toggle", "checked": true},
{"item": "color", "label": "Colour", "type": "color", "options": ["#1E1E1E", "#0D99FF"], "value": "#0D99FF"}]
```
Each item has an `item` name (like a target's, each once), a `label` (default: the name) and a
`type`: `action` (the default), `toggle` (with `checked`) or `color` (with `options`, 1–8 hex
colours, and optionally `value`, one of them: the current one). Every redraw replaces the menu.
## Writing a plugin in Rust
`plugins/` in the repository is a workspace of its own: `plugins/sdk` (the crate
`figcraft-plugin-sdk`, `no_std` with `alloc`) wraps the ABI with serde_json, and
`plugins/examples` has three plugins:
`plugins/examples` has four plugins:
- **Tidy names** (`tidy-names`): cleans up the selected layers' names (extra spaces, the "copy" left
by duplicating) and optionally their letter case, inside the selection too. Two commands: one
@@ -384,13 +432,20 @@ image sizes) and refuses it if anything is off.
button; the palette
picked last is remembered as a setting for next time. **From hex colours…** takes a palette of
hex colours in the parameters form.
- **Counter** (`counter`, API 3): a widget, a count between − and + buttons; each click changes the
count kept in the widget's state. It only reads the document.
- **Counter** (`counter`, API 4): a widget, a count between − and + buttons under a label; each
click changes the count kept in the widget's state, a double-click on the label edits it, and its
menu resets the count, counts in tens or colours it. It only reads the document.
- **Export names** (`export-names`, API 4): saves the names of the selected layers (optionally the
layers inside them too) as `layer-names.txt`. It reads the document and offers the file with
`save_file`; FigCraft asks where it goes.
`cargo xtask plugins` (part of `cargo xtask wasm` and `cargo xtask ci`; it needs the
`wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown`) checks, tests and
builds them, and leaves each example's manifest and module side by side in
`target/plugins/dist/<example>/`, ready to import.
`target/plugins/dist/<example>/`, ready to import. With `CARGO_TARGET_DIR` set, that location is
`$CARGO_TARGET_DIR/plugins/dist/<example>/`. Plugin compilation and host tests use the caller's
same Cargo target directory; wasm modules are built under its `wasm32-unknown-unknown/release/`
directory. The plugin workspace keeps its own lockfile and release profile.
A plugin of your own:
@@ -442,7 +497,11 @@ panel calls back: `control`, `value` and every field's `values`); returning `Err
rolls it back. With `"api": 2` in the manifest: `show_ui(&json!({...}))` and `close_ui()` for the
panel, `setting(key)`, `set_setting(key, &value)` and `delete_setting(key)` for settings. With
`"api": 3` and `"widget": true`: `input.widget` (`id`, `state`, `target`) when the widget is
inserted or clicked, and `render_widget(&json!({"state": ..., "root": ...}))` to draw it. Outside
inserted or clicked, and `render_widget(&json!({"state": ..., "root": ...}))` to draw it. With
`"api": 4`, `input.widget` also has `field` or `menu` and the `value` that came with it, and the
description may have fields and a `menu`; with `"permissions": {"saveFile": true}` too,
`save_file(name, mime, &bytes)` offers a file; FigCraft asks the user where it goes and the plugin
never learns a path. Outside
WebAssembly every host call fails, so the plugin's own logic can be unit-tested with `cargo test`.
Any language that compiles to WebAssembly works the same way: export `memory` and `figcraft_run`,
@@ -455,8 +514,8 @@ import what you need from `figcraft`.
install counts, and updates are found when you browse, not in the background. The browser build
can't use a directory yet.
- Plugins written for other apps (JavaScript plugins) don't run.
- Widgets: clicks are their only events (no text input, hover, property menu or timers), they
can't open a panel, and their layers are frames, text, rectangles, ellipses and images only (no
- Widgets: clicks, field edits and menu items are their only events (no hover or timers), a field
is plain text (no placeholder, length or single-line setting), they can't open a panel, and their layers are frames, text, rectangles, ellipses and images only (no
vectors, components or variables).
- Panels: no images, sliders or free layout inside a panel.
- Data in the document: plugins keep data on layers (pages included) with `node.set {props:
+14 -5
View File
@@ -1,20 +1,21 @@
# Prototyping parity
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-17 · **Change:** minor (a Present tab with device and scale controls (#537); core editor leftovers: delays on mouse triggers and manual overlay offsets; controller buttons on key/gamepad triggers, widgets that react in prototypes, `proto.evaluate` and the mode lookup rules pinned) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-24 · **Change:** patch (the Triggers row no longer lists delays on mouse triggers as missing; top-level frames bigger than the device's screen scroll in the player; Change to, set variable and set variable mode lay out what they change in the player, as the canvas does; presentation tabs, one per page of a file; a Present tab with device and scale controls (#537); core editor leftovers: delays on mouse triggers and manual overlay offsets; controller buttons on key/gamepad triggers, widgets that react in prototypes, `proto.evaluate` and the mode lookup rules pinned) · **Target:** Figma Design (desktop app 126.10.7)
**Breadth ~92%, ready ~74% (estimated).** Every Figma trigger except media, every action except
media control, every transition and easing exist; key/gamepad triggers take controller buttons,
read from controllers on Windows and in the browser. Every rule of the public
documentation on triggers, actions, overlays, animations, easing, scrolling and interactive
components is a test case with what it implies (`crates/engine/src/proto_behaviour_tests.rs`, 7
tests on the model, and `crates/ui-egui/src/present_behaviour_tests.rs`, 12 tests driving the
player, 21 gestures among them; the tail: `crates/engine/src/proto_tail_tests.rs`, 4 tests, and
tests on the model, and `crates/ui-egui/src/present_behaviour_tests.rs`, 14 tests driving the
player, 21 gestures among them; presentation tabs: `crates/ui-egui/src/present_tab_tests.rs`, 6
tests; the tail: `crates/engine/src/proto_tail_tests.rs`, 4 tests, and
`crates/ui-egui/src/present_tail_tests.rs`, 5 tests), and all pass. Not yet checked against
larger real prototypes. Remaining: 2–5 h.
| Group | Figma | FigCraft | Missing |
|---|---|---|---|
| Triggers | On click, on drag, while hovering, while pressing, key/gamepad, mouse enter, mouse leave, mouse down, mouse up, after delay, on media hit, on media end | 10: OnClick, OnHover, OnPress, OnDrag, MouseEnter, MouseLeave, MouseDown, MouseUp, AfterDelay, KeyPress (keys, combinations and controller buttons) | media triggers, delays on mouse enter/leave/down/up; controllers on macOS, Linux and FreeBSD |
| Triggers | On click, on drag, while hovering, while pressing, key/gamepad, mouse enter, mouse leave, mouse down, mouse up, after delay, on media hit, on media end | 10: OnClick, OnHover, OnPress, OnDrag, MouseEnter, MouseLeave, MouseDown, MouseUp, AfterDelay, KeyPress (keys, combinations and controller buttons) | media triggers; controllers on macOS, Linux and FreeBSD |
| Actions | Navigate to, change to, back, scroll to, open link, open / swap / close overlay, set variable, set variable mode, conditional, play/pause/mute/seek media | 11: Navigate, Back, ScrollTo (with an offset), OpenOverlay, SwapOverlay, CloseOverlay, OpenUrl, ChangeTo, SetVariable, SetVariableMode, Conditional | media actions |
| Transitions | Instant, dissolve, smart animate, move in/out, push, slide in/out | all 8 | — |
| Easing | Linear, ease in/out/in-and-out, back variants, gentle, quick, bouncy, slow, custom bezier, custom spring | all 13 (springs physical) | — |
@@ -54,8 +55,10 @@ case pins the current behaviour).
| Open link | pass | always a new tab or browser window (opening in place would leave the prototype) |
| Change to: another variant of the instance, in place, with the interaction's animation | pass | `interactive_components_change_in_place` |
| Interactive components: every instance plays the interactions of the variant it shows; an interaction on the instance with the same trigger wins | **fixed** | instances played only interactions copied onto them when they were placed: an instance placed before its main got interactions did nothing, and one placed after kept the first variant's interactions after changing (a toggle went on but never off). New instances no longer copy them, and copies in older files give way to the variant shown; `instances_play_their_variants_interactions` |
| Presentation tabs: one per page of a file, named "file - page" (renames follow). Play on a page that has a tab shows it as it was (from another start frame it starts again). Tabs keep their own copy of the prototype and play on after their file closes; a saved file opened again gets its tabs back. A tab never changes a file that isn't in front. Its X or Escape ends it and goes back to its file, or Home | **fixed** | one "Present" tab: a second presentation replaced the first, Play restarted it, and closing the file ended it; `present_tab_tests` |
| Change to on a layer inside a nested instance changes the instance whose variant set holds the destination | **fixed** | the innermost instance (an icon inside a button) was changed instead |
| Set variable, set variable mode, conditional | pass | `proto_run_tests`, `proto_conditional_tests`, `present_variables_tests` |
| Change to, set variable and set variable mode lay out what they change as the canvas does: text sizes to what it shows, hugging frames around it | **fixed** | the player didn't lay out after them: an instance with a longer label changed to the main's width, and a bound text kept its old size; `change_to_lays_out_the_instance_around_its_own_content`, `set_variable_and_set_mode_lay_out_the_text_they_change` |
| Expressions: a variable reads the mode where the interaction runs (a frame that chose a mode, else the page's); `name[Mode]` reads a given mode whatever is chosen; operators on numbers, text (`+` joins), booleans and comparisons | pass | `mode_lookups_read_a_given_mode_or_the_layers_current_one`, `proto_expr_mode_tests` |
| Set variable mode, then a conditional or set variable later in the same interaction: they see the new mode | pass | `set_variable_mode_changes_what_later_expressions_read` |
| A set variable writes every mode of the variable, so a lookup of any mode reads the new value; a frame inside the screen that chose a mode keeps it after set variable mode | silent | pinned as stated |
@@ -86,10 +89,11 @@ case pins the current behaviour).
| Widgets react to clicks in a presented prototype | done | clicks only, like on the canvas |
| Interactive components: instances play their variant's interactions, overridden per trigger | done | |
| Drag-driven animations that follow the pointer, in their direction | done | |
| Presenter: device frames and colours, rotation, custom sizes, fit/fill/100%, responsive frames | done | device bodies drawn in code. Presenting opens its own Present tab, which keeps its screen and history while you go back to the design, with a thumbnail of the current frame, previous / next, device presets and scale choices; the frame is laid out again for the device's screen on the prototype's copy (constraints and auto layout; the file is untouched); on phones and tablets overflowing content drags while the device stays put, as the frame's overflow setting allows ("No scrolling" frames clip); closing the window asks about unsaved changes above the presentation (#537). Devices not compared with Figma's |
| Presenter: device frames and colours, rotation, custom sizes, fit/fill/100%, responsive frames | done | device bodies drawn in code. Presenting opens a tab for the page, named "file - page", which keeps its screen and history while you go back to the design, with a thumbnail of the current frame, previous / next, device presets and scale choices; the frame is laid out again for the device's screen on the prototype's copy (constraints and auto layout; the file is untouched); on phones and tablets overflowing content drags while the device stays put: a top-level frame bigger than the screen always scrolls (as in Figma, with no overflow setting), content past the frame's own edges as its overflow setting allows ("No scrolling" clips it); closing the window asks about unsaved changes above the presentation (#537). Devices not compared with Figma's |
| Hotspot hints, inspect panel in the presenter, comments in the presenter | done | |
| Preview pane following edits live | done | |
| Edit interactions on several layers at once | done | |
| Open link: a notice before a website opens, with a checkbox to skip it on this device | done | `http` and `https` links (a mail link opens at once); Open link opens it, Cancel or Escape drops it; the desktop app saves the checkbox with its preferences, and the web build keeps it until the page reloads; `proto.linkNotice {show}` turns it on or off (`present_links_tests`) |
| Opening a link in place | missing | |
| Controllers on macOS, Linux and FreeBSD | missing | the usual Rust controller library links the system's libudev on Linux; the pure-Rust desktop builds read none there yet |
| Prototype in a separate window; mirror to a phone | missing | see [hardware-parity](hardware-parity.md) |
@@ -100,6 +104,11 @@ case pins the current behaviour).
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | patch | The Triggers row's Missing column no longer lists delays on mouse enter, leave, down and up, which landed on 2026-10-13 (`crates/engine/src/proto_delay_tests.rs`, `crates/ui-egui/src/present_delay_tests.rs`). Numbers unchanged |
| 2026-10-24 | patch | Open link to a website first shows a notice that it leaves the prototype, with the link, Open link, Cancel and "Don't show this again", which the desktop app saves with its preferences and the web build keeps until the page reloads (`proto.linkNotice` turns the notice back on); a second link to a website that fires while the notice shows is dropped, and a mail link still opens. Before, the player opened the link at once, also from an After delay trigger. Numbers unchanged |
| 2026-10-23 | patch | A top-level frame taller (or wider) than the device's screen scrolls with no overflow setting, as Figma's documentation says; before, "No scrolling" frames such as the landing page template's Phone frame only clipped. Content past a frame's own edges still scrolls only as its overflow allows. Checked in the app on macOS. Numbers unchanged |
| 2026-10-22 | minor | Change to, set variable and set variable mode lay out what they change in the player as the canvas does: text sizes to what it shows and hugging frames follow (an instance with a longer label changed to the main's width; a bound text kept its old size). Two player tests. Breadth and ready unchanged. |
| 2026-10-17 | minor | Presentation tabs: one per page of a file instead of a single "Present" tab, named "file - page" (renames follow); Play shows a page's tab as it was; tabs outlive their file and come back when a saved file opens again. Numbers unchanged. |
| 2026-10-17 | minor | Community batch #753: a Present tab (#537): playback in its own tab that keeps its screen and history, a current-frame thumbnail, previous / next, device presets and scale; the frame reflows to the device's screen on the prototype's copy; phone and tablet content drags inside a fixed device as its overflow allows ("No scrolling" frames clip; iPad mini, MacBook Air and Desktop keep their bodies); the unsaved-changes question shows above the presentation. Checked in the app on Windows. Percentages and hours unchanged |
| 2026-10-13 | minor | Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. |
| 2026-10-12 | minor | Controller buttons on key/gamepad triggers (Controller menu, player polling on Windows and in the browser, REST import instead of skipping, control channel injection), widgets reacting to clicks in presented prototypes on the prototype's own copy, `proto.evaluate`; the mode lookup and set-variable-mode rules written as cases (they already held). Breadth 90% → 92%, ready unchanged at 74% (controllers on Windows and the browser only; no real prototype QA yet), 3–6 h → 2–5 h |
+24 -5
View File
@@ -68,6 +68,22 @@ FIGCRAFT_DEFAULT_SERVER=wss://design.example.org/ cargo build --release -p figcr
The release workflow leaves it unset. The public server's terms are in
[`online-sharing-terms.md`](online-sharing-terms.md).
## The default font
New text starts in Inter (`figcraft_doc::DEFAULT_FONT`). A distribution whose users mostly write a
script Inter lacks (Thai, say) can name another family with `FIGCRAFT_DEFAULT_FONT` when building;
unset or empty means Inter. Pick a family the build embeds from craft-fonts (the web build has no
other fonts) or one its users have installed:
```sh
FIGCRAFT_DEFAULT_FONT="Noto Sans Thai" cargo build --release -p figcraft
```
Files record each text's family, so the variable only changes what new text starts in: a file
saved by one build keeps its fonts in a build with another default. Like any other family, it
renders only where it is available (embedded or installed); elsewhere the text falls back to the
faces that are. The release workflow leaves it unset.
## What gets built
| Platform | Artifacts | Built on |
@@ -101,11 +117,14 @@ builds with `CRAFT_FONTS_DIR` and `CRAFT_FONTS_REQUIRED=1`, so releases embed it
rather than ship without them. The browser has no system fonts, so a web build without craft-fonts
can't draw text.
`crates/text/build.rs` reads both `fonts/manifest.txt` and `fonts/latin-manifest.txt`, so the pin
must be a commit that has the Latin manifest (today `4391305`, on craft-fonts'
`pr/latin-presentation-fonts` branch; re-pin to `main` once that lands). That commit has no
Noto Sans Georgian (craft-fonts' main has it but no Latin manifest), so desktop Georgian uses an
installed font; re-pin when one commit has both (craft-fonts#42). The web build embeds only Inter,
Noto Sans Georgian and the Archivo SemiBold wordmark, when the pin has them. Packages carry each embedded font's licence as `OFL-<family>.txt`
must combine the Latin presentation catalog and the current core UI faces. Required web builds
validate the emitted Inter Regular/SemiBold and Noto Sans Georgian Regular faces after filtering;
a missing required face, style or asset fails the required web build. Excluded Adobe rows
are skipped before resolving their asset paths.
Native builds retain the permitted Latin presentation faces. Source Sans 3 and Source Serif 4
are filtered on every target under FigCraft's asset policy. The web build keeps its smaller
allowlist: Inter, Noto Sans Georgian and Archivo SemiBold when supplied. Packages carry each
embedded font's licence as `OFL-<family>.txt`
(`copy_font_licences` in `packaging/env.sh`; `Contents/Resources/Licenses` in the macOS app).
### macOS
+64 -7
View File
@@ -1,6 +1,6 @@
# FigCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (community batch #753: Selection colors, smart-selection spacing, a Present tab; community fixes from #524 up (55 PRs: `.fig` import fidelity, paste from other apps, the Actions palette, editor fixes, server limits) and a boolean-op crash guard; community fixes from #478 up and a control-channel fix (#589); recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading, SVG import budgets, Leptos code, full screen, server snapshots, owner-only preferences, ruzstd 0.9.1, layer cycle repair, batch rollback, Direct3D 12 on Windows, plugin metering; vector edit mode regions: hover hatch, selection and drag; repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7)
Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and
[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours
@@ -16,8 +16,9 @@ are one Opus 5.5 agent; most milestones split across 4–6 agents on disjoint cr
3. **Tool-by-tool interaction pass** against the running Figma ([`ui-parity.md`](ui-parity.md)):
40–60 h, parallel by tool.
4. **Languages** on the landed localization layer: native reviews of the es, fr, de, pt-BR, ja,
ko and zh-Hans drafts and the es-419 overlay (people), then the other key languages: 3–7 h per language
plus native review.
ko, zh-Hans, id and cs drafts and the es-419 overlay (people; Russian and Turkish are reviewed),
the entries added since each catalog was written, then the other key languages (Hindi,
Vietnamese, Arabic): 3–7 h per language plus native review.
5. **Multiplayer M9**: accounts and teams on top of the live sessions and the standalone server (the WebSocket transport, hosting from the app, invite links that open the app, document sync, live cursors, selections, text carets, avatars and follow, per-user undo, reconnect with offline edits, collision-free ids for styles, variables, collections and comments, joining from the browser, and `figcraft-server` with persistence, publishing from the app and share links to edit or view landed), per [`collab-protocol.md`](collab-protocol.md) and [`server.md`](server.md): 45–90 h.
## Alpha gate
@@ -53,13 +54,14 @@ blockers.
| M3 Components & styles depth | ~78% (extended collections, variables import/export, gradient stop and variant bindings landed 2026-10-11) | real libraries, component playground | 7–14 |
| M4 Vector & text depth | ~80% (live booleans, arc fields, shape builder on strokes, boolean hit testing on the result, one-sided widths, text on a path, pattern fills, brushes and pen pressure landed 2026-10-11; vector edit mode regions 2026-10-13) | spell check, Figma Draw multi-edit | 7–18 |
| M5 Prototype & present | ~85% | media, controllers on macOS/Linux/FreeBSD, separate window | 2–5 |
| M6 Interop & Dev Mode | ~65% (code connect, Dev Mode plugins and best-effort `.fig` import landed 2026-10-11) | `.fig` import's tail, video/HEIC (animated GIFs and TIFF landed 2026-10-11), CMYK intent | 15–30 |
| M6 Interop & Dev Mode | ~65% (code connect, Dev Mode plugins and best-effort `.fig` import landed 2026-10-11; best-effort `.sketch` import 2026-10-21) | `.fig` import's tail, video/HEIC (animated GIFs and TIFF landed 2026-10-11), CMYK intent | 15–30 |
| M7 Release & fidelity (signed builds, look & feel, shortcuts) | builds done; fidelity pass started | tool-by-tool pass | 40–60 |
| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 |
| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme); the standalone `figcraft-server` (persistence with compaction, restarts that lose nothing confirmed, publishing from the app, share links to edit or view, revocable); team libraries kept on the server and enabled by a share link, with usage counts, and swapping a library; `wss://` from the desktop app, server releases and a deployment guide | accounts, teams | 45–90 |
| M10 Localization (new) | not started | layer + 11 languages | 60–100 |
| M11 Plugins | runtime, API, menu, manager and SDK done (#101); plugin panels and settings (ABI v2); widgets (API 3), panels side by side, colour picker on plugin fields; a plugin directory on `figcraft-server` (publish, browse, install, update) | a public catalogue, publisher accounts and signatures, the directory in the browser, richer widget events; Figma plugin compatibility is an owner decision | 8–26 |
| M12 AI features (new, owner decision) | not started | local models | 30–60 |
| M10 Localization | ~39%: the layer, English, Russian and Turkish reviewed by native speakers; Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Indonesian and Czech drafts | native reviews, Hindi, Vietnamese, Arabic (with mirrored panels), engine messages and number formatting ([localization-parity](localization-parity.md)) | 29–51 |
| M11 Plugins | runtime, API, menu, manager and SDK done (#101); plugin panels and settings (ABI v2); widgets (API 3), panels side by side, colour picker on plugin fields; a plugin directory on `figcraft-server` (publish, browse, install, update); widget fields and property menus (API 4) | a public catalogue, publisher accounts and signatures, the directory in the browser, widget hover and timers; Figma plugin compatibility is an owner decision | 6–22 |
| M12 Boards (new) | first slice landed (2026-10-24): board files, sticky notes, shapes with text, connectors that follow, marker, highlighter, eraser, stamps, board toolbar and contextual bar, live sessions; then a shared timer and voting sessions | widgets, templates, tables, mind maps ([boards-parity](boards-parity.md)) | 20–44 |
| M13 AI features (new, owner decision) | not started | local models | 30–60 |
## Next, after the current focus
@@ -73,8 +75,63 @@ blockers.
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | Installed weights of an embedded font family (M7, #1246): release builds embed some families with only a few styles (Poppins Regular, Italic and Bold), and the first font scan dropped every installed face of such a family, so an installed Poppins SemiBold or Medium was never offered (a rescan did offer them). The first scan now keeps the embedded faces, which still win for the styles they cover, and offers the installed styles they lack beside them. Regression test in the font database tests. Numbers unchanged |
| 2026-10-24 | minor | M12: a shared board timer and voting sessions; 30–60 h → 26–54 h |
| 2026-10-24 | minor | M11: plugin widgets take text input (editable fields, `widget.setText`) and a property menu (`widget.menu`), plugin API 4; 8–26 h → 6–22 h |
| 2026-10-24 | minor | Boards (M12): a file kind of its own (`file.newBoard`, saved in `.figcraft`, carried to live-session joiners) with sticky notes, shapes with text, connectors that re-route when what they join moves (an engine recompute, the same for local edits, undo and others' edits), marker, highlighter and eraser, stamps with the placer's name, a board toolbar and a contextual bar; prototyping and Dev Mode refuse on boards; design files and boards have their own marks on tabs and file rows. Milestone M12 Boards added; AI features move to M13 |
| 2026-10-21 | minor | M6: best-effort `.sketch` import (Sketch 48 and newer), see [file-format-parity](file-format-parity.md). Percentages unchanged |
| 2026-10-15 | minor | Auto-save (#305, #296): Preferences ▸ Save files automatically (`pref.autoSave`, off by default; desktop only, the browser has no files to save to) saves each open file that has a path 3 s after its last change, so steady editing doesn't save every few seconds, and never during a gesture, a live text preview or while a version is shown. The file is encoded on a thread of its own and written as of that moment (edits made meanwhile stay unsaved); a failed save says why in a toast and is tried again a minute later, and version history checkpoints keep coming with it on. Numbers unchanged |
| 2026-10-15 | minor | A text box by dragging (#363, #360): a drag with the Text tool makes a fixed-size text box the size of the drag (at least one line tall); a click, or a drag shorter than the drag threshold, still makes auto-width text. Numbers unchanged |
| 2026-10-15 | minor | Batch step references (#400): inside `command.batch` (MCP `batch`), `{"$step": n}` anywhere in a step's params stands for the `id` step n returned (`{"$step": n, "field": …}` for another field; `$step` is a reserved key), so a frame with its children takes one batch and one undo step; a malformed reference fails the batch, which rolls back, and a nested batch keeps references to its own steps. Numbers unchanged |
| 2026-10-15 | minor | Removing someone from a live session (#338): right-click a person's avatar ▸ Remove *name* from session, or `collab.removePerson {client}`, when you host the session or own the file on a server; the session ends for them (they keep their copy), the code or link they hold no longer gets them back in while it runs, and everyone else stays. Numbers unchanged |
| 2026-10-15 | minor | Pages list (#272, #258): a long pages list scrolls in its own area, so the layers below stay in view; page and layer drops count only inside the list's visible rows. Numbers unchanged |
| 2026-10-15 | minor | Variables export in the browser (#368): Export… ▸ Variables JSON / CSS custom properties did nothing in the web build and now arrives as a download; importing there still needs a file picker. Numbers unchanged |
| 2026-10-15 | minor | Font picker keys (#352, #345): Up and Down step through the font families from the current one, keeping the highlight in view and previewing it on the canvas; Enter applies it, Escape leaves the text as it was. Numbers unchanged |
| 2026-10-15 | minor | Leaving Design ends the pen path (#308, #307): switching to Prototype or Inspect while the Pen draws ends the path and goes back to Move (pressing Design again keeps drawing), and a click on the toolbar no longer reaches the canvas under it. Numbers unchanged |
| 2026-10-15 | minor | Vector edit mode keeps the canvas still (#322): the edit mode hints in the Design panel wrap, so entering edit mode and picking a region no longer shift the canvas by about 25 points in the middle of the click (which dragged the region picked). Numbers unchanged |
| 2026-10-15 | minor | Drop shadows under a background blur (#317, #255): a layer with both casts its shadows from what it paints (a translucent fill casts a lighter shadow than an opaque one) and reads the backdrop only after drawing them, so a shadow shown behind transparent areas is seen through the blur. Numbers unchanged |
| 2026-10-15 | minor | When the window can't open (#306, #261): Windows release builds have no console, so a graphics failure at start quit without a word; the error is now logged and shown in a message box in the interface language, with what to try (unset `WGPU_BACKEND`, update the driver, OpenGL) and where to report it, skipped with `--control`. Numbers unchanged |
| 2026-10-15 | minor | Crashes on Windows (#271, #314, #337; #224 and two more): a text field focused before it was drawn aborted the screen-reader layer; double-clicking a page, creating a variable collection and renaming a style folder no longer close the app. Numbers unchanged |
| 2026-10-15 | minor | Deeply nested layers (#330, #210): one depth guard (256) on the three recursive layout paths, so a deep file can't run the layout out of stack. Numbers unchanged |
| 2026-10-15 | minor | Image memory budgets (#326; #203, #189): decoded images and a file's images have budgets; the decode cache evicts its least recently used entries. Numbers unchanged |
| 2026-10-15 | minor | macOS file dialogs (#253, #250) no longer re-enter the event loop; a dialog's result goes to the file and window it was opened for, and dialogs are parented to the main window. Numbers unchanged |
| 2026-10-15 | minor | Recovery copies (#297, #303, #331, #336; #202, #185, #187): a copy counts only after its write succeeds and a failed one is retried; saves and closes wait for the write in flight; copies keep version history and branches; a recovered copy stays until this run has its own. Numbers unchanged |
| 2026-10-15 | minor | Text edits (#280, #278) no longer roll back earlier changes; a drag released off the canvas ends at the pointer's last canvas position. Numbers unchanged |
| 2026-10-15 | minor | Rollback (#282, #301; #196): a failed command rolls back during a drag or other gesture too; a failed batch keeps the redo steps. Numbers unchanged |
| 2026-10-15 | minor | Atomic save and export (#284): staging files are uniquely named and created exclusively, so a file or link already at the old fixed staging name is never written through, and two writers of one file no longer share one. Numbers unchanged |
| 2026-10-15 | minor | Alt-drag copies (#283) snap to their original, and Duplicate (Cmd+D) then repeats the last copy's offset. Numbers unchanged |
| 2026-10-15 | minor | The pen (#277) shows its Bezier handles while a point is dragged out. Numbers unchanged |
| 2026-10-15 | minor | Corner handles (#293, #292): a press on a corner handle resizes instead of picking the corner-radius handle. Numbers unchanged |
| 2026-10-15 | minor | New text (#372, #362) takes a readable colour from the frame it is drawn in, not only the page (text drawn in a white frame on a dark page was white). Numbers unchanged |
| 2026-10-15 | minor | Outline stroke (#386) works on every selected layer. Numbers unchanged |
| 2026-10-15 | minor | Send to back (#444) counts per parent when the selection spans frames. Numbers unchanged |
| 2026-10-15 | minor | Align buttons (#276) are off when the selection has nothing to align to. Numbers unchanged |
| 2026-10-15 | minor | Gradient type icons (#275) show their gradient. Numbers unchanged |
| 2026-10-15 | minor | Corner resize cursors (#388) stay diagonal at any proportions; the text selection highlight (#390) stays inside the box on the last line. Numbers unchanged |
| 2026-10-15 | minor | New pages (#311, #295) take the current page's background; the theme still decides a new file's page. Numbers unchanged |
| 2026-10-15 | minor | A slice's export preview (#265) shows the content under it. Numbers unchanged |
| 2026-10-15 | minor | SVG export (#327, #325) keeps a nested frame's fill and stroke inside its parent's clip. Numbers unchanged |
| 2026-10-15 | minor | Home screen (#318, #324): recent files notice files deleted while it is open; a long path no longer draws over the file name. Numbers unchanged |
| 2026-10-15 | minor | System theme on first run (#262): the first frame follows the system theme, and going back to System from a manual theme works. Numbers unchanged |
| 2026-10-15 | minor | Web clipboard (#375): copy and paste in the browser go through the system clipboard, so a paste after a copy brings the copied layers instead of whatever text the clipboard held. Numbers unchanged |
| 2026-10-15 | minor | Zoom previews (#312): when renders from scratch are slow, a new zoom first shows the whole view coarsely instead of the page colour, then the refined image; images of another file or page never show. Numbers unchanged |
| 2026-10-15 | minor | Hidden windows (#313, #332; #269): a minimised or covered window answers control and MCP requests (commands, inspection, `ui.render`) at once, each window on its own; screenshots and input still wait for it to be on screen. Numbers unchanged |
| 2026-10-15 | minor | Server (#340, #302): a compaction cut short closes the file instead of confirming lost edits (#183); each team library version is kept in its own file (#206). Numbers unchanged |
| 2026-10-15 | minor | Counters (#343, #344, #353; #309): collab sequence numbers, layer name counters and opacity digits can't overflow. Numbers unchanged |
| 2026-10-15 | minor | Commands refuse bad indexes and ids (#475, #384, #310): `page.select` and `doc.switch`, `effect.set` and `export.update` / `export.remove`, `page.move` with a non-page id. Numbers unchanged |
| 2026-10-15 | minor | Find and replace (#358, #361) changes every occurrence, case-insensitively, also when the replacement contains the search text. Numbers unchanged |
| 2026-10-15 | minor | The gradient shorthand (#254, #252) is read by the engine, so the CLI, control channel and MCP share it. Numbers unchanged |
| 2026-10-15 | minor | .fig import (#279, #281): resized instances lay out their layers by their constraints (#266); explicit variable modes whose collection is named by its asset key, as library collections are, come across (#267). Numbers unchanged |
| 2026-10-15 | minor | Colour emoji (#300, #268) draw from their own colour tables (sbix and CBDT/EBDT bitmap strikes, COLR v0 layers), a colour face tried first; COLR v1 still draws outlines. Numbers unchanged |
| 2026-10-15 | minor | macOS (#290, #289): `.figcraft` files open from Finder, Open With and the Dock. Numbers unchanged |
| 2026-10-15 | minor | Windows (#294, #291): the first preferred language alone picks the UI language. Numbers unchanged |
| 2026-10-15 | minor | Unix font discovery (#474) follows `XDG_DATA_HOME`. Numbers unchanged |
| 2026-10-15 | minor | Linux packaging (#346, #328): the window id matches the desktop entry (FreeBSD keeps its class); the RPM requires glibc 2.35. Numbers unchanged |
| 2026-10-15 | minor | Tests (#472, #471, #339): the rename shortcut test, the hanging punctuation test and the font-scanning tests no longer depend on the platform, font or test order. Numbers unchanged |
| 2026-10-15 | minor | Languages (#257, #263, #469, #477, #288): Russian (with one/few/many plural forms) and Turkish catalogs, reviewed by native speakers; Czech and Indonesian drafts; the Japanese draft completed. 2,177 entries now (1,587 UI strings, 559 command labels, 31 menu wordings); strings added since a catalog was written read English there. Localization 39% and 29–51 h unchanged at this rounding (Indonesian is now a draft, while the five older drafts lack 88 newer entries) |
| 2026-10-17 | minor | Community batch #753 (10 PRs): Selection colors for mixed selections (#403); smart-selection spacing fields and canvas handles (#402); a Present tab with device and scale controls (#537); adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys to frames only and warns about font styles a family lacks (#603); canvas images on whole device pixels (#359); Adwaita window controls on Wayland without server decorations (#592, not run on GNOME or KDE by the maintainers); the keyboard shortcuts panel closes on the home screen (#548); macOS paste modifiers in tests (#750); credits column tests (#347). Numbers unchanged. |
| 2026-10-17 | minor | Community fixes from #524 up (batches #727, #728, #733, #735, #736 and #729; 55 PRs) and a crash guard. `.fig` import: explicit instance identity, overrides by key, text style precedence, grid track sizes, slot content, connector labels, skip ink, paragraph options and hanging lists, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments and embedded images; large files open within budgets from the file's own size; an import report after opening; imported bytes stay out of the command journal. Files and paste: save a copy as `.fig`; Cmd/Ctrl+V pastes images copied in other apps and layers copied from another design app (`edit.pasteDesignLayers`); paste over and paste properties; clipboard reads on Linux and the BSDs time out after 2 s. Editor: live font preview on hover; Shift+I and Resources open Assets; Shift-drag moves the selection; thin strokes take a nearby double-click; dropped points merge; Flatten keeps open paths; the Actions palette gets tabs, ranked search, a keyboard highlight and recents; the colour picker opens on a new fill or stroke; text strokes follow their alignment; selection highlights after line breaks; a draggable pages/layers divider; variant properties reorder by drag; submenus keep their hover with a safe triangle; menu actions that need a value ask for it; File ▸ Rename edits the name in place. Engine: bad paint indexes, non-finite geometry and pages in copy/cut are refused; a param naming a layer says so when it isn't one; a CLI script is one undo step and resolves `$step`. Server and collaboration: message limits follow `max_file_mb` (client capped at 1 GiB); a damaged generation field no longer drops flushed edits; the web build joins a session from a `#join=` link. Also Swedish command labels, Nix CI through the shared workflow, paint bounds once per render, and a guard that turns a panic inside the boolean-op library into bad input (the stroke draws unclipped). Numbers unchanged. |
| 2026-10-16 | minor | Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged |
| 2026-10-16 | minor | Community fixes from #478 up (batches #591, #598, #602; 69 PRs) and a control-channel fix: a line that isn't JSON now ends a `--control` connection, so a web page can't drive the app through a browser request (#589). Highlights: page copies keep navigation, flows, prototype settings and guides; Find and Replace keeps line endings and case-insensitive matches; text hyperlinks survive import and export; recent colours record only what was picked; MCP screenshots respect `maxSize`; Swedish catalog (UI strings), Czech plurals; bounded canvas overscan while panning; Korean fallback at the run's weight; many vector-edit, guide, page and .fig round-trip fixes. Numbers unchanged |
| 2026-10-14 | minor | M7: the Small caps button in Type settings showed a box where the UI font has no small-capital `ɢ` (builds without Inter use egui's fonts); its sample is now a capital and a smaller capital, drawn from ordinary letters every UI font has. Numbers unchanged |
| 2026-10-14 | minor | M7: Home screen recent files as cards: each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. |
+2 -2
View File
@@ -336,8 +336,8 @@ the address it came from and when it was last opened (for expiry).
beside it, flushed, then renamed over the old one. The log is only ever appended to, and synced
before the server tells anyone about the change.
- **Compaction** writes the file's state as the next snapshot and starts an empty log, after
`compact_after` entries or `compact_mb` of log, when the last person leaves, and when the server
stops. A log left over from a compaction cut short is replayed over the snapshot, which already
`compact_after` entries or `compact_mb` of log, when the last person leaves a file that was
edited (the ids of people who only joined stay in the log), and when the server stops. A log left over from a compaction cut short is replayed over the snapshot, which already
holds it all, so nothing changes. If a damaged generation field made a current log look like such
a leftover, its edits the snapshot lacks are recovered the same way and the damage is reported.
- **Damage is skipped and reported, never deleted.** At startup a folder whose description, links
+34 -21
View File
@@ -1,6 +1,6 @@
# FigCraft parity with Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (rows for the community batches: Selection colors, smart-selection spacing and a Present tab (#753), `.fig` import fidelity, paste from another design app, a `.fig` copy from Save as; control requests while the window is minimised (#720); catalog counts re-measured; automation roots (#715); number variables in the text content picker (#629); frames and sections convert into each other (#719); reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; `.sketch` import, best effort: import/export breadth 86% → 87%, file formats 60% → 61%, headline numbers unchanged; Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds; boards: area 22 (weight 0, outside the weighted sum; CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, iteration marks 〱–〵 and the halfwidth parentheses; Turkish reviewed, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7; rows for the community batches: Selection colors, smart-selection spacing and a Present tab (#753), `.fig` import fidelity, paste from another design app, a `.fig` copy from Save as; control requests while the window is minimised (#720); catalog counts re-measured; automation roots (#715); number variables in the text content picker (#629); frames and sections convert into each other (#719); reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, vLine truncated
The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md)
summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is
@@ -9,7 +9,7 @@ summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu c
[`hardware-parity.md`](hardware-parity.md), [`localization-parity.md`](localization-parity.md),
[`auto-layout-parity.md`](auto-layout-parity.md), [`components-parity.md`](components-parity.md),
[`prototyping-parity.md`](prototyping-parity.md), [`dev-mode-parity.md`](dev-mode-parity.md),
[`collaboration-parity.md`](collaboration-parity.md).
[`collaboration-parity.md`](collaboration-parity.md), [`boards-parity.md`](boards-parity.md).
## Headline
@@ -21,7 +21,7 @@ summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu c
| Essentials user | **~59%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.94 × 0.88) |
| Mainstream practitioner | **~50%** | estimated ([below](#mainstream-practitioner): weekly areas 72.7% × discounts 0.9 × 0.9 × 0.85) |
| Remaining to beta | **~155–280 h** one Opus 5.5 agent (~32–64 h wall-clock with 5 parallel agents) | estimated |
| Remaining to full parity | **~262–519 h** one agent (~83–145 h wall-clock with 5 parallel agents) | estimated |
| Remaining to full parity | **~260–515 h** one agent (~83–145 h wall-clock with 5 parallel agents) | estimated |
| Stage | **alpha**: the alpha gate passes ([roadmap](roadmap.md#alpha-gate)); multiplayer and checking the best-effort `.fig` import on real files are beta gaps (see [ROADMAP](../ROADMAP.md#stage)) | |
The previous figures (ROADMAP of 2026-10-10, "feature depth ~79%", "full parity 115–195 h") were
@@ -35,7 +35,7 @@ tool; presence was being graded as behaviour in places.
| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates |
|---|---|---|---|
| Full target (ready for real work) | ~53% | ~268–522 h | multiplayer (42–84), localization (29–51), plugins (8–26), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) |
| Full target (ready for real work) | ~53% | ~266–518 h | multiplayer (42–84), localization (29–51), plugins (6–22), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) |
| Mainstream practitioner | ~50% | ~120–229 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), checking the experimental `.fig` export opens elsewhere (5–15) |
| Essentials user | ~59% | ~55–100 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20) |
@@ -84,8 +84,10 @@ Weights are the share of a typical product designer's working time in Figma Desi
so they can be argued with. Areas 1–16 keep the original rubric's weights (they sum to 94, not the 100 the rubric claimed);
areas 17–21 are added (sum 19) because Figma users depend on them. Percentages below divide by the
total, 113.
Out of scope as separate products: FigJam, Figma Slides, Sites, Make, Buzz, Weave (the `.jam` and
`.deck` document kinds belong to them).
Out of scope as separate products: Figma Slides, Sites, Make, Buzz, Weave (the `.deck` document
kind belongs to them). Whiteboarding is covered by FigCraft's boards (area 22, weight 0: a separate
workflow, tracked here but not in the weighted sum; [`boards-parity.md`](boards-parity.md)); the
`.jam` document kind is not read or written.
## By feature area
@@ -94,27 +96,28 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
| # | Area | Weight | Breadth | Ready | Remaining | Hours |
|---|---|---|---|---|---|---|
| 1 | Canvas & navigation | 6 | 98% | 74% | Outline mode's Include hidden layers and Include object bounds, Display P3 on screen, QA at extreme zooms (behaviour suite for navigation, rulers, guides, layout grids and outline mode landed: guide selection, Auto grid counts, outline-mode clicks) | 1–4 |
| 1 | Canvas & navigation | 6 | 98% | 74% | Display P3 on screen, QA at extreme zooms (behaviour suite for navigation, rulers, guides, layout grids and outline mode landed: guide selection, Auto grid counts, outline-mode clicks; outline mode's Include hidden layers and Include object bounds landed) | 1–4 |
| 2 | Selection & transform | 8 | 97% | 72% | Handles, measuring and snapping checked tool by tool against Figma and real files (snap distance, handle reach, cursors); children turned off a right angle scale without skew, approximately (behaviour suite from the documentation, rotated groups and mixed selections, equal spacing snapping, Cmd/Ctrl to ignore constraints and marquees inside frames landed) | 3–5 |
| 3 | Layers panel | 5 | 99% | 85% | QA with long real files (behaviour suite from the documentation, Rename layers dialog, sweeping eyes and padlocks, page dragging and dividers, several layers dropped together, keyboard box selection, F2 rename, Shift+F10 context menu and 10k-layer timings landed) | 1–2 |
| 4 | Shapes & vector | 8 | 99% | 73% | Corner smoothing on polygons, stars and vectors; the transform box around points of several layers; per-point caps and joins on dashed, variable-width and brush strokes; vector QA with real files (behaviour suites from the documentation, corner radius and star ratio handles, rounded vector points drawn, also beside curved segments, a cap and join per point, several handles dragged together, cut and paint bucket drags, delete and heal along the curve, a boolean group's look by operation and the miter angle landed) | 1–3 |
| 4 | Shapes & vector | 8 | 99% | 73% | Corner smoothing on vectors, corner radius and smoothing on boolean groups that contain rectangles; per-point caps and joins on dashed, variable-width and brush strokes; vector QA with real files (behaviour suites from the documentation, corner radius and star ratio handles, corner smoothing on polygons and stars, rounded vector points drawn, also beside curved segments, a cap and join per point, several handles dragged together, cut and paint bucket drags, delete and heal along the curve, a boolean group's look by operation and the miter angle landed) | 1–3 |
| 5 | Paint | 7 | 90% | 71% | Video fills, wide-gamut canvas, HEIC images; plus darker in PDF (behaviour suite from the documentation, pass-through and isolating groups, clipping frames that don't isolate, plus darker, animated GIF fills, TIFF images and embedded image colour profiles landed) | 2–5 |
| 6 | Strokes | 3 | 95% | 75% | Stroke QA with real files (colour styles on strokes, stroke weight and per-side weight variables, brush strokes landed, see 20) | 1–2 |
| 7 | Effects | 4 | 100% | 75% | Glass and progressive blur tuned side by side (behaviour suite from the documentation, inner shadows and background blur on groups, shadows hidden behind translucent layers on the canvas and in every export landed) | 2–5 |
| 8 | Frames, groups, sections, masks | 5 | 97% | 80% | Visual QA with real files (a one-step frame/group convert, and a behaviour suite from the documentation landed: masked-out areas don't take clicks, mask group bounds and exports follow the mask, sections kept out of frames and groups, frames and sections drawn around layers take them, section resizing leaves contents in place, X/Y measured in the frame) | 1–3 |
| 9 | Auto layout & constraints | 9 | 98% | 74% | Behaviour against real Figma files (behaviour suite from the documentation, `fr` tracks like CSS, vertical wrap, auto gaps evenly/around, cell alignment and wrapped-line alignment landed) ([auto-layout-parity](auto-layout-parity.md)) | 3–6 |
| 10 | Text | 8 | 90% | 70% | Spell check, text metrics on real files and in other fonts (text box sizes measured against documents re-saved by other software: all 230 self-sizing boxes in 7 documents come out to the pixel, one font family; behaviour suite with CJK line breaking, vertical trim and carets by grapheme cluster landed; lists per paragraph with nesting and list spacing, hanging punctuation and lists, decoration style, thickness, offset, skip ink and colour landed) | 3–7 |
| 11 | Components & variants | 9 | 96% | 72% | Real (not synthetic) libraries, `.fig` libraries (a component playground for variant, boolean and text properties, a behaviour suite from the documentation, overrides of padding, gap, grids, exports and interactions, refused geometry overrides, variant-switch and Assets-swap override rules, top-left default variant landed) ([components-parity](components-parity.md)) | 4–9 |
| 11 | Components & variants | 9 | 96% | 72% | Real (not synthetic) libraries, `.fig` libraries (a component playground for variant, boolean, text and instance-swap properties, a behaviour suite from the documentation, overrides of padding, gap, grids, exports and interactions, refused geometry overrides, variant-switch and Assets-swap override rules, top-left default variant landed) ([components-parity](components-parity.md)) | 4–9 |
| 12 | Styles & variables | 5 | 95% | 76% | Real-library QA (variables inside styles, behaviour suite from the documentation, style folders and order, mode order, code syntax, extended collections with their tail (new library variables as updates, chains through libraries) and variables import/export landed) ([components-parity](components-parity.md)) | 2–4 |
| 13 | Prototyping | 6 | 92% | 74% | Video triggers and media actions, controllers on macOS, Linux and FreeBSD, real prototype QA (behaviour suite from the documentation, delays on mouse triggers, manual overlay offsets, key combinations, controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, interactive components playing their variants, drag-driven animations, physical springs landed) ([prototyping-parity](prototyping-parity.md)) | 2–5 |
| 14 | Import/export | 6 | 86% | 58% | `.fig` import's tail (best-effort import, editable vector networks, stand-ins for non-design layers and variable prototype actions landed, then (2026-10-17) grid tracks, slots, labels of connectors saved with their path, per-point joins, text on a path, image adjustments, budgets for large files, an import report and paste from another design app; all on synthetic files, no real-file checks yet), checking the rest of the experimental `.fig` export (landed; written files open and draw in other software) on effects, images, prototypes and real files, HEIC/video import, CMYK output intent ([file-format-parity](file-format-parity.md)) | 5–10 |
| 15 | Dev Mode / inspect | 2 | 96% | 72% | Import lines for connected instances inside generated layer trees; an editor extension is left to the MCP server (inspect behaviour suite with colour formats, rem and icon and image assets, code connect, plugins in Dev Mode and dev resources links landed) ([dev-mode-parity](dev-mode-parity.md)) | 0–1 |
| 14 | Import/export | 6 | 87% | 58% | `.fig` import's tail (best-effort import, editable vector networks, stand-ins for non-design layers and variable prototype actions landed, then (2026-10-17) grid tracks, slots, labels of connectors saved with their path, per-point joins, text on a path, image adjustments, budgets for large files, an import report and paste from another design app; all on synthetic files, no real-file checks yet), checking the rest of the experimental `.fig` export (landed; written files open and draw in other software) on effects, images, prototypes and real files, HEIC/video import, CMYK output intent ([file-format-parity](file-format-parity.md)) | 5–10 |
| 15 | Dev Mode / inspect | 2 | 98% | 78% | Publishing mappings from the code repository; an editor extension is left to the MCP server (inspect behaviour suite with colour formats, rem and icon and image assets, code connect, plugins in Dev Mode, dev resources links, import lines, compare with versions, an inspect-only canvas and layers panel and measurements kept on the canvas landed) ([dev-mode-parity](dev-mode-parity.md)) | 1–2 |
| 16 | Comments, versions, find, misc | 3 | 87% | 63% | Comments are local only; comparing two versions; branches in live sessions; text runs and other lists besides fills, strokes and effects merge whole (branches with review and merge, their own version history and per-item list merges, and a comments and version history behaviour suite from the documentation landed) | 1–3 |
| 17 | Multiplayer & sharing | 8 | 17% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow, cursor chat and spotlight, per-person undo, offline resume; a self-hosted `figcraft-server` with persistence and share links to edit or view, which can be open to the public (owner tokens, limits per address, expiry, moderation). Left: accounts and teams, audio ([collaboration-parity](collaboration-parity.md)) | 42–84 |
| 18 | Plugins & widgets | 4 | 60% | 35% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK), with log byte/count limits checked before allocation and host forwarding ([logging limits](plugins.md#logging-limits)), plugin panels called back per button or changed value and per-plugin settings (ABI v2), then widgets (API 3: plugin-drawn layers on the canvas with state kept in the document, redrawn on clicks; a Counter example), panels side by side and a colour picker for plugin colour fields, then a plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version); layer data written with `node.set {props: {pluginData}}` reads back through `node.get` (#879). Left: a public catalogue, publisher accounts and signatures, ratings, background update checks, the directory in the browser; widgets take clicks only (no text input, property menus, hover or timers) and draw frames, text, shapes and images only; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 8–26 |
| 18 | Plugins & widgets | 4 | 65% | 38% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK), with log byte/count limits checked before allocation and host forwarding ([logging limits](plugins.md#logging-limits)), plugin panels called back per button or changed value and per-plugin settings (ABI v2), then widgets (API 3: plugin-drawn layers on the canvas with state kept in the document, redrawn on clicks; a Counter example), panels side by side and a colour picker for plugin colour fields, then a plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version); layer data written with `node.set {props: {pluginData}}` reads back through `node.get` (#879), then widget fields and property menus (API 4: a text layer edited on the canvas like any text or with `widget.setText`, its value kept in the widget's state; actions, toggles and colour choices in a bar over the selected widget, `widget.menu`). Left: a public catalogue, publisher accounts and signatures, ratings, background update checks, the directory in the browser; widgets have no hover or timers and draw frames, text, shapes and images only; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 8–26 |
| 19 | Team libraries & organization | 2 | 70% | 40% | Libraries kept on `figcraft-server` under their file and enabled by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names. Left: team-wide library lists, accounts and permissions, analytics over time, pushed update notices | 4–8 |
| 20 | Figma Draw extras | 2 | 88% | 60% | Have: variable width, lasso, shape builder, noise/texture, text on a path, pattern fills, stretch and scatter brushes, pen pressure (Windows pens, browser styluses), multi-edit (several vector layers in edit mode at once). Missing: pressure from macOS/Linux tablets | 1–3 |
| 21 | AI features | 3 | 0% | 0% | First draft, rename layers, image edit/remove background, visual search, Make: needs local models (owner decision) | 30–60 |
| 22 | Boards (whiteboarding) | 0 | ~47% | ~29% | Board files with a dot grid, sticky notes, shapes with text, connectors that follow what they join (straight, elbow around the objects, or curved; ends dragged to re-attach), marker, highlighter and eraser, stamps, text and sections, five built-in templates and tables, a shared timer and voting sessions, in live sessions (M12); missing: timer music, board widgets, mind maps, washi tape, audio, quick-create handles, connector waypoints, stamps sticking to objects, `.jam` files ([boards-parity](boards-parity.md)). Not in the weighted sum | 20–44 |
| | **Weighted (÷113)** | 113 | **~85%** | **~63%** | | **117–255** |
| | Core editor only (1–16, ÷94) | 94 | ~94% | ~72% | | 32–74 |
@@ -124,16 +127,16 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
|---|---|---|---|---|---|
| Features (depth) | 35 | 63% | 28–67 (areas 1–13, 15, 16, 20) | The table above; core editor ~72% | this file |
| UI/UX fidelity | 15 | 58% | 40–60 | Look & feel compared side by side with UI3 (~65% by the builders); tool-by-tool interaction (modifiers while dragging, hover states, popovers) not yet compared (a cursor for every tool and modifier, number fields and panel widths built from the documentation, not compared); accessibility names and keyboard navigation landed | [ui-parity](ui-parity.md) |
| File formats | 12 | 60% | 5–10 | `.fig` read, best effort, with editable vector networks and (2026-10-17) more fidelity, budgets for large files and an import report (tested on generated fixtures, not real files); layers copied from another design app paste the same way (tested only with clipboard HTML made from our own export), `.fig` write experimental (round trips through our own importer; written files open and draw in other software); REST JSON import, SVG in/out, PNG/JPG/WebP/PDF export solid, with a behaviour suite from the documentation | [file-format-parity](file-format-parity.md) |
| File formats | 12 | 61% | 5–10 | `.sketch` read (Sketch 48 and newer, best effort; 2026-10-21): live symbols, shared styles, Sketch 2025 frames and stacks, checked on three real files against Sketch's own renders (0.8–2% of pixels differ on average); `.fig` read, best effort, with editable vector networks and (2026-10-17) more fidelity, budgets for large files and an import report (tested on generated fixtures, not real files); layers copied from another design app paste the same way (tested only with clipboard HTML made from our own export), `.fig` write experimental (round trips through our own importer; written files open and draw in other software); REST JSON import, SVG in/out, PNG/JPG/WebP/PDF export solid, with a behaviour suite from the documentation | [file-format-parity](file-format-parity.md) |
| Collaboration | 10 | 13% | 52–104 | Local comments, version history and branches with review and merge; the multiplayer core (sync model, server ordering, client reconciliation, convergence tests) landed in #103, and live sessions (preview) co-edit a file hosted by a desktop app or kept on a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, with cursor chat and spotlight (`docs/collab-protocol.md`) | [collaboration-parity](collaboration-parity.md) |
| Stability | 8 | 55% | 20–40 | Never-crash lints, command guard with rollback, recovery copies, 963 tests incl. junk-parameter sweeps and a seeded retained-render walk; but two days old, one release (v0.2.0), no field reports, QA mostly on macOS | [gaps](gaps.md) |
| Performance | 6 | 60% | 30–60 | `cargo xtask bench`: 50k-layer page ~8 ms zoomed out, 10k-layer edits ~0.2 ms, 2,000-instance design system edits 4–64 ms; CPU (tiny-skia) canvas where Figma renders on the GPU; nothing timed against Figma on the same file | [hardware-parity](hardware-parity.md) |
| Hardware | 3 | 60% | 10–21 | Trackpad pinch and HiDPI yes; pen pressure on Windows and from browser styluses (none from macOS/Linux tablets); several windows (screen-reader tree in the main one only); no GPU canvas, no P3 on screen | [hardware-parity](hardware-parity.md) |
| Localization | 3 | 39% | 29–51 | The layer, English, Russian reviewed by VanTan, and machine drafts of Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Indonesian and Czech (none reviewed by a native speaker; measured 2026-10-17, the drafts cover 95–99% of the 2,272 current strings), Swedish and Turkish catalogs, CJK UI text in installed faces loaded on demand (web: only the craft-fonts CJK build); engine messages stay English | [localization-parity](localization-parity.md) |
| Platforms | 3 | 75% | 10–20 | macOS, Windows (x64/x86/arm64), Linux, FreeBSD, web all packaged (v0.2.0); Figma also has iOS/Android apps; non-macOS QA thin | [hardware-parity](hardware-parity.md) |
| Ecosystem/plugins | 3 | 38% | 8–26 | Own WASM plugins with a Rust SDK and three examples (#101), plugin panels and settings (ABI v2), widgets that take clicks (API 3), a self-hosted plugin directory on `figcraft-server`; no Figma plugin compatibility, no public catalogue or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) |
| Ecosystem/plugins | 3 | 40% | 6–22 | Own WASM plugins with a Rust SDK and three examples (#101), plugin panels and settings (ABI v2), widgets that take clicks (API 3), text input and a property menu (API 4), a self-hosted plugin directory on `figcraft-server`; no Figma plugin compatibility, no public catalogue or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) |
| AI features | 2 | 0% | 30–60 | None | [gaps](gaps.md) |
| **Ready for real work** | 100 | **~53%** | **~262–519** | | |
| **Ready for real work** | 100 | **~53%** | **~260–515** | | |
Dimension hours add up to the full-parity total without double counting: the feature-area rows
for import/export (14), multiplayer and team libraries (17, 19), plugins (18) and AI (21) are
@@ -289,8 +292,9 @@ Hours in this narrative are the builders' estimates; the tables above supersede
Auto / % / px line heights with the leading split above and below every line, letter spacing in
% and px, paragraph spacing and indent (start-aligned text only), resizing and truncation,
decorations from the font's metrics; CJK line breaking (between ideographs, kana and Hangul, no
break inside Latin words, kinsoku for closing and opening punctuation, small kana and the
prolonged sound mark); a style's metrics from its own font, not a fallback; carets, Delete and
break inside Latin words, kinsoku to JIS X 4051 for closing and opening punctuation, small kana
(halfwidth too), the prolonged sound mark, the iteration marks, the (semi-)voiced sound marks
and the halfwidth parentheses); a style's metrics from its own font, not a fallback; carets, Delete and
Backspace by grapheme cluster; double-click and word moves by script run; IME composition with
CJK; vertical trim (cap height to baseline). The tail suite (`crates/text/src/tail_tests.rs`,
`crates/engine/src/text_tail_tests.rs`): lists per paragraph, nested up to 8 levels with Tab and
@@ -420,7 +424,7 @@ Hours in this narrative are the builders' estimates; the tables above supersede
presenter) and variables (the local variables JSON passed along, next to the file or embedded:
collections, modes, aliases, scopes, layer bindings and explicit modes).
Export panel: a row per export setting (typed or preset sizes like `2x` / `512w` / `512h`, PNG,
JPG, WebP, SVG and PDF, suffix, advanced options: contents only, include bounding box, and for
JPG, WebP, SVG and PDF, suffix, advanced options: ignore overlapping layers, include bounding box for text layers, and for
SVG outline text, id attributes and simplified strokes), a preview, and "Export N layers" (`file.exportLayers`) in the
panel and the Export dialog. Paste from other apps: images, SVG and text (the newest clipboard
content wins over our own copy; pasted SVG lands centred in the view), and layers copied from
@@ -513,11 +517,12 @@ Hours in this narrative are the builders' estimates; the tables above supersede
a plugin describes a layer of its own, validated and built as a frame with locked layers; clicks
on its named parts call the plugin back in a fresh sandbox with the state kept in the layer, one
undo step each; `widget.insert` / `widget.click` / `widget.list`; a Counter example), panels of
several plugins side by side and the colour picker on plugin colour fields. Then a plugin
several plugins side by side and the colour picker on plugin colour fields; then widget fields
and property menus (API 4: `widget.setText`, `widget.menu`). Then a plugin
directory on `figcraft-server`: bundles published with the server key and checked in full,
browsed and installed from Plugins ▸ Manage plugins after a permissions confirmation, downloads
checked against their SHA-256, updates by version. *Left:* a public catalogue, publisher
accounts and signatures; richer widget events; the directory in the browser. 8–26 h.
accounts and signatures; widget hover and timers; the directory in the browser. 6–22 h.
9. **Hardening and performance**: *landed:* `cargo xtask bench` (synthetic 1k/10k/50k-layer
pages: engine, render and headless UI-frame timings). The canvas keeps its last image and
re-renders only what a pan uncovers or an edit touches: moving one layer at 10k layers re-renders
@@ -616,7 +621,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
toolbar with flyouts and mode switcher, Design / Prototype / Inspect panels (Dev Mode: properties with their styles and variables, annotations
with pinned properties and categories drawn as coloured callouts on the canvas (filtered by category),
a ready-for-dev list, a focus view on one frame with previous / next, Compare changes since a frame
was marked ready (before / after side by side or overlaid, property-level diffs), CSS/HTML/React/Vue/SwiftUI/Compose
was marked ready (before / after side by side or overlaid, property-level diffs), CSS/HTML/React/Vue/SwiftUI/Compose/ArkUI
code where components take their properties as parameters and instances are calls, components
connected to their code components showing the real snippet, read-only plugins), colour picker
(HSB square, hue/alpha, hex, gradient stops, image fills), main menu generated from the catalog,
@@ -638,7 +643,15 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | patch | Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds, which landed (`view.outlinesHiddenLayers`, `view.outlinesObjectBounds`, `crates/engine/src/outline_options_tests.rs`). Numbers unchanged |
| 2026-10-24 | minor | Boards (area 22, weight 0): a shared timer and voting sessions (M12); ~35%/~20% → ~40%/~24%, 30–60 h → 26–54 h; weighted numbers unchanged |
| 2026-10-24 | minor | Boards (M12): templates (five built in, `board.template`) and tables (`board.table`, `board.tableEdit`); area 22 ~35%/~20% → ~40%/~24%, 30–60 h → 26–54 h (outside the weighted sum) |
| 2026-10-24 | minor | Plugin widgets take text input and a property menu (gap 6, plugin API 4; API 1–3 plugins and existing widgets unchanged): a widget's text layer can be an editable field, edited on the canvas with a double-click like any text (or `widget.setText`), its value cleaned and capped at 1,000 characters, written into the widget's state under the field's name and the field's text, and the plugin called back to redraw, all as one undo step that syncs in live sessions; a widget can carry a property menu of up to 8 actions, toggles and colour choices (up to 8 colours each), shown in a small bar over the widget while it is selected and driven with `widget.menu`; descriptions, values and menus read back from files are checked, an API 3 plugin that draws a field or a menu is refused, and older FigCraft builds refuse API 4 manifests. The Counter example gained a label field and a menu (reset, steps of ten, a colour); the SDK reads the new events. Hover and timers are still missing. Plugins & widgets 60%/35% → 65%/38%, 8–26 h → 6–22 h; Ecosystem dimension 38% → 40%; weighted totals unchanged at this rounding; full parity ~262–519 h → ~260–515 h |
| 2026-10-24 | minor | Boards (M12): a file kind of its own (`file.newBoard`, saved in `.figcraft`, carried to live-session joiners) with sticky notes, shapes with text, connectors that re-route when what they join moves (an engine recompute, the same for local edits, undo and others' edits), marker, highlighter and eraser, stamps with the placer's name, a board toolbar and a contextual bar; prototyping and Dev Mode refuse on boards; design files and boards have their own marks on tabs and file rows. Area 22 added at weight 0 (breadth ~35%, ready ~20%); headline numbers unchanged (the weighted sum leaves area 22 out) |
| 2026-10-15 | minor | Localization (#263, #469, #477, #288): Turkish reviewed by a native speaker, Czech and Indonesian drafts, the Japanese draft caught up; coverage re-measured (2,177 entries; the five older drafts at 95%). Localization 39% and 29–51 h unchanged at this rounding (see [localization-parity](localization-parity.md)) |
| 2026-10-22 | minor | Vector edit mode: the transform box around points selected in several layers (in the page's axes; scale, rotate and drag in one undo step). Shapes & vector's remaining work loses that item; numbers unchanged |
| 2026-10-20 | minor | Layer data reads back (#879): `node.get` (the command, the control method and MCP `get_node`) and `document.inspect` report a layer's or page's `pluginData` as `{key: value}` when it has any, so a tool that keeps state in a file with `node.set {props: {pluginData}}` can read it on its next run (`plugin_data_tests`). Numbers unchanged |
| 2026-10-21 | minor | `.sketch` import (best effort, Sketch 48 and newer), a feature Figma has (its file browser imports Sketch files, desktop app included, per the help center) that the area table hadn't listed: pages, layers, styles, live symbols with overrides, shared styles and Color Variables, prototype links, Sketch 2025 frames, stacks as auto layout and tints, text placed as Sketch places it; checked on three real files (Sketch 50, 64 and 2026) against Sketch's own renders. Import/export breadth 86% → 87% (ready stays 58%: best effort, three files), file formats 60% → 61%; feature breadth (~85%), ready for real work (~53%) and the audience numbers unchanged after rounding |
| 2026-10-17 | minor | Menu breadth re-run with `cargo xtask parity`: 212 / 212 (100%; the catalog gained View ▸ Full screen with #227 on 2026-10-14), 561 commands. Rows for the 2026-10-17 landings: import/export (area 14) and the file formats dimension name the `.fig` import's new fidelity (instance identity, overrides by key, text style precedence, grid tracks, slots, connector labels, per-point joins, text on a path, image adjustments, list start numbers, vertical trim), budgets from the file's size, the import report, a `.fig` copy from Save as and paste from another design app; the localization evidence takes the re-measured catalog counts (2,272 entries, drafts at 95–99%, Indonesian and Czech drafts, Swedish and Turkish catalogs); the Automation line adds control requests answered while the window is minimised (#720); batch #753's Selection colors, smart-selection spacing, Present tab and auto layout on empty frames are in ui-parity, prototyping-parity and auto-layout-parity. Numbers unchanged: every `.fig` and paste change is checked on synthetic files or clipboard HTML made from our own export, and real files are what gap 1's blocker and the file-exchange discounts wait for; the UI changes (menus, the Actions palette, editor fixes) are not compared side by side, so UI/UX stays at 58%; the localization grade and hours stay (Indonesian's draft is offset by the drafts' new gaps) |
| 2026-10-17 | minor | Automation roots (M8, #715, #481): `figcraft-cli mcp --automation-read-root DIR --automation-write-root DIR` (each repeatable, existing folders) and the desktop app with the same flags confine every file a command reads to the read and write roots and every file it writes to the write roots; paths are resolved (links followed, `..`, Windows `\\?\` and UNC paths, case) and compared by whole folder names, anything else is an ordinary error. The engine reaches the file system through one module (enforced by `crates/engine/clippy.toml`), and a sweep runs every command with paths outside the roots. `--attach` refuses the flags (the app confines itself); `ui.screenshot` and `ui.render` return PNG data without a `path`, so the attached MCP server writes no temporary files. Numbers unchanged. |
| 2026-10-17 | minor | Number variables on text content (M3, #629): the Typography section's variable picker also lists number variables for a text layer's content, as the engine already accepted (the text shows the number, e.g. 42.5, and follows the variable). Other text fields keep their own types. Numbers unchanged. |
+26 -23
View File
@@ -17,12 +17,12 @@ Simplified.
The catalogs: English (`en`, the source), and Spanish (`es`, with the Latin American overlay
`es-419`), French (`fr`), German (`de`), Dutch (`nl`), Brazilian Portuguese (`pt-BR`), Japanese
(`ja`), Korean (`ko`), Simplified Chinese (`zh-Hans`), Czech (`cs`), Indonesian (`id`), Finnish
(`fi`), Polish (`pl`) and Bengali (`bn`), machine-drafted and waiting for native speakers' review. Georgian
(`ka`) was written with AI assistance and reviewed by a native speaker, and stays a draft until a
maintainer changes its status. Russian (`ru`, Русский) was
reviewed and approved by VanTan; its catalog keeps Frame, Dev Mode and Smart animate as familiar
tool names and uses « » quotes. Turkish (`tr`) was machine-drafted and reviewed by a native speaker,
and Swedish (`sv`) translates every UI string, command label and menu wording.
(`fi`), Bengali (`bn`), Italian (`it`), Vietnamese (`vi`) and Thai (`th`), machine-drafted and waiting for native speakers' review. Polish (`pl`,
Polski) was reviewed and approved by Lucas (@lukasz-sklad). Georgian (`ka`) was written with AI
assistance and reviewed by a native speaker, and stays a draft until a maintainer changes its status.
Russian (`ru`, Русский) was reviewed and approved by VanTan; its catalog keeps Frame, Dev Mode and
Smart animate as familiar tool names and uses « » quotes. Turkish (`tr`) was machine-drafted and
reviewed by a native speaker, and Swedish (`sv`) translates every UI string, command label and menu wording.
## The catalogs
@@ -61,10 +61,17 @@ formal Anda (never kamu), keeps established design terms in English (frame, laye
instance, auto layout, plugin, file) and keeps a noun unchanged after a count, so its plural entries
give only `other`. Georgian has no letter case (never write Mtavruli for capitals), uses verbal nouns
for commands (გვერდის დამატება) and keeps a noun singular after a numeral, so counts read
"შრე: {n}" and its plural entries give the same text for `one` and `other`.
"შრე: {n}" and its plural entries give the same text for `one` and `other`. Vietnamese uses concise
imperatives for buttons and menu items (Lưu, Xóa) and keeps a noun unchanged after a count, so its
plural entries give only `other`. Thai is concise, with bare imperatives for buttons and
menu items (บันทึก, ลบ), no ครับ/ค่ะ endings, no spaces between Thai words (a space only separates
clauses, and Thai from Latin words or numbers), and design terms transliterated as Thai designers say
them (เฟรม, เลเยอร์, คอมโพเนนต์, อินสแตนซ์, ออโต้เลย์เอาต์). It has no plural forms: plural entries give
only `other`, and dates read `{day} {month} {year}` with Thai month abbreviations.
CJK text needs a CJK font, which egui and Inter lack: the desktop app adds an installed one when
something on screen needs it (see `crates/ui-egui/src/cjk.rs`). The web build has no system fonts,
CJK and Thai text need their own fonts, which egui and Inter lack: the desktop app adds an installed
one when something on screen needs it, so layer and page names in those scripts show whatever the
UI language (see `crates/ui-egui/src/cjk.rs`). The web build has no system fonts,
so Japanese, Korean and Chinese appear there only in a build with craft-fonts and
`FIGCRAFT_EMBED_CJK=1` (Noto Sans CJK SC, +16 MB); otherwise they are left out of the picker. A
Chinese UI puts its Chinese face first so Han takes Simplified Chinese shapes. Georgian is drawn in
@@ -80,8 +87,8 @@ picker leaves Georgian out. A new CJK or other non-Latin language also needs its
set `[meta] name` and `status = "draft — needs native review"`.
2. Translate every value; keep the keys. Keep `{placeholders}` exactly, "…" where English has it,
brand names and keyboard glyphs. English labels of commands: `figcraft-cli commands`.
3. Plurals: give `one` only if the language has a singular form (Japanese, Korean and Chinese give
only `other`). `other` is required; `one`, `few` and `many` are optional. `trn` picks `one` for
3. Plurals: give `one` only if the language has a singular form (Japanese, Korean, Chinese and
Vietnamese give only `other`). `other` is required; `one`, `few` and `many` are optional. `trn` picks `one` for
1 (also 0 in French and Brazilian Portuguese). Russian uses `one` for 1, 21, 31… except 11;
`few` for 2–4, 22–24… except 12–14; and `many` for the other integers, including 0. Keep `{n}`
in all Russian forms. Add new rules to `plural` in
@@ -91,13 +98,9 @@ picker leaves Georgian out. A new CJK or other non-Latin language also needs its
three forms. Czech's CLDR `many` applies only to decimals; `trn` takes integer counts.
Polish uses `one` for 1 only, `few` like Russian (2–4, 22–24… except 12–14) and `many` for
the other integers, including 0 and 21 (e.g. `{n} warstwa`, `{n} warstwy`, `{n} warstw`).
4. To require full coverage, add the code to `COMPLETE` in
[`crates/ui-egui/src/i18n_tests.rs`](../crates/ui-egui/src/i18n_tests.rs). Every later PR that
adds a string then has to translate it into each complete language, so only Spanish is there:
French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Czech, Indonesian and
Georgian are registered as drafts, Russian and Turkish are reviewed but not required, and a string added
later shows English in them until it is translated. To list what a draft lacks,
add its code to `COMPLETE` locally and run the check.
4. A string added later shows English in every language until someone translates it; a missing
translation never fails a check or a build. `cargo xtask i18n-report` lists what each language
lacks (UI strings, command labels, menu wordings), and `cargo xtask ci` prints the same list.
5. Run the check and look at the panels in the app (longer words must fit the right panel):
cargo test -p figcraft-ui-egui i18n
@@ -106,9 +109,9 @@ picker leaves Georgian out. A new CJK or other non-Latin language also needs its
with "…" and show it in full on hover; elsewhere, look for clipped or wrapped text.
The check fails on keys the code uses that `en.toml` lacks (it prints the lines to paste), keys no
code uses, unknown command ids, placeholders that don't match English, and, for complete languages,
anything untranslated. Machine-drafted catalogs stay marked as drafts until a native speaker has
reviewed them; a reviewed catalog stays a draft until a maintainer approves it (its `status` says which).
code uses, unknown command ids and placeholders that don't match English. Machine-drafted catalogs
stay marked as drafts until a native speaker has reviewed them; a reviewed catalog stays a draft
until a maintainer approves it (its `status` says which).
## Adding strings in code
@@ -116,11 +119,11 @@ Wrap every string a user sees or a screen reader hears: `tr` for plain text, `tr
placeholders, `trn` for counts, `tr_noop` for literals in `const` tables that are looked up later
with `tr(label)`. The key must be a string literal as the first argument (the check reads the
source). Don't wrap ids, command ids, JSON, log messages, file extensions or document content. Then
add the key to `en.toml` and to each complete language (Spanish and Russian); the drafts may follow later.
add the key to `en.toml`; translations may follow later.
Labels from the engine and document crates (tool names, blend modes, easing curves…) are looked up
at display with `tr(x.label())`; they are listed in `dynamic_labels` in `i18n_tests.rs`.
The importers' fixed notes (the string literals passed to `warn` in `crates/format/src/fig` and
`crates/format/src/rest`) are keys too: the import report shows them in the UI language, so a new
note goes into `en.toml` and each complete language. Notes built with `format!` show in English.
note goes into `en.toml`. Notes built with `format!` show in English.
+15 -9
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (a rename selects the whole name, so typing replaces it; Escape or Enter after a pen path stays on the same vector (#760); IME composition kept on through idle frames of a text edit, candidates at the caret (#747); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (Context menus: the Menu key reaches egui 0.37 but is unbound (row done → partial); the Text on a path tool; board tool keys pinned; community fixes: the text box by dragging, the font picker keys, the pen path on leaving Design, a still canvas in vector edit mode, a scrolling pages list, colour emoji, readable new text in frames, system theme on first run; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths; a rename selects the whole name, so typing replaces it; Escape or Enter after a pen path stays on the same vector (#760); IME composition kept on through idle frames of a text edit, candidates at the caret (#747); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes;Line truncated
Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys,
drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status:
@@ -20,7 +20,7 @@ interaction feel not yet compared.
| Minimize UI (Cmd+\\) | done | |
| Resizable panels | done | each panel's edge drags its width (left 200–480, right 240–480, 240 by default; a double-click on the edge or View ▸ Panels ▸ Reset panel widths puts the default back); the widths are each window's own, the main window's are kept in the preferences when a drag ends and come back at the next start, a new window opens with those of the window it comes from; Minimize UI hides the panels and brings them back as they were; the view keeps its centre and zoom while a panel is resized; in a window too small for them the panels give way so the canvas keeps 160 px (the left one first), down to the smallest window (800×500) (`panel_width_tests`). Not compared with Figma's widths; no snap points |
| Actions palette (Cmd+K, Cmd+/) | done | tabs: All (every command, grouped by menu, or Other), Assets (components and styles) and Plugins & widgets; results ranked prefix first, then shortest, at most 60 rows; Up/Down move a highlight and Enter runs it; Recents list what was run from the palette (matching translated labels); one open/toggle path from every entry point (`palette_tests`). Ranking and the Assets results not compared with Figma's |
| Context menus | done | canvas and layers; submenus as in the main menu below, and a right-click in a new spot starts a fresh menu (Select layer closed); Shift+F10 opens them from the keyboard (at the focused layer row or below the canvas selection) and the menu takes the keyboard; the Menu key isn't delivered by the windowing layer (egui 0.36 has no key for it) |
| Context menus | partial | canvas and layers; submenus as in the main menu below, and a right-click in a new spot starts a fresh menu (Select layer closed); Shift+F10 opens them from the keyboard (at the focused layer row or below the canvas selection) and the menu takes the keyboard; the Menu key reaches egui as `Key::Menu` (egui-winit 0.37 maps the native key), but FigCraft's keymap has no binding for `Menu`, so it does nothing yet |
| Home screen: Recents, Online and Templates | done (ours) | The sidebar switches between the three (`ui.set {homeSection}`). Online: your online files (see the collaboration checklist). Recents: new file, open, join with a code, open files, recent files (cards with the picture each file carries on its page colour, the file's icon, name and "Edited ... ago" or "File not found", full path on hover; or a Name, Last modified and Created table with sortable headers; sorted by name, creation or last change either way; grid or list and the sort kept in the preferences, `ui.set {homeRecentsView, homeRecentsSort, homeRecentsOrder}`; a click selects, a double-click or Enter opens, arrow keys move the selection; Open, Show in folder and Remove from recents on right-click), a Getting started card (six tips with their shortcuts from the keymap; its close button is remembered in the preferences and Help ▸ Getting started brings it back) and, while there are no recent files, four templates to start from. Templates: the eight starter templates as cards (thumbnail rendered from the document, cached per theme; name, description, kind and size), a search field and a kind filter, an empty state; Tab reaches every card and Enter opens it; cards and tabs are named for screen readers. A first run opens here. No Community files or drafts folder |
| Main menu from the logo | done | generated from `catalog.rs`; popups sized to content. Submenus open after 0.15 s on their row (a click or the keyboard opens one at once) and close at once when another row is hovered; while the pointer heads for an open submenu inside the triangle to its near edge, the rows it crosses don't take over, and resting 0.3 s on one hands over; closing the menu, however it closes, clears which submenus were open, so it reopens with none (`menu_aim_tests`, 10 tests; timings tuned by feel, not measured against Figma). Menu and palette items that need a value ask for it first: Create link, Offset path and Move to page in a dialog of the shared controls, the language, user name, server and nudge items in Preferences, Open recent on the home screen's Recents; File ▸ Rename edits the file name in place |
| Keyboard shortcuts panel | done | closes when the home screen shows (#548); docked, tabbed by group like the documentation, searchable, per-platform keycaps; held keys, gestures and presentation keys listed beside the bindings; a label too long for its column ends with "…" and shows in full on hover |
@@ -41,7 +41,7 @@ interaction feel not yet compared.
| Eye and padlock: click, or drag across rows to set several in one step; layers in a locked frame show a faint padlock | done | the toggles no longer vanish under the pointer (clicks on them could miss) |
| Rename: double click, F2 / Cmd+R, Tab to the next row; several layers open Rename layers (match, regular expressions, current name and number codes, start value, preview) | done | numbered in stacking order, pinned |
| Find layers (filter by name) | done | not covered by the documentation |
| Pages: add, rename, drag to reorder, duplicate ("Copy of"), delete, dividers (a blank page named with a hyphen) | done | the line between Pages and Layers drags to size the pages list, kept in the preferences (`layers_behaviour_tests`) |
| Pages: add, rename, drag to reorder, duplicate ("Copy of"), delete, dividers (a blank page named with a hyphen) | done | the line between Pages and Layers drags to size the pages list, kept in the preferences; a long list scrolls in its own area and a page dropped outside the visible list stays put (#272) (`layers_behaviour_tests`) |
| Assets panel: folders from page, frames and slash names (fold away), search, thumbnails, drag onto the canvas or into frames and auto layout, Alt-drop swap, enabled libraries | done | Shift+I (Quick insert) and the toolbar's Resources open it (they didn't, #710) |
| Hide when publishing from the Assets panel's context menu | done | a right-click on one of this file's components (also Libraries ▸ Hide when publishing for the selection, `component.hideWhenPublishing {ids?, on?}`): the model's sign of it is the name, so hiding puts a `.` before the name and showing takes the leading `.` and `_` away (one undo step, saved and synced like a rename; a variant or a layer inside hides its whole component or set); a hidden component shows a crossed eye on its tile and a dimmer name, the menu item its check mark; the next publish leaves it out and brings it back when shown again (`hide_publishing_tests`). Library components (read-only here) have no such menu |
@@ -70,7 +70,7 @@ interaction feel not yet compared.
| Transform box at small sizes | done | a press inside a layer smaller than its handles moves it, the nearest handle wins, handles answer just outside and are hidden under 16 px on screen; cursors follow each handle and the box's rotation |
| Locked layers | done | selectable from the layers panel; no handles, not dragged with the rest of a selection |
| Smart selection spacing | partial | with layers in a row, a column or an aligned grid selected, the Layout section shows horizontal and vertical spacing ("Mixed" until set; typed, scrubbed, fractional and negative) and pink handles in the gaps on the canvas drag them, one undo step, Escape cancels (`arrange.spacing`; sizes, parents and offsets within each track kept; at most 2,000 layers; flow children, locked layers and layers inside instances left out). No reorder rings for swapping layers; checked by its author on macOS and in the app on Windows (#402) |
| Selection colors | partial | for a mixed selection the Design panel lists their solid and gradient paints (children and text ranges included; hidden paints, masks, images, patterns and boolean operands left out; styles and variables listed apart): editing a colour changes every matching fill and stroke in one undo step, a row selects the layers that use it or applies a colour style of the document (`selection.colors`, `selection.setPaint`, `selection.applyColorStyle`, `selection.selectColor`). No library styles or variable binding from there (#403) |
| Selection colors | partial | for a mixed selection the Design panel lists their solid and gradient paints (children and text ranges included; hidden paints, masks, images, patterns and boolean operands left out; styles and variables listed apart): editing a colour changes every matching fill and stroke in one undo step, a row selects the layers that use it or applies a color style or color variable of the document, and a row linked to a style or variable detaches it, keeping the color (`selection.colors`, `selection.setPaint`, `selection.applyColorStyle`, `selection.bindColorVariable`, `selection.detachColor`, `selection.selectColor`). No library styles or variables from there, and no new style or variable from a row (#403) |
| Keyboard box selection | done | Alt+Space on macOS, Ctrl+Space on Windows and Linux, or F8 (`select.area`): a box around the selection or in the middle of the view; arrows move it, Cmd/Ctrl+arrows or Shift+arrows size it (Shift with Cmd/Ctrl: farther), Enter selects like a marquee (Shift+Enter toggles, Cmd/Ctrl+Enter nested), Escape closes |
## Tools
@@ -81,14 +81,14 @@ interaction feel not yet compared.
| Frame (presets panel, swap orientation), section, slice | done | |
| Rectangle, ellipse (arc handles and Design panel arc fields), polygon, star, line, arrow; Shift/Alt | done | Alt draws lines from the centre too (`shapes_behaviour_tests`) |
| Corner radius handles (inside each corner of a rectangle or frame, a polygon's or star's top point; Alt: one corner), star ratio handle; Count and Ratio fields | done | handles hide under 48 px on screen; no count handle on the canvas (the Count field instead); corner smoothing rounds rectangles and frames only |
| Pen: vector networks, branch, close, bend; pencil | done | pen pressure on Windows and from browser styluses only; outside edit mode the Pen closes on its first point, connects to its other points and continues a selected open path from an end point (`vector_multi_behaviour_tests`); Escape or Enter ends the path and keeps its layer in edit mode with the pen, so the next path joins the same vector and another Escape leaves edit mode (#760, `pen_end_tests`) |
| Vector edit mode: bend, paint bucket, lasso, cut, eraser, transform box, point snapping, mirroring | done | a double-click a few pixels off a thin stroke enters edit mode (#646); a point dropped on another point merges with it, and dropping one end of an open path on the other closes it (a 2-point line's end just moves, #401); Flatten keeps open paths open (#377); paint bucket drags across areas; cut drags divide a shape into layers; delete and heal follows the curve; rounded points are drawn; Shift-click deselects a point at a corner of the box (`vector_behaviour_tests`); several vectors edited at once, Shift-click handles to drag them together, a cap and join per point, rounded points beside curves (`vector_multi_behaviour_tests`); filled regions with the Move tool: the area under the pointer is hatched ("/" lines in the selection colour, clipped by the area's own fill rule, holes left out, gone 3.5 px from a point or 2 px from a segment at any zoom), the line spacing follows octave folding (fitted to measurements at several zoom levels: 8 px at every power-of-two zoom, growing with the zoom and stepping back at √2 · 2ⁿ) and stays fixed to the shape when panning; a click selects the area and its points (moved with it but drawn unselected), Shift adds or removes an area, a press on a selected area drags the whole selection, no hover hatch during drags, a click on an empty spot (a hole) clears it and stays in edit mode; `vector.edit.select {regions}` (`regions_tests`, `vector_regions_tests`). Not yet: the transform box around points of several layers; areas of other layers edited at once |
| Pen: vector networks, branch, close, bend; pencil | done | pen pressure on Windows and from browser styluses only; outside edit mode the Pen closes on its first point, connects to its other points and continues a selected open path from an end point (`vector_multi_behaviour_tests`); Escape or Enter ends the path and keeps its layer in edit mode with the pen, so the next path joins the same vector and another Escape leaves edit mode (#760, `pen_end_tests`); switching from Design to Prototype or Inspect ends the path and returns to Move (#308) |
| Vector edit mode: bend, paint bucket, lasso, cut, eraser, transform box, point snapping, mirroring | done | a double-click a few pixels off a thin stroke enters edit mode (#646); entering edit mode and picking a region keep the canvas still (#322); a point dropped on another point merges with it, and dropping one end of an open path on the other closes it (a 2-point line's end just moves, #401); Flatten keeps open paths open (#377); paint bucket drags across areas; cut drags divide a shape into layers; delete and heal follows the curve; rounded points are drawn; Shift-click deselects a point at a corner of the box (`vector_behaviour_tests`); several vectors edited at once, Shift-click handles to drag them together, a cap and join per point, rounded points beside curves (`vector_multi_behaviour_tests`); filled regions with the Move tool: the area under the pointer is hatched ("/" lines in the selection colour, clipped by the area's own fill rule, holes left out, gone 3.5 px from a point or 2 px from a segment at any zoom), the line spacing follows octave folding (fitted to measurements at several zoom levels: 8 px at every power-of-two zoom, growing with the zoom and stepping back at √2 · 2ⁿ) and stays fixed to the shape when panning; a click selects the area and its points (moved with it but drawn unselected), Shift adds or removes an area, a press on a selected area drags the whole selection, no hover hatch during drags, a click on an empty spot (a hole) clears it and stays in edit mode; `vector.edit.select {regions}` (`regions_tests`, `vector_regions_tests`). Not yet: areas of other layers edited at once |
| Shape builder (Shift+M) on fills, strokes and open paths; variable width (Shift+W), one side with Alt | done | |
| Boolean groups: clicked where the result is, double-click or Cmd/Ctrl-click into operands, live results while dragging or editing points, per-operand operations | done | |
| Text: point, auto height, fixed; caret, words, lines, IME | done | the font picker previews a family on the canvas while hovered (no undo step; Escape restores, #653); a selection highlight after a line break starts at that line's start and an empty last line shows its mark (#665); carets step over whole emoji and accented letters; IME composition replayed in tests as macOS sends it, not yet checked on a Mac (#747); CJK words by script run (no dictionary); vertical trim in Type settings; no spell check |
| Text: point, auto height, fixed; caret, words, lines, IME | done | the font picker previews a family on the canvas while hovered (no undo step; Escape restores, #653) and Up/Down step through it (#352); a selection highlight after a line break starts at that line's start and an empty last line shows its mark (#665); carets step over whole emoji and accented letters; colour emoji draw from their colour tables (#300); IME composition replayed in tests as macOS sends it, not yet checked on a Mac (#747); CJK words by script run (no dictionary); vertical trim in Type settings; no spell check |
| Eyedropper (I) | done | picks the colour seen under the pointer (opaque) into the selection's fill and the recent colours, one undo step (`fx_behaviour_tests`) |
| Comment (C), Shift+C to show/hide | done | local only |
| Text on a path (Figma Draw) | done | put on from the Design panel or `text.onPath`, not by clicking a path with the Text tool; the path is edited by resizing only |
| Text on a path (Figma Draw) | done | the Text on a path tool in the Text flyout (`tool.textOnPath`) shows the I-beam over a shape's or vector's path, and a click there puts new text on the path, starting at the click, with the path's effects and its fill when one shows; the path layer is removed, and the path comes back if nothing is typed. Also from the Design panel or `text.onPath`; the path is edited by resizing only |
| Stretch and scatter brushes (Figma Draw) | done | eight presets drawn in code; the Pencil's brush is set by `tool.pencilBrush`, not from the toolbar |
| Image crop handles | done | |
@@ -129,7 +129,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
| Bound on other keys (documented conflict) | 1 | Run last plugin: the documentation also gives Ctrl+Alt+P to pixel preview on Windows; the plugin takes Ctrl+Alt+Shift+P there |
| Missing | 0 | moving the rotation origin (Alt+R, `transform.rotationOrigin`) and placing an object with Enter after choosing its tool (`tool.place`) were the last two |
| Documented keys a platform keeps | 2 | Ctrl+Space (Windows/Linux keyboard box selection) switches some CJK input methods; F6 goes to a browser's address bar in the web build |
| Undocumented bindings pinned | 49 | of which Cmd/Ctrl+W, Cmd/Ctrl+N and Ctrl+Tab are taken by browsers (web build) and Ctrl+Alt+T / Ctrl+Alt+L by some Linux desktops |
| Undocumented bindings pinned | 57 | of which Cmd/Ctrl+W, Cmd/Ctrl+N and Ctrl+Tab are taken by browsers (web build) and Ctrl+Alt+T / Ctrl+Alt+L by some Linux desktops |
- **Newly bound:** A for the Frame tool, Shift+' for the pixel grid (Cmd+' kept: the articles
disagree), Alt+Cmd+\ / Ctrl+Alt+\ for others' cursors (`view.multiplayerCursors`, new), Ctrl+F6
@@ -172,6 +172,12 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | minor | Context menus: the Menu key is no longer an egui limitation at 0.37 (egui-winit maps the native Menu/ContextMenu key to egui's `Key::Menu`), but FigCraft normalizes no binding for `Menu`, so it does not open a context menu yet; Shift+F10 still does, with the same behaviour. The row moves from done to partial. Numbers unchanged |
| 2026-10-24 | minor | Selection colors: a row's Styles menu also lists the document's color variables, and picking one binds it to every fill and stroke the row stands for, replacing a color style there (`selection.bindColorVariable`; a text range with fills of its own takes the variable's value); a row linked to a style or variable has a Detach button that unlinks it and keeps the color (`selection.detachColor`). Still partial: no library styles or variables, no new style or variable from a row |
| 2026-10-24 | minor | The Text on a path tool (`tool.textOnPath`, in the Text flyout): a click on a shape's or vector's path puts new text on it, starting at the click, with the path's fill (when one shows) and effects, as the help center's Guide to text describes; the path layer is removed, and an edit left empty puts it back with no undo step (`draw_tools_tests`, `cursor_tests`) |
| 2026-10-24 | minor | Boards (M12): eight board bindings pinned (tool keys Q sticky, B shapes, X connector, M marker, U highlighter, E eraser, W stamp, and Shift+Cmd/Ctrl+Enter for the next sticky; boards only, on keys no design-file shortcut uses): 49 → 57 pinned |
| 2026-10-15 | minor | Community fixes (#262, #272, #277, #300, #308, #311, #322, #352, #363, #372, #390): first runs follow the system theme; new text is readable on the frame it is drawn in; a long pages list scrolls on its own and new pages take the current page's colour; the pen shows its handles while dragging a point out and ends its path when you leave Design; vector edit mode keeps the canvas still; a drag with the Text tool makes a fixed-size box; colour emoji; the selection highlight on the last line; Up and Down in the font picker with a live preview. Numbers unchanged |
| 2026-10-22 | minor | Vector edit mode: points selected in several layers get the transform box, in the page's axes since the layers can be turned differently; its corners and edges scale them, outside a corner rotates them and inside drags them, each layer's points in its own coordinates and one undo step for all, and a click inside clears every layer's points (`transform_tests`). Numbers unchanged |
| 2026-10-20 | minor | Readable disabled and selected text (#890): the dim text colour (disabled buttons, placeholders, hints) is at least 3:1 against panels, fields and selected rows in both themes (it was 2.1:1 in the light theme), a disabled button is grey with that text instead of fading to 40%, and selected text (renaming a page or layer, any text box) takes the theme's readable green on the highlight (1.4:1 in the dark theme before, now 4.2:1 or more); checked with the colour contrast helper (`theme::tests`). Numbers unchanged |
| 2026-10-20 | minor | Measuring while nudging (#894): with Alt held over another layer, the arrows nudge the selection (Shift: the big step) and the distances follow every nudge (Alt+arrows used to do nothing); while measuring, the selection's handles and size label hide and both layers keep their outlines (`measure_nudge_tests`). Numbers unchanged |
| 2026-10-18 | minor | A rename (layers double-click, F2, File ▸ Rename, a page) selects the whole name in the frame its field first draws, so the first key replaces the name instead of being appended. Checked in the app on Windows over the control channel |