mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-01 06:16:46 -06:00
Merge remote-tracking branch 'origin/main' into feat/996-inspect-fields
# Conflicts: # docs/roadmap.md
This commit is contained in:
@@ -75,7 +75,7 @@ content. Exports: PNG, JPG, WebP, SVG, PDF, code. There is no `.fig` reader yet
|
||||
|
||||
Workspace lints deny `unwrap`, `expect`, `panic!` and friends in shipped code; `unsafe_code` is
|
||||
forbidden. `figcraft_engine::guard` turns a panicking command into an error and rolls the document
|
||||
back; the desktop app also guards each UI frame. Junk-parameter sweeps run every command with bad
|
||||
back; the desktop app also guards each UI frame. The browser build can't catch a panic (wasm32 aborts), so there a panic stops the page and its panic hook keeps the files with unsaved changes in local storage for the next page. Junk-parameter sweeps run every command with bad
|
||||
input.
|
||||
|
||||
## Revision history
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Auto layout and constraints parity
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-29 · **Change:** minor (Layout sidebar: Add auto layout (+) is available for groups and converts a single group to a frame in place, with mask order, flow-only inference and Fill sizing preserved; adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys only to frames, with warnings (#603); core editor leftovers: auto gap handles, wrapped-line alignment; behaviour suite written from the public documentation, every rule's status; fixes and the missing settings it found) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-30 · **Change:** minor (grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); Layout sidebar: Add auto layout (+) is available for groups and converts a single group to a frame in place, with mask order, flow-only inference and Fill sizing preserved; adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys only to frames, with warnings (#603); core editor leftovers: auto gap handles, wrapped-line alignment; behaviour suite written from the public documentation, every rule's status; fixes and the missing settings it found) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Auto layout is the most used layout feature in Figma and the one where small differences (a
|
||||
hugging frame one pixel off, text wrapping at another width) break real files. **Breadth ~98%,
|
||||
@@ -95,6 +95,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M8, #1083: grid placement finds free cells on a bitmap, a word at a time, with the same placements; crafted grids of wide children no longer stall a layout pass. Numbers unchanged. |
|
||||
| 2026-10-29 | minor | M2 review fixes: conversion preserves mask runs and Fill sizing per axis; layout inference uses visible, non-absolute flow children; the sidebar hides Add for mixed selections containing existing auto layout. Regression coverage includes rendered mask pixels, visible-child positions, parent layout stability and the sidebar accessibility tree. Numbers unchanged |
|
||||
| 2026-10-29 | minor | M2: the Layout sidebar exposes Add auto layout (+) for groups. `layout.add` converts one selected group into a frame in place rather than wrapping it: same id, name and direct children; the layout arranges those children, and one undo restores the group. Sidebar, menu, shortcut and API calls all use the corrected command. This follows the contributor’s observed Figma behavior and the workflow familiar to users of similar design tools. Tests cover direct-child reflow, stable hierarchy, undo/redo and wrapping a multi-group selection. Numbers unchanged |
|
||||
| 2026-10-17 | minor | Community batch #753: adding auto layout to an empty frame keeps its size, Fixed, while a frame with content hugs it (#369); `node.set` applies auto layout keys to the frames among its targets and warns about the others, and a font style the family lacks warns with the nearest face (#603). Checked over MCP. Numbers unchanged |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Boards parity
|
||||
|
||||
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-29 · **Change:** minor (board grid settings: dots, lines, graph paper or none, spacing, size and contrast; the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); 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)
|
||||
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-30 · **Change:** minor (a FigJam `.jam` file opens as a board (#1342); board grid settings: dots, lines, graph paper or none, spacing, size and contrast; the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); templates and tables; facilitation: a shared timer and voting sessions; the dot grid, dragging a connector's end to re-attach it, curved connectors, elbows that go around what they join) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
|
||||
|
||||
**Breadth ~47%, ready ~29% (estimated, erring low).** A board is a FigCraft file of its own kind
|
||||
(`file.newBoard`) with a dot grid on the page, sticky notes, shapes with text, connectors that
|
||||
@@ -57,12 +57,13 @@ all end with the same geometry.
|
||||
| Audio and huddles | missing | |
|
||||
| AI features (summaries, sorting stickies, generating boards) | missing | |
|
||||
| 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. Dots by default, or lines, graph paper (a strong line every 2–12 cells over fainter ones, like Japanese section paper) or none; spacing (4–512), mark size and contrast are settings kept with the preferences (`board.grid`; Preferences ▸ Board grid). Zoomed out it thins out (dots and lines by halves, graph paper down to its strong lines) until the marks are at least 14 points apart (10 for lines), the marks the next step drops fading out first; dots stay about a point and a half across on screen (1 to 3 points), scaled by the size setting. Dark marks on a light page, light ones on a dark page, from the theme's ink; written straight into the image, no allocation per mark, a crossing of two lines blended once; never in exports. FigCraft's own settings: the incumbent's board grid has none |
|
||||
| `.jam` import and export | missing | see [`file-format-parity.md`](file-format-parity.md) |
|
||||
| `.jam` import and export | **partial**: a `.jam` opens as a board, best effort (#1342): its stickies and shapes with text come in as frames with their generated layers and its connectors as vectors, not as native board objects; no export | see [`file-format-parity.md`](file-format-parity.md) |
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M12, #1342: a FigJam `.jam` file opens as a board, our whiteboard file kind, instead of a design file: the `.fig` reader marks a canvas with FigJam's `fig-jam.` header as `FileKind::Board`, so the file opens with the board toolbar, the dot grid and the board panels, and saves as a board. A `.fig` stays a design file, and `file.import` of a `.jam` adds its pages to the open file without changing its kind. Its stickies and shapes with text are still frames with their generated layers and its connectors vectors, not native board objects. Tests: `board_import_tests.rs` (the kind, `.jam` against `.fig`), `jam_open_tests.rs` (open as a board, import into a design file). |
|
||||
| 2026-10-29 | minor | M12: board grid settings (`board.grid`): dots, lines, graph paper or none, with spacing, size and contrast, in Preferences; the default grid looks as before. Numbers unchanged (beyond the incumbent's behaviour) |
|
||||
| 2026-10-29 | minor | M12, #1387 (connector tool): while a connector is dragged out with the connector tool, the canvas shows the connector itself as it will be made (its straight, elbow or curved route, line, weight and arrowhead, built by the same code as `board.connector`), not a thin line; the object under the pointer is outlined with its four attach points (the middles of its sides), the one the end goes to ringed; within 10 px of one the end keeps that side (`fromSide`/`toSide`, and `side` when re-attaching). Hovering with the tool shows the attach points too. Escape drops the preview; releasing makes the connector in one undo step. Tests: `board_connector_tests`. Numbers unchanged |
|
||||
| 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 |
|
||||
|
||||
+14
-5
@@ -203,6 +203,14 @@ a server that knows it keeps the new name and tells everyone with the client's `
|
||||
(apps take a `PeerJoined` for someone already there as their new name and colour); an older
|
||||
server drops it, so the session keeps showing the name the client joined with.
|
||||
|
||||
**Comment links** came later within version 2 the same way. A client that reads the `comment`
|
||||
access says so with the word `comments` in its `client_version` (hello, managing and library
|
||||
connections); a server welcomes a client that doesn't with a comment link as a viewer (and enforces
|
||||
it as one), and lists comment links to such a managing client as view links, so an older app never
|
||||
gets a level it can't read. This build reads a level it doesn't know (a later one) as `view`, in
|
||||
both encodings. An older server can't read `Admin createLink` with the `comment` access: the app
|
||||
asks for the comment link on a connection of its own and shares without one when it fails.
|
||||
|
||||
**Open hosting and reports** came later within version 2 the same way. A connection that opens
|
||||
with `OpenHosting` asks a server about its hosting, publishes without the key when the server is
|
||||
open to the public, or manages one file with its owner token; a server older than that can't read
|
||||
@@ -264,7 +272,7 @@ Server → client:
|
||||
|
||||
| Message | Meaning |
|
||||
|---|---|
|
||||
| `Welcome {session, client_id, seq, catch_up: Snapshot or Delta, processed, peers, access}` | Join accepted: the full document, or for a reconnecting client the batches it missed; what the server did with that client's batches (`processed {last_batch, refused}`); `access` is `edit` or `view` (a view link: every batch is refused). |
|
||||
| `Welcome {session, client_id, seq, catch_up: Snapshot or Delta, processed, peers, access}` | Join accepted: the full document, or for a reconnecting client the batches it missed; what the server did with that client's batches (`processed {last_batch, refused}`); `access` is `edit`, `view` (a view link: every batch is refused) or `comment` (a comment link: a batch is refused unless every change in it is to comment threads: a thread's `put` or `remove`, their order or the whole list). |
|
||||
| `Changes {seq, author, batch_id, changes}` | Accepted changes, in order, to everyone (the author gets its own back as confirmation). |
|
||||
| `Reject {batch_id, reason, fixes: [Change]}` | A batch (or part of it) was invalid: the current values to roll back to. |
|
||||
| `Presence {client_id, …}` / `PeerJoined` / `PeerLeft` | Other people's cursors, selections and arrivals. |
|
||||
@@ -492,7 +500,7 @@ resuming with its offline edits, and leaving the session going back to snapshot
|
||||
|
||||
- "Share" in the right panel opens a popover: your name, a choice between **Online** and **Local
|
||||
network**, and **Start sharing**; while sharing, the join code (large), **Copy code** and **Copy
|
||||
link**, what the code lets people do (online: can edit or can view), who is here, and **Stop
|
||||
link**, what the code lets people do (online: can edit, can comment or can view), who is here, and **Stop
|
||||
sharing**; while joined, **Leave session**. **Join with a code** is one field in the popover, on
|
||||
the home screen and in "File ▸ Join live session…": it takes a code of either kind or an invite
|
||||
link. Another server, a server key, the port and "this computer only" are under **Advanced**;
|
||||
@@ -550,10 +558,11 @@ resuming with its offline edits, and leaving the session going back to snapshot
|
||||
A joiner's app sends `Resolve {code}` to its own server in Preferences, gets the file and the
|
||||
link's token, and joins with them as with the link.
|
||||
- **The file is its owner's.** Publishing gives the file its first edit and view link and its
|
||||
owner token; those stay with the owner, whose app joins with that edit link. Sharing makes two
|
||||
more links for others (`Admin createLink`, labelled "Shared (edit)" and "Shared (view)") and
|
||||
owner token; those stay with the owner, whose app joins with that edit link. Sharing makes three
|
||||
more links for others (`Admin createLink`, labelled "Shared (edit)", "Shared (view)" and
|
||||
"Shared (comment)") and
|
||||
asks for their codes; those are what the popover shows and copies. **Stop sharing**
|
||||
(`collab.stop`) revokes these two links (`Admin revoke`, as the owner): their codes stop
|
||||
(`collab.stop`) revokes these links (`Admin revoke`, as the owner): their codes stop
|
||||
resolving ("That code has expired or its sharing was stopped"), the links stop joining, and
|
||||
everyone who joined with them is disconnected at once with close code 1001 and "Sharing was
|
||||
stopped: the link you joined with no longer works. Your copy stays open", which apps take as
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Collaboration parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); an AI agent attached over MCP shows where it works (#1416); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); comment.list reports each message's @mentions (#1415); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); a refused batch's fixes take time linear in the batch (#1063); image entries are checked as they apply; 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)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; `comment.list {mentions}` lists only the threads that @mention a name (#1415); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); an AI agent attached over MCP shows where it works (#1416); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); comment.list reports each message's @mentions (#1415); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); a refused batch's fixes take time linear in the batch (#1063); image entries are checked as they apply; 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
|
||||
| Live cursors, selections, viewports; follow / observation mode; spotlight | yes | **done, in live sessions**: named cursors, selection outlines, avatars (an AI agent attached over MCP also shows its avatar and an outline around the layers it last edited or selected while it works, in or out of a session, #1416), follow (click an avatar), others' text carets and selected characters with their names, a note when two people type in one layer; spotlight (your avatar's menu, `collab.spotlight`): everyone follows your view and page, people who join later too, anyone who moves their view stops following and a banner offers to follow again, a newer spotlight takes over. Left: polish (no accounts, so anyone in the session can spotlight) | 2–4 |
|
||||
| Live cursors, selections, viewports; follow / observation mode; spotlight | yes | **done, in live sessions**: named cursors (one name for live sessions and comments: Preferences ▸ Your name, also in the Share popover and the Join dialog, kept in the browser too; a rename shows at once in the sessions you're in, #1380), selection outlines, avatars, follow (click an avatar), others' text carets and selected characters with their names, a note when two people type in one layer; spotlight (your avatar's menu, `collab.spotlight`): everyone follows your view and page, people who join later too, anyone who moves their view stops following and a banner offers to follow again, a newer spotlight takes over. Left: polish (no accounts, so anyone in the session can spotlight) | 2–4 |
|
||||
| Cursor chat, audio, high-fives | yes | **partial, in live sessions**: cursor chat (`/` opens a bubble at your pointer; everyone sees it as you type, it fades a few seconds after the last change, Enter sends and Escape takes it back, 100 characters; `collab.cursorChat` for agents); sent as ephemeral presence the host checks and rate-limits, never stored. No audio, high-fives or emote reactions | 2–5 |
|
||||
| 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, and the Join dialog then shows the join until its file is ready (Cancel), or why it failed (Try again, #1381); 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), and shares online and publishes to a server as the desktop app does, though not on the local network (#917; the owner token of a file it puts on an open server lasts as long as the page)); `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 |
|
||||
| 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, comment 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, and the Join dialog then shows the join until its file is ready (Cancel), or why it failed (Try again, #1381); 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), and shares online and publishes to a server as the desktop app does, though not on the local network (#917; the owner token of a file it puts on an open server lasts as long as the page)); `figcraft-server` share links per file with edit, comment or view access (a viewer sees changes and presence, a "View only" badge and only the moving tools; a commenter also adds, replies to, reacts to and resolves comment threads, with a "Can comment" badge and the comment tool, #1525; the server refuses their other 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 (an AI agent attached over MCP comments as its own name, marked Agent); 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. `comment.list` (CLI, MCP) gives each message its `mentions` (`{name, known, you}`, by the highlighting rule). 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, 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 |
|
||||
@@ -29,6 +29,10 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M8, #1074: `figcraft-server` keeps a file everyone left loaded for a minute (at most 16 files, 256 MiB together), so rejoining it reads nothing, and writes a file's open time at most hourly (and when it stops) instead of at every join and leave: join/leave cycles of an unchanged file do no disk I/O on the hub thread. Edits are saved as before. Numbers unchanged. |
|
||||
| 2026-10-30 | minor | M9, #1415 (the rest): `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry each thread's mentions as `comment.list` does. Numbers unchanged. |
|
||||
| 2026-10-30 | minor | M9, #1525: share links can comment. A third access level, `comment`, sits between `edit` and `view`: someone who joins with a comment link (or its join code) sees the file live, as with a view link, and adds comment threads, replies, reactions and resolves them; any other edit is refused, by their app and by the server, which accepts a comment link's batch only when every change in it touches comment threads. Sharing online makes a comment link and code beside the edit and view ones, and the Share popover offers Can edit, Can comment and Can view; the toolbar shows a "Can comment" badge with Move, Hand and Comment. `collab.share {access: "comment"}`, `collab.manageFile {action: createLink, access: "comment"}` and `figcraft-server links create FILE --access comment` make and pick it. Older apps are unaffected: one that joins with a comment link is welcomed as a viewer, links listed to it read as view links, and a level an app doesn't know reads as view; an older server makes no comment link and sharing goes on without one. Comment authorship is the name in Preferences, as before (no accounts), so a commenter can edit or remove other people's threads. Tests: `comment_access_tests.rs` (collab), `comment_link_tests.rs` (engine, server end to end), the Share popover and toolbar in `live_share_tests.rs` and `live_server_tests.rs`. |
|
||||
| 2026-10-30 | minor | M9, part of #1415: `comment.list {mentions}` lists only the threads where a message @mentions a name, combined with the other filters, so an agent tagged in comments reads just its threads. Numbers unchanged. |
|
||||
| 2026-10-30 | minor | M9, #917: the browser build publishes to a `figcraft-server`. Share ▸ Online (the server in Preferences) and Share ▸ Publish to server… upload the open file over the page's own WebSocket, make the links and join codes others join with, and keep the tab in sync with the new file, as the desktop app does; asking a server about its hosting, stopping a share and sharing again go out the same way. The Share popover offers only online sharing there (a page can't listen, so the local network stays the desktop app's); without a server in Preferences it says so and offers joining. A failed publish says why and leaves the file as it was. The browser keeps only your name across reloads, so the owner token of a file it put on an open server lasts as long as the page (`crates/collab-net/src/exchange.rs`, run by the browser client's driver; the requests and what their answers mean are shared with the desktop transport in `requests.rs`). Tests: `exchange_tests` (scripted server: with and without the key, sharing or not, refusals, an old server without join codes, time-outs, leaving while publishing, the background requests), `live_join_tests`; checked in a browser against a local server. Percentages and hours unchanged |
|
||||
| 2026-10-30 | minor | Where an AI agent works (M8, #1416): an agent attached over MCP (`figcraft-cli mcp --attach`) changed the file with no sign of where or whether it was still at work. Now, while it runs commands, the people row at the top of the right panel shows its avatar, ringed in its own colour and named as its comments are signed ("Claude (agent)"), and the canvas outlines the layers its last edit changed (the innermost ones), or that it last selected, in that colour with its name. Both go 8 seconds after its last command, and only the file it works in shows them. Display only: no undo step, never saved, never sent to a live session's other people, and the user's own commands never make one (`agent_presence_tests` in the engine, `live::agent` tests in the UI). Ready unchanged |
|
||||
| 2026-10-29 | minor | M9, #1380: your name is one setting, shown where you join. Preferences calls it "Your name" (it signs your comments and is what others see in live sessions). The Join dialog shows it, editable, beside the code, as the Share popover does, and the field is empty while it is the default. A new name is kept at once, and the browser build keeps it in the page's local storage, so a reload joins as you (the other preferences still start afresh there). Changing it while in live sessions renames you in each of them: a `name` signal, which the server cleans and answers with your `PeerJoined` to everyone, and newcomers' welcomes list the new name. A rename made while joining, or before a reconnect, goes out once the host reads signals. An older server ignores it, and clearing the name keeps the session's name until you join again. Tests: `collab/src/rename_tests.rs`, `collab-net/src/rename_tests.rs` (two apps over loopback), `ui-egui/src/display_name_tests.rs`, and `figcraft-web`'s `kept` round-trip. Numbers unchanged |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Components, variants, styles and variables parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and
|
||||
variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300
|
||||
@@ -71,13 +71,14 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`.
|
||||
| Explicit modes per frame, library collections' modes | done | |
|
||||
| Gradient stops and variant properties bound to variables | done | a stop's binding follows it when earlier stops are removed |
|
||||
| Extended collections | done | library collections, including a library's own extended collections (chains through libraries); overrides per mode with reset; follow library updates, variables new to the library collection being an update of their own; modes inherited. Checked with synthetic libraries only ([gaps](gaps.md) #14) |
|
||||
| Variables import/export (JSON) | done | the REST local variables shape (round-trips; merges by collection, mode and variable name; `variableCollections` and `variables` read under `meta` or at the top level, a warning when no variables are found or fewer than the collections list); CSS custom properties and Dart constants (Flutter) export; in the browser the export arrives as a download (import there still needs a file picker) |
|
||||
| Variables import/export (JSON) | done | the REST local variables shape (round-trips; merges by collection, mode and variable name; `variableCollections` and `variables` read under `meta` or at the top level, a warning when no variables are found or fewer than the collections list); CSS custom properties and Dart constants (Flutter) export; a variables file named by path must be a regular file of at most 256 MiB; in the browser the export arrives as a download (import there still needs a file picker) |
|
||||
| Expressions in prototype set-variable actions | done | see [prototyping-parity](prototyping-parity.md) |
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M8, #1114: variables import from an explicit file path (`variable.import`, the `variables` of `file.open` and `file.import`) takes regular files of at most 256 MiB, the limit the sibling `variables.json` already had; anything else is refused before the document changes. Grade unchanged. Tests: `import_export_tests::an_explicit_variables_path_is_capped_like_the_sibling_file`, `variables_io_tests::variables_import_caps_the_file_it_reads` |
|
||||
| 2026-10-30 | minor | M3, #1536 and #1537: component properties are listed in the order they were made in, and renaming one keeps its place (they were sorted by name, so a rename moved the property); that holds for every kind, on every variant of a set and on instances, in `component.properties`, the Design panel and `.fig` export. Files from before keep their properties by name until one is added or renamed. Make slot (the Slot menu of a frame inside a component) asks for the slot's name first: a field under the Slot row with the frame's name selected, Enter or Make slot to make it, Escape to cancel; a name taken keeps the field open. Renaming stays in the component's Properties list. Tests: `engine/src/prop_order_tests.rs`, `ui-egui/src/make_slot_naming_tests.rs`. Left: dragging the shared properties to reorder them, and renaming a slot with only its frame selected. Numbers unchanged |
|
||||
| 2026-10-29 | minor | M3, #1338: the instance-swap property's menu (on an instance, its main component's default and the component playground) and the instance's swap menu open with a search field at the top that has the keyboard; typing filters the components by name (any case, headings without matches drop out), an empty result reads "No matching components", Up and Down step through the matches and Enter picks one (the first without a step), Escape clears the search and then closes the menu. Only the list scrolls; the menu grows back when the search is cleared. Tests: `component_swap_search_tests`. Numbers unchanged |
|
||||
| 2026-10-26 | minor | An instance-swap property's default menu scrolls (#1338): on the main component's Properties row the menu listed every component in the file at full height, so in a file with many (an icon library) the bottom ones ran off the screen and couldn't be picked. It now scrolls past 320 pt like the instance's swap menu, is at least 200 pt wide, ticks the current default and is named for assistive technology (`component_swap_default_tests`). Left: searching these menus. Numbers unchanged. |
|
||||
|
||||
@@ -211,7 +211,8 @@ returns one level of children.
|
||||
UI state: panels and tabs (`panels`: the `left` and `right` panels' widths as asked for and whether the left one is closed, `leftCollapsed`), the cursor the canvas shows (`cursor`: a CSS cursor name such as `default`, `crosshair`, `ew-resize`, `grab`, `copy`, `not-allowed`, `zoom-in`, or one FigCraft draws: `rotate <degrees>`, `pen`, `pen-close`, `pen-add`, `pen-bend`, `eyedropper`, `comment`; `null` while the pointer is elsewhere or over something with a cursor of its own, a ruler guide or a gradient handle; the engine's own hint is `tool.overlay.cursor`), current tool (`tool`: its id, overlay hints and, for a selected
|
||||
ellipse, its arc handles `arcHandles`, for a selected rectangle, frame, polygon or star, its corner radius and ratio handles `shapeHandles`; see [mcp.md](mcp.md#measuring-the-scale-tool-arc-handles-and-selecting)), canvas rect, dialogs, view, timings (`frameMs`,
|
||||
`renderMs`), frames drawn since start or the last `ui.set {resetFrameStats: true}` (`framesDrawn`;
|
||||
an idle window draws none) with the most frequent repaint requests behind them (`repaintCauses`:
|
||||
an idle window draws none), how long building them took (`frameTimes`: `{frames, meanMs, maxMs}`, the
|
||||
UI pass before it is painted) and the most frequent repaint requests behind them (`repaintCauses`:
|
||||
`[{at: "file:line", count}]`, every request made in the frame before each one, delayed ones such as
|
||||
the autosave deadline included; egui's `context.rs` begin-pass line means input events) and what the last canvas render had to do (`canvas.renderWork`: `full`, `partial` with
|
||||
the `pixels` re-rendered after a pan or an edit, or `reused`), whether the canvas shows a coarse
|
||||
@@ -257,7 +258,7 @@ server dialogs are open (`joinDialog`, `publishDialog`; Escape closes them), the
|
||||
Share popover shows (`code`, with `share: {via, access}`; null when the file isn't shared with
|
||||
one), what is typed in the popover's and the home screen's join field (`joinField`), whether the
|
||||
popover has Local network chosen and Advanced open (`local`, `advanced`), and why the active
|
||||
document can't be edited now (`readOnly`: the "View only" or "Publishing" reason, else null); the
|
||||
document can't be edited now (`readOnly`: the "View only", "Can comment" or "Publishing" reason, else null); the
|
||||
session itself is `engine.execute {command: "collab.peers"}` (sharing with a join code, hosting,
|
||||
joining and publishing are `collab.share`, `collab.host`, `collab.join` and `collab.publish`, opening invite links `collab.registerLinks`,
|
||||
see [mcp.md](mcp.md#live-sessions)). The app also joins the invite link it
|
||||
@@ -368,8 +369,8 @@ frame or section id opens the Compare changes view, like `dev.compare {id, show:
|
||||
true}`; `null` closes it) with `devCompareVersion` (a version id to compare with; `null`: when it
|
||||
was marked ready for dev), `devCompareMode` (`sideBySide|overlay`) and `devCompareSplit` (the
|
||||
overlay slider, 0–1), `annotationFilter` (`all`, `none` for uncategorised, or a category id or name:
|
||||
the annotations the canvas shows). `resetFrameStats` (true: start `framesDrawn` and
|
||||
`repaintCauses` over). Returns `ui.inspect` (which reports `stylePicker`,
|
||||
the annotations the canvas shows). `resetFrameStats` (true: start `framesDrawn`,
|
||||
`frameTimes` and `repaintCauses` over). Returns `ui.inspect` (which reports `stylePicker`,
|
||||
`stylePickerView`, `comments`: `open`, `draft`, `filter`, `sort`, `search`, `editing` and the
|
||||
`mentions` the picker offers, `devCompare` (`{id, mode, split, version}` or `null`), `devFocus` (the frame the
|
||||
focus view shows, or `null`) and `annotationFilter`).
|
||||
|
||||
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-30 · **Change:** minor (the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); macOS: the Quit menu and ⌘Q ask about unsaved changes (#857); 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)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (one depth cap bounds a whole render, pattern sources included, and render threads get 8 MiB stacks (#1082); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); macOS: the Quit menu and ⌘Q ask about unsaved changes (#857); 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
|
||||
@@ -44,13 +44,15 @@ Nothing has been timed against Figma on the same document (needs `.fig` import).
|
||||
| Windows x64 / arm64 | desktop app | `.msi` and portable zip for x64, x86 and arm64 | built in CI; Windows: QA pass done 2026-10-11, findings in PR. Draws with Direct3D 12 (FXC shaders) or OpenGL; when the graphics fail before the first frame it starts again on Vulkan, then OpenGL, then the power-saving GPU with lower limits (#965) |
|
||||
| Linux | none (browser only) | AppImage, `.deb`, `.rpm`, Flatpak, tarball, x86_64 and aarch64; riscv64 tarball | CI tests run here. Clipboard (also the BSDs): reads run on a helper thread that gives up after 2 s, and images copied in other apps paste under Wayland through the data-control protocol (#693). Under Wayland the app can't tell its window is minimised, so control requests that need a drawn frame may still wait there (#720). None of this run on a Linux desktop here. On a Wayland compositor without server-side decorations (GNOME) the window gets winit's Adwaita controls (#592: checked by its author in a GNOME Wayland session; KDE not run) |
|
||||
| FreeBSD | none | tarball | |
|
||||
| Web | primary (figma.com) | `figcraft-web` (WASM, WebGPU/WebGL2), web zip | no accessibility tree; file access through the browser; copy and paste through the system clipboard (copies written from the copy event, pastes read from the paste event; no pasting images from other apps) |
|
||||
| Web | primary (figma.com) | `figcraft-web` (WASM, WebGPU/WebGL2), web zip | no accessibility tree; file access through the browser; copy and paste through the system clipboard (copies written from the copy event, pastes read from the paste event; no pasting images from other apps); an internal error stops the page (a panic can't be caught there), but its unsaved files are kept in the page's local storage (about 5 MB) and open again on reload (#1123) |
|
||||
| iOS / Android | apps: view, comment, mirror prototypes | none (the web build is not touch-first) | 20–40 h for a viewer |
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M8, #1082: canvas rendering of the deepest documents can't overflow a render thread: one depth cap bounds a render, pattern sources included, and render threads get 8 MiB stacks (a level takes about 6.5 KB). Numbers unchanged. |
|
||||
| 2026-10-30 | minor | M8, #1123: the browser can't catch a panic, so an internal error stopped the page and lost its unsaved changes. Its panic hook now keeps the files with unsaved changes in the page's local storage (as many as fit, about 5 MB) and says so on the page; reloading opens them again as unsaved files. Numbers unchanged. |
|
||||
| 2026-10-30 | minor | M1, #1320: settings in portable mode. The settings folder now comes from craft-settings, the suite's rules: `FIGCRAFT_CONFIG_DIR` names a folder (set but empty: none, nothing read or written); a `portable` file next to the executable (next to the `.app` on macOS) puts the preferences, recovery copies and installed plugins in `config/` beside it; otherwise they stay in `FigCraft` in the platform folder, where every earlier version kept them, so nothing moves for an installed app. The Windows portable zip now ships the `portable` file. A portable folder without preferences yet starts with a copy of the platform folder's `state.json` (owner-only, the original left for an installed app), so a portable copy that kept its settings in `%APPDATA%` before keeps them after the update. Checked on Windows 11 with a portable copy beside an installed copy's settings: the first start took them (its name and start screen), a name changed there was kept in `config\` only, and without the marker the app read the installed settings again. Tests: `settings_dir_tests`. Numbers unchanged |
|
||||
| 2026-10-30 | minor | M1, #965, #1438: "Parent device is lost" at start. Both reports lost the device while wgpu compiled its indirect-call validation shaders, which it builds with every device: on Windows Direct3D 12 took another program's `dxcompiler.dll` from the search path and failed without its `dxil.dll`; Intel Macs' Metal compiler aborts on them. Direct3D 12 now compiles with FXC (part of Windows; `WGPU_DX12_COMPILER` still chooses), and the validation shaders are left out (FigCraft draws nothing indirectly; `WGPU_VALIDATION_INDIRECT_CALL` turns them back on). When the graphics still fail before the window's first frame, the app starts itself again with the next attempt, from a fixed list: on Windows Direct3D 12/OpenGL, Vulkan, OpenGL, then the first again on the power-saving GPU with WebGL2-level device limits; on Linux and FreeBSD the defaults, OpenGL, then lower requirements; on macOS the defaults, then lower requirements. The last failure's message names what was tried; the log names the backend and GPU that drew. `WGPU_BACKEND` is still the one attempt. A restarted app waits up to 5 s for its `--control` port. Checked on Windows 11 with a local build that made the first one, two, three and then all four attempts fail: each fallback opened the window (Vulkan, OpenGL, Direct3D 12 on the integrated GPU) and four failures ended with the message. Not run on the reporters' machines or on macOS. Tests: `graphics_tests`, `startup_tests`. Numbers unchanged |
|
||||
| 2026-10-29 | minor | Large images on the canvas (#1483): an image shown smaller than its pixels draws on the canvas from a copy scaled down to 4096 or 8192 px on its longer side, the smallest that holds what the screen shows (`figcraft_render::images::shown`), cached beside the image itself in the same 512 MB budget. Two 16 000 × 10 000 images took 640 MB each and evicted each other on every frame that drew both; as copies they take 80 MB together zoomed out and 336 MB at 8192. Moving one over the other went from about 2.1 s a frame to 11 ms zoomed to fit and 15 ms at 30% in the app (debug build). Closer than that, and for tiles, the canvas draws from the image itself, so 100% and closer stay sharp; a view of both at 100% still takes about 2 s a frame, and an image with shadows still decodes in full for them. Exports, thumbnails and every other render are unchanged. Tests: `large_image_tests`. Numbers unchanged |
|
||||
|
||||
+29
-15
@@ -358,7 +358,7 @@ a later step can use `{"$step": 0, "field": "ids/0"}` to target the first match.
|
||||
| `select {ids}` | Replace the selection (`[]` deselects). |
|
||||
| `export {ids, format, scale \| size, path?, bleed?, cropMarks?, bleedMarks?, cmyk?, contentsOnly?, includeBoundingBox?, svgOutlineText?, svgIdAttribute?, svgSimplifyStroke?, vdCompat?}` | png, jpg, svg, webp, pdf, xml (an Android Vector Drawable: `svgIdAttribute` writes `android:name`, `vdCompat` is `bestEffort` (default: approximated with a warning), `exact` (left out with a warning) or `ignore` (no warnings: overlapping layer opacity dropped, even-odd fills kept as `fillType="evenOdd"`, diamond gradients left out), and the result lists `warnings` for what was changed or left out), 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, XML as text followed by `{format, bytes, warnings}`. 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?, reload?}` / `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 gives its `index`, with `stale: true` when the file changed on disk since that tab read or saved it; `reload: true` reads it again into the tab, refused while the tab has unsaved changes) and the document's `document` outline. |
|
||||
| `open_file {path, variables?, reload?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` or FigJam `.jam` (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 gives its `index`, with `stale: true` when the file changed on disk since that tab read or saved it; `reload: true` reads it again into the tab, refused while the tab has unsaved changes) 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. |
|
||||
@@ -1162,7 +1162,8 @@ default mode's named after the collection and the others after the collection an
|
||||
`static const` `Color`, `double`, `String` or `bool` per variable; names become camelCase Dart
|
||||
identifiers kept apart, aliases are resolved to their value in that mode), and
|
||||
returns `{text}` (or writes `path` and returns `{path}`). `variable.import {variables}` (an object,
|
||||
JSON text or a path; `variableCollections` and `variables` under `meta` or at the top level) reads
|
||||
JSON text or a path to a regular file of at most 256 MiB;
|
||||
`variableCollections` and `variables` under `meta` or at the top level) reads
|
||||
one back: a collection joins this file's collection of the same name
|
||||
(values matched by mode name, new modes and variables added, a same-named variable of another type
|
||||
skipped with a warning; library copies and extended collections are never targets), others are
|
||||
@@ -1203,22 +1204,29 @@ and read state are its own, it can edit and delete only its own messages, and it
|
||||
Preferences is unchanged. It is set once per connection, so no command needs `author`; an `author`
|
||||
param still wins. In the app, comments act as the name from Preferences (`pref.userName {name}`,
|
||||
default "You"); only a message's author can edit or delete it. `@Name` in a message mentions
|
||||
someone (`comment.people` lists the names used in the file and, as `you`, who is acting).
|
||||
someone (`comment.people` lists the names used in the file: everyone who wrote or reacted, and
|
||||
every name a message @mentions, once each in any case; and, as `you`, who is acting).
|
||||
|
||||
Read state is kept per name: a new message is unread for everyone but its author;
|
||||
`comment.markRead {id? | link?, read?}` marks one thread (or, without `id`, all of them) read or
|
||||
unread (not an undo step). `comment.list {page? | "all", filter? (all|open|resolved|mine|unread),
|
||||
sort? (date|page), query?, frame?}` returns the threads with their position, `frame`/`frameName`,
|
||||
sort? (date|page), query?, frame?, mentions?}` returns the threads with their position, `frame`/`frameName`,
|
||||
`pageName`, `unread`, `link` and replies (with `edited` times and `reactions`). Each message, the
|
||||
first and every reply, has `mentions`: the names it @mentions by the rule the app highlights them
|
||||
with, `{name, known, you}` each. `known` means the file's people (`comment.people`, or you) have
|
||||
the name, spelt as they spell it; `you` means it names you (or `author`). `@Name` counts at the
|
||||
start of the text or after a character that isn't a letter or digit, so `a@b.com` mentions nobody;
|
||||
an unknown name is the letters and digits after the `@`. Links are
|
||||
an unknown name is the letters and digits after the `@`. `mentions: "<name>"` (any case; a leading
|
||||
`@` is fine) lists only the threads where a message, the first or a reply, mentions that name as
|
||||
those `mentions` report it, combined with the other filters: `{"mentions": "Claude", "filter":
|
||||
"unread"}` is the unread threads that tag Claude. `@Ada Lovelace` mentions "Ada Lovelace", not
|
||||
"Ada", when the file knows Ada Lovelace; a name nobody in the file has can still be several words
|
||||
(`"Zoe Smith"` finds `@Zoe Smith`). Links are
|
||||
`figcraft://comment/<id>`: `comment.link {id}` returns one, `comment.open {id | link, view?}` goes
|
||||
to its page, zooms to its frame (`fit`, the default; `center` keeps the zoom; `keep` doesn't move)
|
||||
and opens it in the app, and `comment.next {id?, dir?, …list params}` steps to the next or previous
|
||||
thread of a list the same way. `comment.zoom {ids}` zooms in on a group of pins. Comment ids made in
|
||||
and opens it in the app, and `comment.next {id?, dir?, …list params}` (`mentions` too) steps to the
|
||||
next or previous thread of a list the same way. Both return the thread as `comment.list` lists it,
|
||||
with each message's `mentions`. `comment.zoom {ids}` zooms in on a group of pins. Comment ids made in
|
||||
a live session are large numbers (their creator's id space): pass them back as given.
|
||||
|
||||
An agent can read every open thread as one resource (`figcraft://comments`) or call
|
||||
@@ -1226,8 +1234,9 @@ An agent can read every open thread as one resource (`figcraft://comments`) or c
|
||||
query?, includeResolved?}` returns `{count, truncated, text, comments}`, where `text` is a Markdown
|
||||
task brief with the document, each thread's page, frame, anchor, point, messages, replies and link,
|
||||
and the instructions to fix, reply and resolve. `comments` is bounded metadata for at most 200 of
|
||||
the selected threads (id, page, frame, anchor, status, link, author, preview and reply count), not
|
||||
the full threads: use `comment.list` for those. The Comments panel's **Copy for AI** action uses
|
||||
the selected threads (id, page, frame, anchor, status, link, author, preview, reply count and
|
||||
`mentions`: the names the whole thread @mentions, as `comment.list` reports them per message, at most
|
||||
32 with names cut to 80 characters), not the full threads: use `comment.list` for those. The Comments panel's **Copy for AI** action uses
|
||||
it. The `fix_comments` prompt runs the review loop: inspect the anchored layer, make the
|
||||
smallest change (one `batch` per thread), reply with what changed, verify with a screenshot, then
|
||||
resolve. Reply before resolving: `comment.reply` reopens a thread, so resolving first and replying
|
||||
@@ -1372,10 +1381,12 @@ only: a host stops hosting it, a guest leaves it.
|
||||
FigCraft server unless changed; `{url: ""}` turns online sharing off, `{}` goes back to the
|
||||
built-in one; `server` names another for this call, with `?key=…` when it needs its key) and
|
||||
returns `status: "connecting"`; once `collab.peers` says `"live"`, `code` is the code to give
|
||||
others (`K7M-2QX-9TD`) and `share` is `{via, access, shared, editCode, viewCode, editLink,
|
||||
viewLink}`: links and codes made for others (`published.edit` / `published.view` and `link` are
|
||||
others (`K7M-2QX-9TD`) and `share` is `{via, access, shared, editCode, viewCode, commentCode, editLink,
|
||||
viewLink, commentLink}`: links and codes made for others (`published.edit` / `published.view` and `link` are
|
||||
your own way in; keep them). `access: "view"` (or calling again with it while shared) makes
|
||||
`code` the view-only one. A server older than join codes leaves `code` null: give
|
||||
`code` the view-only one, and `access: "comment"` the one for viewing and commenting (whoever
|
||||
joins with it adds, replies to, reacts to and resolves comments but can't change the design;
|
||||
`commentCode` is null from a server older than comment links, and asking for it then fails). A server older than join codes leaves `code` null: give
|
||||
`share.editLink` / `share.viewLink` instead. `via: "local"`
|
||||
hosts the document for people on the same network and returns at once with a longer `code`
|
||||
(20 symbols, carrying this machine's IPv4 address, the port and the session's secret; `bind:
|
||||
@@ -1420,6 +1431,8 @@ only: a host stops hosting it, a guest leaves it.
|
||||
file, name, edit, view}` with the file's first edit link and view link (they carry their
|
||||
tokens: share them like passwords). Someone joining with the view link gets `access: "view"`:
|
||||
every edit on the synced document is refused with "View only: …" (the server refuses them too).
|
||||
With a comment link (`access: "comment"`) the comment commands work and sync, and every other edit
|
||||
is refused with "Can comment: …".
|
||||
On a server open to the public ([`server.md`](server.md#running-a-public-server)) `server` needs no
|
||||
key (`wss://figcraft.example.com/`): `published` then also has `owner` (the owner token, shown this
|
||||
once: keep it) and `expires` (when the server deletes the file unless someone opens it, seconds
|
||||
@@ -1480,10 +1493,11 @@ only: a host stops hosting it, a guest leaves it.
|
||||
and the rest is null or empty), status
|
||||
(off|connecting|live|reconnecting|failed), reason?,
|
||||
attempt?, role (host|guest), me, name, color, fileName, pages, link, url, address, network,
|
||||
access (edit|view), readOnly (the active document can't be edited now: view only, or being
|
||||
published), published (a file you published: {server, file, name, edit, view, owner, expires}),
|
||||
access (edit|view|comment), readOnly (the active document can't be edited now: view only,
|
||||
comment only, or being published), published (a file you published: {server, file, name, edit, view, owner, expires}),
|
||||
code (the join code to give others when the document was shared with `collab.share`, else
|
||||
null), share ({via: online|local, access, shared, editCode, viewCode, editLink, viewLink}, or
|
||||
null), share ({via: online|local, access, shared, editCode, viewCode, commentCode, editLink, viewLink,
|
||||
commentLink}, or
|
||||
null; `shared: false`: your file on a server, not shared now),
|
||||
hosting (what the server of the file you're in says: {open, operator, terms, idleDays, maxFileMb,
|
||||
expires}, or null),
|
||||
|
||||
+22
-2
File diff suppressed because one or more lines are too long
+16
-6
@@ -55,7 +55,8 @@ prints the address to publish to, `ws://<host>:47900/?key=<server key>`. In the
|
||||
**File ▸ Publish to server…** (or **Share ▸ Advanced ▸ Publish to another server…**) asks for that address and the
|
||||
file's name. The open file is uploaded as a new file and the tab keeps syncing through the
|
||||
server; until the upload is done the tab is read-only ("Publishing…" in the toolbar). The Share
|
||||
popover then shows an **edit link** and a **view link** to copy. Agents do the same with
|
||||
popover then shows an **edit link** and a **view link** to copy (sharing with a join code also makes a
|
||||
**comment link**, see below). Agents do the same with
|
||||
`collab.publish {server, name?}` ([`mcp.md`](mcp.md#live-sessions)).
|
||||
|
||||
Whoever has the server key can publish files and manage every file's links: give it only to
|
||||
@@ -181,7 +182,12 @@ app's invite links and join the same way (the same **Join with a code** field ta
|
||||
The file opens as a new tab. With a **view link** you see everyone's changes and presence, and
|
||||
others see your cursor and selection, but you can't change anything: the toolbar shows a **View
|
||||
only** badge with only the Move and Hand tools, edits are refused with a message (and the server
|
||||
refuses them too). **Leave session** keeps the file open as your own copy, editable again.
|
||||
refuses them too). A **comment link** is a view link that also lets you comment: the badge reads
|
||||
**Can comment**, the Comment tool is there beside Move and Hand, and you add, reply to, react to
|
||||
and resolve comment threads; any other edit is refused, by the app and by the server, which takes a
|
||||
comment link's batch only when every change in it touches comment threads. An app from before
|
||||
comment links joins with one as a viewer. **Leave session** keeps the file open as your own copy,
|
||||
editable again.
|
||||
|
||||
## Team libraries
|
||||
|
||||
@@ -257,7 +263,7 @@ port as everything else; no other service is involved.
|
||||
| `figcraft-server files [list] [--sort size\|age\|idle]` | The files: id, name, people in it now, size, created, days unused, expiry, reports, and who published it (`key`, or the address of a file published without it); biggest, oldest or longest unused first |
|
||||
| `figcraft-server files remove FILE` | Remove a file and its links for good (whoever is in it is disconnected) |
|
||||
| `figcraft-server links list FILE` | A file's links: id, access, created, label |
|
||||
| `figcraft-server links create FILE --access edit\|view [--label TEXT]` | A new link, printed once |
|
||||
| `figcraft-server links create FILE --access edit\|comment\|view [--label TEXT]` | A new link, printed once |
|
||||
| `figcraft-server links revoke FILE LINK` | Revoke a link: it no longer joins, and whoever joined with it is disconnected |
|
||||
| `figcraft-server plugins [list]` | The plugin directory: every plugin, its versions, author, access, size and SHA-256 |
|
||||
| `figcraft-server plugins remove ID [--version V]` | Unpublish a version of a plugin, or every version |
|
||||
@@ -331,7 +337,8 @@ behaves as before (blocked addresses, report rates and the limits on
|
||||
```
|
||||
|
||||
A file published without the key also has, in its `meta.json`, the SHA-256 of its owner token,
|
||||
the address it came from and when it was last opened (for expiry).
|
||||
the address it came from and when it was last opened (for expiry; written at most hourly while the
|
||||
file is used, and when the server stops).
|
||||
|
||||
- **Writes are atomic**: a snapshot, a description or a link list is written to a temporary file
|
||||
beside it, flushed, then renamed over the old one. The log is only ever appended to, and synced
|
||||
@@ -341,6 +348,8 @@ the address it came from and when it was last opened (for expiry).
|
||||
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.
|
||||
- **A file everyone left stays loaded for a minute** (at most 16 such files, 256 MiB together),
|
||||
its log already synced: someone joining in that time gets it without anything being read again.
|
||||
- **Damage is skipped and reported, never deleted.** At startup a folder whose description, links
|
||||
or snapshot can't be read is left out and logged; a log that ends in a cut-short or damaged
|
||||
record loses only that end (logged when the file opens). Leftover temporary files are removed.
|
||||
@@ -459,8 +468,9 @@ What someone can do, by what they hold (the server is reached through TLS at the
|
||||
| Who | Can | Cannot |
|
||||
|---|---|---|
|
||||
| Anyone who reaches the server, with nothing | Ask about its hosting (open, operator, terms, limits); on an open server publish files within the limits per address and for everyone; list and download the plugin directory; ask `/healthz`; try join codes, within the limits on wrong ones | Join, read or list any file; see links, reports or blocked addresses; use or learn the server key or anyone's token (none are ever sent back, and all are compared in constant time against hashes); publish plugins; spoof their address through `X-Forwarded-For` (believed only from `trusted_proxy`); get past a block |
|
||||
| Someone with a **join code** | Exactly what its link gives (a view code: the view link's row; an edit code: the edit link's), until the code or its link is gone | Get a code for any other link; learn anything from a wrong code but that it is wrong |
|
||||
| Someone with a **join code** | Exactly what its link gives (a view code: the view link's row; a comment code: the comment link's; an edit code: the edit link's), until the code or its link is gone | Get a code for any other link; learn anything from a wrong code but that it is wrong |
|
||||
| Someone with a file's **view link** | See that file and everyone's changes and presence in it; read its team library and usage; report it | Change it (the server refuses a viewer's edits); reach any other file; manage links |
|
||||
| Someone with a file's **comment link** | What a view link gives, and add, reply to, react to, resolve, move and remove that file's comment threads (anyone's: comments are signed with the name in Preferences, there are no accounts) | Change anything else in it (the server refuses a batch that touches more than comment threads); reach any other file; manage links |
|
||||
| Someone with a file's **edit link** | Change that file, within its size cap; publish its team library (on a public server only for files published with the key); report it | Reach any other file; grow it past `max_file_mb` or its publisher's share; list, make or revoke links; rename or remove it |
|
||||
| Whoever holds a file's **owner token** (published without the key) | List, make and revoke that file's links (disconnecting whoever joined with a revoked one, which is how the app stops sharing), ask for join codes for links whose token they hold, rename it on the server, remove it; ask whether it is still there, its name, link ids and expiry (asking doesn't count as opening it) | Touch any other file, even with its id; tell a file that doesn't exist from one they don't own; list files; read reports; block; anything of the plugin directory |
|
||||
| The **operator** (server key, or the machine) | Everything above for every file, reports, blocks, the plugin directory; read every file on disk | Recover a lost link or owner token (only hashes are kept) |
|
||||
@@ -738,7 +748,7 @@ sudo systemctl start figcraft-server
|
||||
(128-bit) per file published without the key, or the server key (128-bit) for managing files.
|
||||
Tokens are stored as SHA-256 hashes and every comparison runs in constant time; revoking a link
|
||||
disconnects whoever joined with it. A view link's batches are refused by the server whatever the
|
||||
client does.
|
||||
client does, and so are a comment link's that change more than comment threads.
|
||||
- **Join codes** are 45 random bits naming one link: far easier to guess than a link, so every
|
||||
server limits wrong codes per address and for everyone
|
||||
([Sharing with a join code](#sharing-with-a-join-code)); with those limits, finding any one of
|
||||
|
||||
+9
-5
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user