**A styles and variables behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule (what a style holds, creating from a selection, applying, editing, detaching, deleting a used style, organising by folders and order, descriptions; variable types, modes and the default mode, aliases, scopes, mode inheritance, bindings by field and type, deleting a bound variable, code syntax, number variables in pixels) is a table-driven case with a one-line derivation; where the documentation is silent the case says so and pins the behaviour.
**Fixes (10 of the suite's 18 tests failed on the old code, counting the missing settings below)**
- **Editing a styled property unlinks that style**: a fill, stroke, effect or layout grid list edited away from its style (through `node.set`, a paint row, an added fill, or a variable bound to it) drops the link, as text chars already did. Before, the layer kept a stale link and the next style edit silently overwrote the user's edit. Moving or renaming a layer keeps its links.
- **Applying a style replaces variables bound to what it sets** (the fill list for a paint style, the typography for a text style); other bindings stay. Before, the variable won again on the next edit.
- **Text styles go on text layers only**: applying one to a mixed selection leaves the other layers alone (they used to get a stray text style link); a selection without text is refused.
- **Deleting a bound variable unbinds the layers using it**: they keep the value they show; variables aliasing it take its values per mode (docs silent on aliases). Before, the binding dangled. `variable.deleteCollection` now does the same for aliases from other collections into it.
- **Bindings are type-checked**: a variable of a type the field doesn't take (a colour on `width`, a number on `visible`) is refused; scopes still only filter pickers. Number variables can fill text content (shown as text).
- **Aliases are checked when a variable is created** (`variable.create {value: {alias}}` took any name, of any type).
- **Style names** are trimmed per `/` folder part; empty names are refused.
**Missing documented settings, added**
- **Organising styles**: `style.move {styles, before?, folder?}` reorders and moves into a folder (renaming the style to match), `style.duplicate` puts "<name> copy" right below the original, `style.renameFolder` renames a folder's styles. The page's **Local styles** list (nothing selected) is now grouped by type and folder with swatches and text sizes: click opens the style editor, right-click offers Edit style / Duplicate / Delete, a row dragged by its grip moves (into the folder it lands in), right-click on a folder renames it.
- **Modes**: Duplicate mode (`variable.addMode {from}`), Set as default and Move column left/right (`variable.moveMode {collection, mode, to}`); layers on Auto follow a new default, layers set to a mode keep theirs.
- **Code syntax** per platform (Web, iOS, Android): `variable.update {codeSyntax}`, three fields in Edit variable, carried by the variables JSON (import and export); generated CSS and the CSS variables export use the Web name (`var(--x)` or `--x`).
- **Number variable scopes**: font weight, paragraph spacing, paragraph indent and text content (and `fontWeight`, `paragraphSpacing`, `paragraphIndent` bind with their scopes).
New strings go through `tr(...)` with `en.toml` / `es.toml` entries, new commands have Spanish labels; the new rows, fields and menus have accessible names. Each edit stays one undo step.
Library collections can be extended in a consuming file (inherited modes, per-mode overrides with reset, extra variables; library updates keep overrides). Variables export to the REST JSON shape or CSS custom properties and import back (merged by name, validated by the REST reader, one undo step). Gradient stops and variant properties bind to variables. Also fixes clicking a gradient stop adding a new stop.
- Expressions as text: Expr::parse (variables by name, literals, and/or/not, comparisons,
arithmetic, text joins) with plain-English errors, type checks for conditions and set variable
values; display reads back as the same expression.
- New commands proto.action.add/set/remove/move and proto.conditional.addBlock/setCondition/
removeBlock edit single actions by path, inside conditionals too; `expression` text accepted by
these and proto.interaction.add/set.
- Interaction editor: an action list (open, move, remove, add), Add action > Conditional, if /
else if / else blocks with a condition builder (variable picker, operator, value or variable) or
an expression field with inline errors, nested actions, set variable expressions.
- Presenter: hotspot hints toggle (Options menu, proto.presentOptions {hotspotHints}), hotspots from
the engine (proto_run::hotspots) and in ui.inspect; an Inspect panel showing the hovered layer's
properties ({inspect}).
- Fields keep the keyboard on the click that opens them, and the Enter that commits a field no
longer reaches the shortcuts.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
PDF export
- Vector masks clip to the mask's outline, luminance masks are luminosity
soft masks (alpha masks as before).
- Background blur: the backdrop the page draws under the layer is rendered,
blurred and cut to its shape, under the layer's vector content.
- Text hyperlinks become clickable link annotations (one per line and link).
- Frames-to-PDF shares one path (`frames_pdf`): sRGB conversion for Display
P3 files, hidden frames left out, the file name as the PDF title.
- `figcraft-cli export -o x.pdf` and `convert x.figcraft x.pdf` write the
page's frames one PDF page each.
- Tests: mask types, backdrops, links, image deduplication, page sizes in
points, hostile inputs; CLI end to end. Checked by rendering the PDFs back
to PNG and diffing against the PNG export.
Prototyping
- Model: conditional actions (if / else if / else blocks), set variable mode,
and expressions (literals, variable references, + - * /, comparisons,
and/or/not, negate) for set-variable values and conditions; walk helpers
for nested actions (remap on paste, colours, destination checks, library
copies and pruning, canvas connections).
- REST import maps CONDITIONAL, SET_VARIABLE_MODE and SET_VARIABLE with
variable references and EXPRESSION values; unsupported functions (mode
lookups) skip the action with a warning.
- The presenter runs them in order (`figcraft_engine::proto_run`); the
Prototype panel summarises conditionals, edits set-variable-mode, and
`ui.set {protoEditor}` opens an interaction's editor.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>