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.
- Compare changes: a ready-for-dev frame or section keeps a snapshot of itself from just before
its first change (Document::dev_snapshots, saved only while changed; dropped on resolve, re-mark,
other status or delete). `dev.compare {id?, show?}` diffs it layer by layer: added/removed
layers, and property changes formatted like Inspect (`width 120 → 136`, `fill #FF0000 →
#E03030`, text, parent, order, component, auto layout, typography, effects). The compare view
shows before / now side by side or overlaid with a slider, changed layers outlined, the list of
changes and Resolve change.
- Focus view: `dev.focus {id? | direction}` / `dev.unfocus`: one frame drawn and hit alone
(render Options::solo), a bar with name, status, Compare changes, previous / next ready frame and
exit; Escape leaves.
- Annotation categories: defaults plus the file's own with colours (`annotation.categories`,
`addCategory`, `editCategory`, `removeCategory`), `category` on annotations, coloured callouts,
a category filter and editor in the Inspect panel; stored backwards-compatibly.
- Shared widgets::icon_button / canvas_banner / dropdown_lead; has_status moved to the engine.
- Tests: devcompare_tests, devfocus_tests, annotation_categories_tests, doc handoff_tests, render
solo_tests, UI dev_mode_tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Interaction editor: actions (top level and inside conditional blocks) reorder by dragging
their row's grip (list_drag, now also for lists shown in model order), one proto.action.move
per drop; the open action stays open where it lands; Escape cancels a row drag without
deselecting or closing what holds the list. The ... menu keeps Move up / Move down.
- Prototype expressions: `accent[Dark]` reads a variable's value in one mode of its collection
(Expr::InMode) and `mode(Theme)` is the name of the mode a collection is in where the
interaction runs (Expr::Mode); parsed, type-checked, displayed, stored by id (names accepted in
JSON), evaluated with the layer's modes. REST import maps VAR_MODE_LOOKUP over a variable and
one of its modes.
- Overlays opened through commands close on a click outside unless told not to, as in the editor.
- Collection::find_mode / current_mode replace repeated lookups.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 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>
* 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>
* M2: auto layout keyboard reordering, nested auto layout fixes, section hide contents and dev status
- Arrow keys move auto layout children one place: along the flow (past hidden and
absolute siblings), between wrapped rows, to the neighbouring grid cell, and into
the neighbouring nested auto layout frame; no undo step when nothing moves.
- Sections: section.toggleContents (hide contents), object.wrapInSection, frames in
sections are top-level frames (labels, flows, navigation, PDF export), navigating
to a section shows its frame shown last.
- Ready for dev turns "Changed" when contents are edited; dev.resolveChange with a reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M2: README and ROADMAP for auto layout arrow keys and sections
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: roadmap refresh after #68–#77
Parity table rows for canvas, selection and transform, layers, effects,
text, prototyping and comments now reflect the merged work; weighted
feature depth ~76%; README command count and hand-off features.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M2: the dev change reason resolves only on Enter or Resolve
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Ruler guides can belong to a frame: `guide.add/move/remove/clear` take `frame` (as well as
`page`), keeping the guide in the frame's own coordinates. Frame guides move with the frame, span
only it on the canvas, are saved and copied with it, and snap the layers inside it. A guide dragged
out of a ruler and dropped on a frame becomes that frame's guide; its label shows the offset in the
frame.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Background blur sees the whole backdrop under the layer: the renderer keeps the stack of
isolated surfaces it draws into (clipping frames, rounded clips, faded groups, masks) and
flattens it for the layer's area, reading past the layer's own box. A blur inside a clipping
frame now shows the frame's fill instead of the sharp layers below, on the canvas and in PNG
exports. The PDF export draws background blur and glass through the same code from a render of
what lies under the node (`backdrop::render_beneath`, which the PDF's backdrop now shares), and
its images match the PNG's pixels.
- Shadow spread offsets the outline: the shape filled and stroked twice the spread wide with
mitred joins (grow) or cut away (shrink), the stroked bands cached per outline. Ellipses stay
ellipses, stars keep sharp points, text keeps its letterforms, rounded corners grow their
radius. Inner shadows shrink the outline the same way. Canvas, PNG and PDF share the shadow
code; SVG writes spread shadows as the same geometry and now exports inner shadows.
- Glass effect (light angle and intensity, refraction, depth, dispersion, frost, splay): the
frosted backdrop bent towards the inside within `depth` of the edge, colour fringes, light on
the rim, from an exact distance transform with smoothed normals.
- Noise: mono, duo and multi colour modes, grain size, density, opacity and blend mode; grains
live in the layer's coordinates (stable when panning and zooming). Texture is now drawn
(pixels scattered by the radius, grain size, clip to shape).
- Design panel rows for noise mode/colours/opacity, texture clip to shape and every glass value;
`effect.add` takes `glass`; REST import of NOISE, TEXTURE and GLASS; Inspect rows and CSS;
the scale tool scales grain sizes, texture radius, glass depth and frost.
- Tests in new modules: render backdrop/spread/grain tests, PDF backdrop tests, SVG, doc,
engine and REST effect tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Canvas previews zoomed out to 50% or less draw levels of detail
(crates/render/src/lod.rs): subtrees under 2 device px are splatted as
their averaged paint colours (box filter, no paths or effects), text
under 4 px is greeked into bars, small solid rectangles are splatted and
rectangular clip layers are composited without the blend pipeline.
Image paints and masks keep the exact path. Exports, thumbnails and
every other render stay exact (Options::lod is off by default); render
bands are assembled with a row copy instead of a pixmap draw.
.figcraft documents are serialized straight into the compressor and
parsed straight from the text (a header pass reads version and image
index without building a value tree); layer and text fields at their
default values are left out (63 -> 22 MB of JSON at 50k layers, older
files still load); JSON members use deflate level 3.
At 50k layers: zoomed-to-fit render ~58 -> ~8 ms, save ~1.0 s -> ~0.09 s,
load ~0.86 s -> ~0.16 s.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A stroke can carry a width profile: widths at positions along the path
(each subpath by length), interpolated smoothly (monotone cubic).
- doc: WidthProfile / WidthPoint / WidthPreset (uniform, wedge, taper both
ends, eye, rounded, pinch) on StrokeProps, serde-defaulted so older files
load; paint reach and hit testing follow the widest point.
- pathops: path measuring by length and the variable outline (offset along
the normals, round/miter/bevel joins, caps, dashes keep their width,
Inside/Outside clip as before; bounded samples and dashes, uniform
fallback). Canvas, PNG, SVG (filled outline), PDF and outline stroke use it.
- engine: stroke.widthProfile {preset | points}, node.set widthProfile,
inspect output; the Variable width tool (Shift+W) in vector edit mode:
drag the path to add a width point, slide points, drag their handles,
Delete removes the selected one; vector.edit.widthPoint add/set/remove.
Every gesture is one undo step.
- UI: width profile dropdown with previews drawn in code and a flip button
in the stroke settings; the tool in the edit toolbar (new icon drawn in
code); width handles and a hover preview on the canvas; Position/Width
fields for the selected width point.
- Tests: profile maths, outlines, render regressions, tool gestures,
commands, exports and junk sweeps. ROADMAP, README, docs updated.
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>
- Object ▸ Resize to fit (Alt+Shift+Cmd+R, `object.resizeToFit`): frames, sections and
components shrink or grow to their visible contents without moving them on the page; auto
layout frames switch to hug instead.
- Sections get colour swatches (light tints) in the Design panel.
- Frames and sections get a handoff status (`devStatus`: ready for dev / completed) set from
the panel and shown as a badge next to their name on the canvas; `node.get` reports it.
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>
- Layers gain `maskType` (alpha by default, so files are unchanged): a vector mask clips by the
mask layer's outline whatever its fills and opacity; a luminance mask shows content where the
mask is bright and hides it where it is dark.
- `node.set {maskType}` (validated), `node.get` reports it, and the Appearance section shows a
Mask dropdown when the selection is masks.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>