A nested instance opened from a .fig or file JSON document is first linked to its
own component; matching it to the outer main's layer then went by that component's
name instead of the layer's, so nested instances of variants (or under a name of
their own) and their layers stayed unlinked, and a second export lost what they
changed. They go by their own name until they mirror a layer.
- Dev resources: links for developers (a URL and a name) on any layer, stored on the layer (`Node::dev_resources`, a new synced property `devResources` at the end of the property list), saved in the file and undoable.
- Commands: `devResource.add {id?, url, name?}`, `devResource.edit {id?, index, url?, name?}`, `devResource.remove {id?, index?}`, `devResource.list {id?}` (Spanish labels added).
- An instance lists its own links, then its main component's and that component's set's, read-only with `from`; layers inside an instance show their main layer's and take none of their own (refused with a hint). Instances don't copy the main's links (created empty, own links kept across main edits).
- URLs are data: http or https with a host, no whitespace or control characters, at most 2,048 characters; names clipped to 200; at most 32 per layer. Nothing is fetched: a link added without a name is named after its URL (scheme, `www.` and trailing slash dropped). The app opens a link only when it is clicked.
- Inspect tab: a Dev resources section (after Annotations) lists the links with a link icon, name and URL (or "From <main>"), opens on click, edits name and link inline (pencil), removes (minus) and adds from a "Paste a link" field (plus). New strings in the en and es catalogs; links are announced as links to screen readers.
- Agents: `get_node` / `inspect_document` return `devResources` (own and inherited); docs/mcp.md and docs/control-protocol.md describe them.
- The editor extension stays out of scope: the MCP server already serves design data, code and links to agents and editors.
The tail of the text area, from the public documentation of the target app's text settings, with a table-driven behaviour suite (each case derived in one line; "silent" cases pin today's behaviour).
- **Lists per paragraph** (`TextData::paragraphs`, one `ParaList {list, indent}` per paragraph; empty when the layer has one list style at level 0, so old files load and save unchanged). The list buttons, Shift+Cmd+8 / Shift+Cmd+7, `text.listStyle` and Type settings act on the paragraphs the selection touches while editing (the whole layer otherwise). The per-paragraph list follows edits: a new paragraph copies the one it split from, joined paragraphs keep the first one's.
- **Nesting** up to 8 levels: Tab nests a list item, Shift+Tab moves it out; Enter on an empty item and Backspace at an item's start move it a level out, then out of the list. Typing `- `, `* ` or `1. ` at the start of a plain paragraph starts a list. Numbers count per level (1. a. i., then round again), bullets go • ◦ ▪; a plain paragraph, a shallower item or the other list kind starts the count again. Each level indents by one marker gutter; a nested bullet a font lacks comes from a fallback font.
- **List spacing** (space between two items of a list, in place of paragraph spacing), **hanging lists** (the outer gutter outside the box, item text lined up with plain paragraphs) and **hanging punctuation** (opening quotes and brackets outside the start edge of left-aligned and justified lines, closing punctuation outside the end edge of right-aligned and justified ones; hung punctuation takes no room when lines wrap).
- **Decoration look** per character (`TextStyle::decoration_look`, boxed, None by default): style (solid, dotted, wavy), thickness and underline offset (auto, px or % of the font size), skip ink (underlines break around descenders) and colour (its own style index in the layout, so canvas, PNG and PDF paint it without renderer changes). Decorations are drawn from `Decoration::path()` (bar, dots or wave) on the canvas, in PNG, PDF and outlined SVG.
- `node.set` keys: `listStyle` and `indentation` (range-aware with start/end), `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`, `textDecorationThickness`, `textDecorationOffset`, `textDecorationSkipInk`, `textDecorationColor`; documented in `KEYS` and docs/mcp.md. `text.edit.key` documents Tab.
- Inspect: `listStyle`, or `lineTypes` and `lineIndentations` per paragraph; `listSpacing`, `hangingPunctuation`, `hangingList`, the decoration keys. CSS: `text-decoration-style`, `-thickness`, `-color`, `text-underline-offset`, `text-decoration-skip-ink: none` (browsers skip ink by default). Dev Mode rows: decoration style, list spacing.
- REST import: `lineTypes` with `lineIndentations` per paragraph (they collapsed to one list style for the layer before), `listSpacing`, `hangingPunctuation`, `hangingList`, the decoration style, thickness, offset, skip ink and colour; instance override names for them.
- Type settings ▸ Basics: decoration style, thickness and offset fields (with Auto), Skip ink, decoration colour; List spacing; Hanging punctuation and Hanging list checkboxes; the list control acts on the edited paragraphs. New strings in en/es.
- Components carry the new text fields in the characters and text-style override groups; scaling scales list spacing and px decoration lengths.
- `TextData::path` is boxed (`Option<Box<TextPath>>`) to keep `NodeKind` under clippy's enum size limit after the new fields.
**A prototyping behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule on triggers, actions, overlays, animations, easing, scrolling and interactive components is a case with what the rule implies and a one-line derivation; where the documentation is silent the case says so and pins today's behaviour. The model rules are table-driven tests in the engine; the player's are driven through egui headless (real pointer, key and timer input into the presenter) or on the player directly.
**Fixes (the suite failed on these before them)**
- **On drag follows its animation**: with a move, push or slide animation, only a drag that way starts it (a drag another way can scroll instead), and the animation follows the pointer while the button stays down, then plays on from there when it lets go (it played by time, and any drag direction fired it).
- **Key combinations**: a key trigger can be a combination (`Shift+K`, `Ctrl+Alt+Delete`, other spellings of the modifiers accepted); the modifiers held must match, so `K` no longer fires for Shift+K. The interaction editor's recorder records the modifiers held, and REST files' modifier key codes import as the combination (they imported as the key alone, with a warning).
- **Overlay timers**: an overlay's after-delay timer starts when the overlay opens, and again each time it opens (it was timed from when the screen under it arrived, so a toast opened late closed at once, and it fired only the first time).
- **Interactive components**: every instance plays the interactions on the variant it shows; an interaction on the instance with the same trigger wins. Before, instances played only interactions copied onto them when placed: an instance placed before its main got interactions did nothing, and one placed after kept the first variant's interactions after changing (a toggle went on but never off). New instances no longer copy them, and copies in older files give way to the variant shown (`proto_run::play_interactions`, on the player's copy).
- **Change to on nested instances** changes the instance whose variant set holds the destination (`proto_run::change_to_target`), not the innermost instance (an icon inside a button).
- **Springs**: the presets are physical springs (mass 1; stiffness / damping gentle 100/15, quick 300/20, bouncy 600/15, slow 80/20; they were ad-hoc curves), and every spring plays until it settles instead of being squeezed into the duration field's time; the settling time accounts for over-damped springs. The interaction editor shows the spring's duration in place of the duration field, with a tooltip.
- **Smart animate overshoot**: matched layers follow spring and back easings past their end and back (they snapped to the end once the curve passed 1).
- **Fixed layers on top**: fixed layers draw and take clicks above the content scrolling under them, wherever they are in the layer list (the file's order doesn't change).
**Missing documented setting, added**
- **Scroll to offset**: `{type: "scrollTo", dest, offset: [x, y]}` stops the layer that far from the top-left of the innermost frame scrolling it (room for a fixed header); X / Y fields in the interaction editor; files carry it only when set.
- Already there and now covered by the suite: click / mouse down / mouse up / while pressing / while hovering / mouse enter / mouse leave / after delay, nested hotspot priority and click-through, navigate (closing overlays), back with the reversed animation, restart, overlay stacking / swap / close / close on a click outside, change to with smart animate, reversed directional transitions, easing curves.
New strings go through `tr(...)` with `en.toml` / `es.toml` entries. No new command ids. The player still draws nothing while idle: the drag-follow only runs during an animation, and fixed layers and instance interactions are worked out once per layout, not per frame.
**A components and variants behaviour suite written from the public documentation, and fixes for every divergence it found.** Each documented rule of instances (what an instance overrides and what it can't, how overrides persist when the main changes, reset), variants (which overrides survive a switch, the default variant, property order, duplicate combinations), swaps (instance menu vs Assets panel), exposed nested instances, detach / reset all / push changes / go to main, resizing and auto layout instances is a table row with the values the rule implies and a one-line derivation; where the documentation is silent the row says so and pins today's behaviour. Nested instance overrides stay covered by `nested_override_tests`, slots by `components_tests`, accepting library updates with overrides kept by `library_tests` / `components_scale_tests` (#91).
**Fixes (9 of the 10 new tests fail on the old code)**
- **Overrides that were silently lost**: auto layout **padding**, **gap** and **alignment** (three override groups, so a padding override still follows the main's gap), **layout grids**, **export settings** and **prototype interactions** on layers inside instances, and every **text property** (resizing, paragraph spacing and indent, lists, max lines, OpenType features) were reset by the next edit of the main. They are override groups now: kept when the main changes that property, carried by variant switches and swaps, pushed to the main, reset per group, and listed in the Overrides section. REST imports map the matching `overriddenFields`. An instance's own export settings and interactions stay its own, like its place (as before).
- **What an instance can't override is refused** (rolled back with a message, one place for every path: commands, MCP, gestures): moving, rotating, flipping, nudging or aligning a layer inside an instance (auto layout items included: their place is the layout's), resizing it (text boxes keep their main's bounds; text whose box follows its content still grows), changing its constraints or resizing (hug / fill / min / max / absolute), and an instance's auto layout direction or other auto layout settings (only padding, gap and alignment are its own). Before, these became `position` / `size` overrides or were lost on the next main edit. Resizing the instance itself still works and its layers follow the main's constraints. Existing `size` / `position` overrides in files are still read and kept.
- **Variant switches** keep an override only where the layer's name path matches *and* both variants had the same value for that property; text layers keep all their changes (by name and hierarchy). Before, every same-named override survived, even where the new variant differs (a hover background overridden back to the default one). The instance's own overrides follow the same rule. Undo brings the dropped overrides back.
- **Alt-drop swaps from the Assets panel** (`instance.swapAt`, `library.swapAt`) keep only text layers' overrides; the instance menu's swap (`instance.swap`) still keeps every override on same-named layers.
- **Default variant = the top-left one** (was the first in layer order): new instances of a set, the set's Assets thumbnail, generated code, `component.properties` defaults and the panel's default dropdown. Picking another default moves that variant to the top-left corner (one undo step).
- **Hidden exposed instances hide their properties** (`instance.exposed`).
- **Push changes from a nested layer** goes to the outer instance's main, never into the nested instance's own main; it reuses the override copy code, so every group above is pushed too.
**Added**
- **Variant property order**: `component.editProperty {name, index}` moves a variant property; every variant's name follows.
- **Duplicate combinations**: `component.properties` reports `conflicts` (variants sharing one combination of values), and the component panel warns until each has a unique one.
- **Design panel**: Position, Rotation, Constraints, Dimensions / resizing and grid cell fields are greyed out (with a tooltip) for layers inside an instance; the auto layout direction, wrap, strokes / canvas stacking and add / remove auto layout are greyed out or hidden for instances. New strings go through `tr(...)` with `en.toml` / `es.toml` entries; disabled controls are announced as disabled.
Components (or sets) can be linked per language to a real code component (import path, name, and a template whose {prop} and {prop ? "a" : "b"} placeholders take the instance's property values, escaped per language). Dev Mode's Inspect panel shows the connected snippet with its import for instances, a Connect to code editor with live preview, and read-only plugins. Links export to and import from figcraft.connect.json.
Boolean operands are editable in place with live results (per-operand operations, cached prefixes so a drag recomputes from the edited operand up); a Design panel Arc section; the shape builder takes strokes and open paths as outlined areas; variable width points can be wider on one side.
* M3: components at scale; Alt-drag to swap instances
- Synthetic design system (engine `synthetic_ds`): ~300 components (icons; button, input,
card, nav and badge sets of size x state x type x theme variants), boolean/text/instance
swap/slot properties, instances nested four deep, text and paint styles, light/dark
variables bound across it, and 2,000 instances with overrides at every level.
- Propagation: value, size and added/removed-layer edits of mains reach their copies
directly; other edits rebuild each affected instance once, inner mains first; rebuilds keep
unchanged layers shared; bound variables no longer touch unchanged layers; auto layout
re-flows only where sizes or places changed.
- Fixes: values from variables (theme switches, bound radii) are no longer recorded as
overrides; a detached variable on an instance layer is an override that survives main
edits; exposed nested instances stay exposed; swapped nested instances keep their layer
names and the ids/overrides of layers their own main adds; renamed nested instances keep
their name when detached out.
- Instance swap properties have preferred values. `instance.swapAt` / `library.swapAt` and
Alt-drop from the Assets panel onto an instance swap it (highlighted while dragging).
- Instances pasted into other files follow their mains (library copies shared, published
components used from their library, other mains left behind as a missing main).
- Assets panel lays out only visible tiles and renders thumbnails lazily with a cache
invalidated per changed component; instance panel lists components once per revision.
- `figcraft-cli perf --instances`, UI frame bench and tests in components_scale_tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: the swap perf case drops on a glyph the hit test reaches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge origin/main into feat/components-scale; weighted depth 79%
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fill, stroke, effect and layout grid rows drag to reorder by their grip (a handle shows over the
row, an insertion line where it lands, the row's label follows the pointer like a dragged layer;
Escape cancels; lists that differ across the selection have nothing to drag). New commands
`fill.move`, `stroke.move` and `grid.move {ids?, from, to}` join `effect.move` (lists bottom first,
one undo step; items keep everything, linked list styles are unlinked).
Variable bindings on list items follow their items when a list is reordered or shrinks:
paint colours bind per paint (`fills/<i>/color`, `strokes/<i>/color`; `fill`/`stroke` stay the
first paint's), so a moved bound paint keeps its variable and its neighbours are never recoloured.
`fill.remove`, `stroke.remove` and `grid.remove` refuse negative indices.
Layout grids get settings in the Design panel (size; count, type, width or height, margin or
offset, gutter; colour) whose numbers bind to number variables (`layoutGrids/<i>/<field>`),
re-applied on variable and mode changes. Instance boolean and text properties bind to boolean and
string variables (`componentProperties/<name>`, from the instance's properties panel), kept across
instance rebuilds, following property renames and dropped on delete, reset and detach.
Inspect names the variable of each paint.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M6: Dev Mode component code, annotations and ready for dev
- Component code: React (new), SwiftUI and Compose write components as reusable
units whose component properties are parameters (variant unions/enums,
boolean flags, text strings, instance swaps as views, slots as optional
content); variant sets share one definition (React: data-* rules for the
other variants); instances are calls passing only the values they change.
Escaped, unique identifiers. `inspect.componentCode` returns the parts.
- Annotations: notes with pinned properties on any layer, saved with the file
(left out when empty), undoable `annotation.add/edit/remove`, `annotation.list`
with live values; drawn as callouts on the canvas in the Inspect tab and
edited in its Annotations section. Instances keep their own annotations.
- Ready for dev: `inspect.readyForDev` lists frames and sections by handoff
status; the Inspect tab sets the status and lists them with a filter.
- Shared widgets: a multi-line field and the status dropdown are reused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6: test that bound variables become CSS custom properties
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6: regenerate the parity report
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Slots: a frame of a main component becomes a slot property
(component.makeSlot / removeSlot, preferred components, type "slot" in
component.properties). Instances show the main's default content until
the slot is edited; from then on its layers are the instance's own
(override group "slot"): free to add, remove and reorder, kept across
main updates, variant switches, copy/paste and detach, and instances
placed there stand alone. instance.slotInsert fills a slot,
instance.resetSlot brings the default content back. Structure
protection allows edits inside slots only; drops from the Assets panel,
canvas drags, drawing and paste land in slots. UI: slot icon in the
layers panel, dashed outlines and labels on the canvas, slot property
rows with a fill menu in the properties panel, Make/Remove slot on
frames inside a main.
Nested overrides: overrides inside a nested instance survive the main
moving it into another frame (layers matched by source alone when
unique); a nested instance copied out of an instance is re-linked to
its own main with its look kept (values set by component properties
aren't overrides); plain layers copied out of instances stop mirroring
them; canvas drags can no longer pull layers out of an instance or
reorder its auto layout children. Tests for swaps, variant switches,
main restructuring, detach and copy/paste.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Publish (`library.publish {description?, include?, exclude?}`, Libraries > Publish library…):
the file's components, component sets and styles (by default all but names starting with `.` or
`_`, or what the last publish offered) and every component and style they use go into a snapshot
with a manifest (library id, version, per-asset version stamps). An asset keeps the version it
last changed in; publishing without changes is refused.
- .figcraft: `library/manifest.json`, `library/document.json` (the snapshot) and
`library/images/<sha256>` for images only the library still uses. Older files and readers are
unaffected (extra members); a damaged library is skipped and reported when the author's file
opens. Files using a library read only these members (size-limited), never unpublished edits.
- Version history: the library entries are written and read alongside the versions (a library
survives saving with versions and reopening); restoring a version keeps what the file published.
- Enable: `library.add {path}` / `library.remove {id}` / `library.list {assets?}`; the document
keeps its enabled libraries (id, name, path, last seen version) in its data. The session caches
each library file and reads it again when it changes on disk; in the browser these commands
return a clear error (no file system).
- Use: `library.place {library, component, x?, y?}` / `library.import` copy the component (with
the components and styles it uses) into a hidden library store (a page-like root outside the
page list) as read-only main components and place an instance; instances behave like any other
(overrides, properties, variants, swap, reset, detach). `library.applyStyle` copies a style in as
a linked remote style. Editing the copies or library styles is refused; Go to main component
names the library. Importing a file's pages brings its library copies along (shared with copies
the document already has).
- Updates: `library.updates` and `library.acceptUpdates {ids?}` re-import changed copies in place
(layer ids kept, so instance overrides survive, also when layers are added); unused copies are
dropped when a library is removed or updated.
- Assets panel: each enabled library's components (thumbnails, search, click or drag to place,
shared with the local tile code) and styles (click to apply, paint swatches), collapsible per
library; "Library updates available" banner with Update; a libraries view (book button,
Libraries menu) to publish this file (choose assets, description) and add or remove library
files. Local styles in the panels list only this file's styles.
- Tests: manifest round trip and versions, add/place/update keeping overrides (incl. a structural
update), variants/swap/undo, library styles and importing pages, missing/corrupt/truncated/
oversized/never-published files; format tests for damaged and oversized members.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- A nested instance inside a main component can be exposed (`component.exposeInstance`, an
"Expose properties to instances" toggle in its panel); the main's instances then list it by
name with its variant and component property controls, recursively for exposed instances
inside it. Values set there are nested overrides and survive main edits.
- `instance.exposed {id}` lists an instance's exposed nested copies (for the CLI and MCP).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Assets tab rebuilt as its own module: local components as two-column tiles with live
thumbnails (re-rendered when a component's content changes, throttled), a search field that
filters components and styles, and the local styles list.
- Drag a tile onto the canvas: a ghost follows the pointer, the frame under it highlights and auto
layout frames show the insertion line; releasing drops an instance there, centred on the
pointer. Clicking a tile places one in the middle of the view.
- New command `instance.place {component, x, y}` (page point; drops into the frame or auto layout
slot under it; never inside its own main) for the CLI, control channel and MCP.
- The auto layout drop slot is shared with moving layers (`drop::auto_layout_slot`).
- Creating a component from a single shape names it after the shape.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>