* M6: .fig export writes a page's prototype background (#2326)
The export writes a page's prototype background as prototypeBackgroundColor (field 141, a plain colour) when it differs from the default, as Figma does for a page whose background was set. The device-settings warning now fires only for the device, rotation, colour, custom size and responsive settings. Measured on a synthetic file saved from Figma's web app.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 measured and wrote at medium
* M6: the .fig export writes a prototype background opaque and warns for its opacity, and its tests read the written record (#2326)
The colour is written with alpha 1, as the measured record has it and as the import reads it, and a background that isn't opaque warns that its opacity was left out. The tests now read the written record, and each device setting alone is checked.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 reviewed; Sonnet 5.5 wrote at medium
* M6: the .fig export writes every page's prototype background, so Figma shows the same colour (#2326)
A page at FigCraft's default background used to write no field, so Figma showed its own default instead. Every page now writes its colour, opaque. The test for a default page reads the written record and the import.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Shift+Cmd/Ctrl+V pasted into the selected layer's parent at its top-left,
but appended the copies, so they went to the top of the parent's layers
whatever was selected. In an auto layout frame that put them at the end of
the flow, away from the layer they were pasted over.
They now go just above the selected layer, in the copied order: in the
Layers panel the row above it, in an auto layout frame the item after it.
A section pasted over a layer in a frame still goes on the page, on top
of its layers, as before. Paste to replace already kept the replaced
layer's place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
board.mindMap's default x and y came from the view's centre, which is on the page, but x and y are the root's place in its parent: under a section at (1000, 500) the map landed 1000 right and 500 down of the view's centre. The default now takes the centre through the parent's inverse world transform (a parent that can't be inverted gives a non-finite centre, which the coordinate check refuses). Explicit x and y stay parent-local. Tests: two in board_mindmap_tests (a section; a frame turned 30 degrees, with a given place carried through the frame).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
* M6: .fig export writes list spacing, hanging punctuation and lists, and vertical trim (#2305)
The export wrote none of a text's list spacing, hanging punctuation, hanging lists or vertical trim, although the import reads all four. It now writes listSpacing, hangingPunctuation, hangingList and leadingTrim, each only when it differs from its default (as Figma saves them), and an instance override back to the default clears the field. The warning narrows to OpenType features, which the import does not read from a .fig.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 briefed; Sonnet 5.5 wrote at medium
* M6: test that an instance override back to the default clears it (#2305)
Adds a round-trip test for the unset entries: an instance whose text goes back to the defaults must not take the main's list spacing, hanging flags and vertical trim again. Also scopes the comment and the docs row to a design file: a text at the defaults carries none of the four there, and a text style holds none of them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 briefed; Sonnet 5.5 wrote at medium
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Pages take a variable mode, set from the Page section of the right sidebar once a local collection has more than one mode. With nothing selected the Page section now shows the same Variable modes section the frames have: a row per multi-mode collection, Auto or one of its modes, one undo step per pick (variable.setMode without layers sets the page's mode). The frame's row drawing is shared, not copied.
Model-Effort: Opus 5.5 drove; Fable 5.2 queued; Opus 5.5 briefed; Sonnet 5.5 measured and wrote at medium
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
inspect.code {language: "tailwind", mobile} dropped every mobile max-width and put the desktop one behind lg:, so a fixed-size layer that is the same size on both frames (a 48 x 48 badge) stretched to the full width below 1024 px. A mobile max-width that equals the desktop one is now kept as a plain class, with no lg: copy. A differing max-width still goes fluid on mobile with the desktop cap behind lg:.
Tests: the issue's two frames (a 1440 and a 390 page, each with a 48 x 48 badge) keep max-w-[48px] unprefixed, and a page-wide banner still gets its lg: cap only. The slashed-names test gave the CTAs frame the same 342 px on both frames, which the new rule rightly keeps, so its desktop frame is now 600 px wide. docs/mcp.md notes the rule.
Model-Effort: Opus 5.5 drove; Fable 5.2 queued; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* M8: Tab while renaming the last layer wraps to the first, as in Figma
Measured in Figma's web app on 2026-11-12: Tab on the last row of the Layers panel commits the name and opens the first row's rename. FigCraft ended the rename there. Shift+Tab on the first row is unmeasured and unchanged, and Tab still moves the selection with the rename.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 queued; Sonnet 5.5 measured and wrote at medium
* M8: Tab while renaming moves the rename, not the selection, as in Figma
Measured in Figma's web app (second run, 2026-11-12): while renaming, Tab and Shift+Tab open the next or previous row in edit, and the selection stays on the layer first renamed. FigCraft moved the selection to the renamed row. Tab now moves only the rename field.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 queued; Sonnet 5.5 measured and wrote at medium
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/geom/src/transform.rs:58 (round_to), used by crates/engine/src/automation/inspect.rs:517 (round)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Rust 1.100 marks `ExitCode` as must-use and extends `needless_bool`,
so `cargo clippy --workspace --all-targets -- -D warnings` fails on main:
- `crates/ui-egui/src/shortcuts.rs`: the end of `key_event` returns the
condition directly instead of `if ... { return true; } false`.
- `apps/figcraft-cli/src/files.rs`: three script tests now assert the exit
code that `run_script` returns (`SUCCESS` for the two scripts that run,
1 for the script whose step fails) instead of dropping it.
flutter_line skipped `spacing:` when the main-axis alignment string
contained "Space", but the strings are MainAxisAlignment.spaceBetween,
spaceAround and spaceEvenly (lower-case s), so it never matched and the
gap was written on top of the distribution. It now checks the MainAlign
value itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote; Fable 5.2 spotted it
inspect.code wrote one `background` per visible fill, so only the last survived. A non-text layer now gets one `background` with every visible fill as a layer, top fill first (a document's fills run bottom to top), joined with ", ". A solid above another fill is `linear-gradient(0deg, c, c)` since CSS allows a plain colour only as the last layer; the bottom solid stays a plain colour (and keeps its variable token); gradients, images and patterns keep their layers. A single fill is unchanged. Text keeps one `color` per fill. Tailwind emits a layered value as one `[background:...]` class. Native targets are unchanged.
Tests: several_fills_are_one_layered_background_top_first, one_solid_fill_stays_a_plain_colour, several_fills_are_one_layered_background_class.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 queued; Opus 5.5 briefed; Sonnet 5.5 measured and wrote at medium
The engine test now follows the issue: a list typed at 5, saved as .fig and opened, split by Backspace at the start of "b". The REST fixture is a top-level list, the junk sweep reaches the new loop through a listed text layer, and the document crate tests the new rule for nested, bullet, plain and over-deep levels. ParaList gets its doc comment back, and the revision row names the item written with 0.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 queued; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
Figma writes a list's start on every line of the list (measured in Figma's web app, 2026-11-12, from a saved .fig: a list started at 5 has listStartOffset 4 on all its lines, and the item split off by Backspace is written with 0). The import kept every line's value, so after a split the items after it restarted at the old start instead of 1.
The import now keeps a start only on the item where a numbered list starts counting, and gives every item that goes on counting 0, for .fig and REST listStartOffsets alike. The rule is one function in the document model, list_item_starts_counting, which the layout's list numbering now uses too. The export is unchanged and its re-import now equals what went in.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 queued; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
The popover's 24 pt close button painted its cross through rect.shrink(6.0), a 12 pt glyph; it now goes through icon_glyph_rect like the other 24 pt buttons, with a painted-size test that failed before the change. The style picker's menu_icon picked the 16 pt glyph with an exact size equality, so a button at a fractional x that measured a hair off 24 in f32 painted at 14 pt; it now allows 0.5 pt of rounding.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
Measured in FigJam's web app on 2026-11-12: a sticky's own toolbar has a text-size menu (Small 16, Medium 24, Large 40, Extra large 64, Huge 96), a fresh sticky is Small = 16 (the 18 of 2026-11-09 did not reproduce), the note grows taller at Huge and shrinks back, a size chosen with two stickies selected sets both, and the sticky tool remembers the last size.
STICKY_TEXT_SIZE is 16 and STICKY_SIZES holds the table. board.setStyle {fontSize} sizes stickies' notes and sets the sticky tool's size (BoardTool.sticky_size); board.sticky takes the tool's size or a fontSize (1 to 1000). The contextual bar shows a size menu (the current name or number, a check on the current entry) when only stickies carry text; shapes keep S/M/L/XL. Docs: boards-parity rows, revision history and status line.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium