- The entry size cap is figcraft_doc::ImageData::MAX_BYTES (256 MiB), the
cap the .fig, .sketch and REST importers already read images with (now
one constant), instead of 32 MiB: an imported image larger than 32 MiB
made the document unjoinable and its server snapshot unrestorable.
- History written before the checks (a restored or joined snapshot, a
replayed server log, a file published to a server) applies without the
image entries they refuse (docprops::without_refused_images), instead of
the file not opening or a restart dropping every edit after the image.
- A key that already holds an image can't be given other bytes: producers
key images by their bytes (the REST importer's placeholder keys always
hold the same picture), and merge treats a key's image as never
changing.
- REST import: an unresolved imageRef shaped like a content hash shares
the rest-image-missing placeholder (the placeholder's bytes don't hash to
it); ImageData::claims_content_hash is shared with collab.
- Tests: collab image_tests (4 new, size test uses a small cap), format
image_tests (1 new).
- Docs: collab-protocol.md, collaboration-parity.md, file-format-parity.md.
Image entries applied bytes to any key with no checks (audit COL-9): a
collaborator could poison the shared content-addressed image store, claim
absurd dimensions, balloon memory, or name an image `../../x`, which a
saved file then wrote as the archive member `images/../../x`.
- Every key must be 1–128 ASCII letters, digits, `-` or `_`:
`figcraft_doc::ImageData::is_valid_key`, promoted from the format
crate's private `is_valid_hash` so the file reader and collab share one
rule. The reader now also takes `-` and `_`, so the REST importer's
`rest-image-missing` placeholder survives a save and load.
- The REST importer's side of the same attack: an unresolved `imageRef`
that can't legitimately be an archive member name (path text, an
over-long ref) shares the `rest-image-missing` placeholder instead of
being keyed by its raw name
(`image_tests::a_path_like_rest_image_ref_never_names_an_archive_member`).
- A key with the shape of a content hash (64 hex chars) must be exactly
the lowercase sha256 of the bytes, the form `image_hash` writes; an
uppercase look-alike is refused. Other keys (svg FNV-128, REST
placeholders) claim no content address and only meet the rules above.
- A side is at most 65 535 px; decoded bytes at most 32 MiB, with the
base64 text capped before decoding. 0×0 stays valid: the format crate
stores it for sizes it can't probe.
- Refusals are Error::Invalid and reject their batch, so every replica's
store stays clean; catch-up snapshots apply through the same check.
- Docs: collab-protocol.md, collaboration-parity.md and
file-format-parity.md (rows, status lines, histories).
- Tests: collab image_tests (8), format image_tests: round-trip and the
REST placeholder gate.
The 0.12.0 release's Web job failed at 25,319,327 bytes against the 24 MiB (25,165,824-byte) gate. opt-level z, fat LTO and wasm-opt -Oz are already in place and extra wasm-opt passes save only ~20 KB, so the gate moves to 25,690,112 bytes, still under Cloudflare's 25 MiB per-file cap. The wasm grew ~2 MB in three days (egui 0.37, new features); bringing it back down is follow-up work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The last line shown runs on into the word wrapped after it, so the ellipsis
fills the line ("Lorem consec…") instead of following the last whole word.
- An explicit height (node.set, transform.resize, a resize handle, fixed
sizing) on truncating text keeps it truncating (TextData::fix_size).
- Max lines on auto-height text hugs up to that many lines and ends in an
ellipsis; text.maxLines keeps auto height, Type settings show it as
truncating, .fig export writes textTruncation and import keeps auto height.
- node.get reports maxLines; text.fit measures every line.
Co-authored-by: Cursor <cursoragent@cursor.com>
With a file open in the desktop app, figcraft-cli could change it on disk;
the next Save (or auto-save) wrote the app's copy over it and said
"Saved", losing the CLI's changes.
Each tab now remembers its file's size and modification time when it
opens, reverts or saves it (the Stamp library files already use), and
Save and auto-save refuse while the file on disk differs, pointing to
Revert to saved or Save as. Scripts pass `file.save {overwrite: true}`.
Save as and saves to an explicit path are unchanged.
On a main component's Properties row, the default of an instance-swap
property opened a menu of every component in the file at full height, so
in a file with many components (an icon library) the bottom ones ran off
the screen and couldn't be picked. The menu 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.
Branches of a file in a live session: create, edit, review, update from
main and merge while the file is in a session. The branch stays with its
author and makes ids in their own client space far above the main file's;
merging is one synced edit and one undo step; peers' edits merge against
the branch's own base and conflicts resolve as offline; a view link still
refuses merging; a branch made outside the session that adds layers is
refused with a reason. Branch history entries for creation and updates
from main. Fuzz and junk tests extended to export settings, guides and the
live case. Docs: gap 17, collaboration parity, ROADMAP, mcp, protocol.
* M8: widgets take text input and a property menu
Plugin API 4 (gap 6). A widget's text layer can be an editable field:
a double-click edits it like any text, and when editing ends the value
(cleaned, at most 1,000 characters) goes into the widget's state under
the field's name and its plugin redraws, as one undo step. A widget can
also carry a property menu of up to 8 actions, toggles and colour
choices, shown in a small bar over it while it's selected.
- widget.setText and widget.menu drive the same paths from scripts,
the CLI and MCP; widget.insert/list also report fields and menu
- descriptions, values and stored menus are validated as untrusted;
an API 3 plugin drawing a field or menu is refused, older hosts
refuse API 4 manifests; no new host imports
- SDK reads the new events; the Counter example gains a label field
and a menu (reset, steps of ten, a colour)
- tests for fields, menus, canvas editing, rollback, forged data and
live sync; junk sweep extended; labels in every command catalog
- docs: plugins, mcp, gaps, target-app-parity, ROADMAP, roadmap
* M8: widget property menu on egui 0.37
Id::new and Ui::id are deprecated in egui 0.37: the widget bar's area uses Id::unique and its buttons' interaction ids come from ui.scope_id().
* M11: a double-click a board object or a widget field takes, in a function of its own (Sonar)
* M12: board templates and tables
Five built-in board templates (retrospective, brainstorm, weekly planner,
flow chart, kanban board) placed as ordinary board objects in one undo step
(board.template, board.templates) from a toolbar picker with pictures, and
board tables made of auto layout frames (board.table, board.tableEdit; up to
50 x 20), typed into with a double-click and edited by row and column from
the bar over the selection. Docs: boards-parity, gap 24, ROADMAP, mcp.
* M12: board templates picker on egui 0.37
Id::new and Ui::id are deprecated in egui 0.37: the templates picker's area uses Id::unique, its buttons' interaction ids come from ui.scope_id(), and its test reads the board bar's area by Id::unique.
* M12: board timer and voting
A countdown everyone on a board sees (board.timer, board.timerState), synced as its end time or the
time left while paused, and voting sessions (board.vote, board.voteResults): votes per person, on
everything, stickies or the selection; clicks vote, your badge takes a vote back; your own votes
show while it runs, everyone's with a ranked list once it ends. Timer and vote sync as document
properties and ballots as one entry per voter; neither is an undo step. A pill at the top right
of the board, new Timer, Pause and Vote icons, labels and strings in every catalog, docs.
* M12: board timer and voting on egui 0.37
Id::new is deprecated in egui 0.37: the timer and voting area and its memory ids use Id::unique.
* M12: board.vote's start and ballot changes in functions of their own (Sonar)
* M12: boards dot grid, re-attaching connector ends, curved connectors
Board pages draw a dot grid in the canvas render, after the page colour
and under the layers (figcraft_render::Dots), thinned out when zoomed out
and coloured from the theme's ink against the page. A selected connector
shows its ends as handles; dragging one onto an object re-attaches it,
onto empty canvas leaves it free (board.reattach, one undo step, also
over MCP). Connectors can be curved, and elbows go around the objects
they join when the short way would cross them.
* M12: boards dot grid: blending a dot in a function of its own (Sonar)