mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-01 22:36:51 -06:00
Merge remote-tracking branch 'origin/main' into 1235-transliterate-class-names
# Conflicts: # ROADMAP.md # crates/engine/Cargo.toml
This commit is contained in:
+22
-14
@@ -1,12 +1,14 @@
|
||||
# Boards parity
|
||||
|
||||
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-24 · **Change:** major (created: board files, the first slice of the whiteboard workflow) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
|
||||
> **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 ~35%, ready ~20% (estimated, erring low).** A board is a FigCraft file of its own kind
|
||||
(`file.newBoard`) with sticky notes, shapes with text, connectors that follow what they join,
|
||||
marker, highlighter and eraser, stamps, text and sections, edited together in the existing live
|
||||
sessions. The facilitation tools (timer, voting, widgets), templates, tables, mind maps and the
|
||||
rest of the tail are missing. Remaining: ~30–60 h. Tracked as area 22 of
|
||||
**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).
|
||||
|
||||
@@ -26,14 +28,15 @@ all end with the same geometry.
|
||||
|---|---|---|
|
||||
| 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 and its arrow; the Design panel opens with its button or View ▸ Design panel |
|
||||
| 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 or elbow, arrowheads (the stroke's caps), a label in the middle (double-click to add); re-routed when either end moves or resizes; a deleted end stays where it was |
|
||||
| Curved connectors, connector waypoints, dragging an end to re-attach | missing | ends are attached when drawn or through `board.connector` |
|
||||
| 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 |
|
||||
@@ -42,20 +45,25 @@ all end with the same geometry.
|
||||
| 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 and music | missing | |
|
||||
| Voting sessions | missing | |
|
||||
| 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…) | missing | |
|
||||
| Tables | missing | |
|
||||
| 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 | missing | the canvas grid is drawn over the render; a dot grid needs a render pass under the layers |
|
||||
| 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 |
|
||||
|
||||
+20
-7
@@ -8,6 +8,10 @@ commands, serve MCP and measure performance. Build it with
|
||||
`--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
|
||||
|
||||
Text lays out and renders with the same fonts as the desktop app: the fonts built into FigCraft
|
||||
@@ -21,6 +25,10 @@ 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, 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
|
||||
@@ -38,7 +46,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
|
||||
@@ -92,7 +101,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
|
||||
@@ -112,14 +121,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"}}}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
+14
-1
@@ -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,
|
||||
|
||||
@@ -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. |
|
||||
|
||||
+33
-14
@@ -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 {}`
|
||||
@@ -307,10 +317,19 @@ 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?}`
|
||||
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (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)
|
||||
> **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,6 +18,7 @@ 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 |
|
||||
| `.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`); 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) | — |
|
||||
@@ -49,7 +50,7 @@ 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, except that with contents only on a slice inside a frame or group exports only that frame or group; 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, 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 |
|
||||
@@ -165,9 +166,11 @@ each.
|
||||
|
||||
| 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 |
|
||||
|
||||
+57
-21
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
# Hardware and platform parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (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)
|
||||
> **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
|
||||
@@ -15,10 +15,10 @@ since 2025). FigCraft renders the canvas on the CPU and presents it through wgpu
|
||||
| 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; a minimised or covered window still answers control and MCP requests, while screenshots and input fail at once until it is on screen (#313, #332, #720). 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,8 @@ 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 |
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 308 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 294 KiB |
+27
-24
@@ -1,10 +1,10 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (a missing translation fails no check: `cargo xtask i18n-report` lists them; 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)
|
||||
> **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,21 +18,21 @@ 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
|
||||
@@ -54,7 +54,7 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
|
||||
- **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
|
||||
@@ -87,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
|
||||
@@ -115,13 +116,15 @@ 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 |
|
||||
|
||||
+147
-37
@@ -308,8 +308,8 @@ 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, 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 and Leptos, `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, 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 and Leptos, `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). |
|
||||
@@ -317,7 +317,7 @@ Or in a project's `.mcp.json`:
|
||||
| `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, 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. |
|
||||
@@ -396,20 +396,36 @@ one undo step and returns the ids it made:
|
||||
(`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, startCap?, endCap?, label?}`
|
||||
- `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). 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).
|
||||
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?, startCap?, endCap?}`: a shape's
|
||||
outline and text size, a connector's routing and arrowheads.
|
||||
- Tools for `pointer`: `tool.sticky {color?}`, `tool.shape {shape?}`, `tool.connector {style?}`
|
||||
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.
|
||||
|
||||
@@ -425,6 +441,40 @@ one undo step and returns the ids it made:
|
||||
`{"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
|
||||
@@ -535,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.
|
||||
|
||||
@@ -1118,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.
|
||||
@@ -1140,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
|
||||
|
||||
@@ -1379,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)):
|
||||
@@ -1408,24 +1477,43 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path
|
||||
|
||||
### Dev Mode
|
||||
|
||||
- **Component code:** `inspect.code {id, language: react|leptos|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|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), 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) 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 and Leptos the other variants'
|
||||
differences become CSS rules on `data-*` attributes. On an instance (or a layer tree containing
|
||||
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, 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`, `swiftui`, `compose`): `codeConnect.set {id?, language?, importPath?,
|
||||
as, per language (`react`, `leptos`, `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
|
||||
@@ -1825,25 +1913,47 @@ 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.
|
||||
|
||||
`"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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
+24
-5
@@ -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
|
||||
|
||||
+9
-4
@@ -1,6 +1,6 @@
|
||||
# FigCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (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)
|
||||
> **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
|
||||
@@ -54,13 +54,13 @@ 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 | ~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) | 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 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 | timer, voting, widgets, templates, tables, mind maps ([boards-parity](boards-parity.md)) | 30–60 |
|
||||
| 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
|
||||
@@ -75,7 +75,11 @@ 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 |
|
||||
@@ -127,6 +131,7 @@ blockers.
|
||||
| 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
@@ -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
|
||||
|
||||
+20
-14
File diff suppressed because one or more lines are too long
+18
-11
@@ -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
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user