Commit Graph
32 Commits
Author SHA1 Message Date
BFlat 8167f13a6a M12: Boards: a collaborative whiteboard file kind (#1229)
Board files (File > New board) with sticky notes, shapes with text, connectors that follow what they join (straight or elbow, arrowheads, labels), marker, highlighter and eraser, and stamps; a board toolbar and a contextual bar; the Design panel hidden on boards, Prototype and Dev Mode refused there. Boards are ordinary FigCraft files built from ordinary layers, so saving, undo, export, MCP and live sessions work unchanged. File tabs, recent files and Open files show a pen nib for design files and a note for boards (original icons drawn in code).
2026-10-24 04:18:55 +07:00
RealtaNua cae69a7ae8 Merge remote-tracking branch 'upstream/main' into feat/752-report-missing-fonts
# Conflicts:
#	ROADMAP.md
#	docs/file-format-parity.md
2026-10-17 22:21:25 +07:00
RealtaNuaandClaude Opus 5.5 825cb1e7af M7: list the fonts a file uses that aren't installed (#752)
Text in a family that isn't installed was drawn in a fallback font
without a word. Now:

- `text.missingFonts` lists the families a document names that no
  installed or built-in font provides: text layers and their styled
  ranges, text styles, and string variables bound to a font family
  (every mode, through aliases), with the number of text layers.
- The desktop app shows a Missing fonts window after the user opens a
  file that has any: once installed fonts are indexed (before that every
  one of them would count as missing) and after any import report is
  closed. Files named on the command line count. Programmatic opens and
  the browser build never open it by themselves. Text > Missing fonts...
  shows it for the file in front; Escape closes it.
- `figcraft-cli info` prints `missing font` lines (`missingFonts` with
  --json).
- file-format-parity: the row claimed REST import warnings that no code
  produced.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 21:49:32 +07:00
Ivan Shishkanov 7a5885cde9 M3: add Selection colors for mixed selections (#403) 2026-10-15 19:05:24 +03:00
bflatastic e56856f401 M3: component playground: variant, boolean and text properties 2026-10-13 13:36:54 +07:00
bflatastic c2a67f81e5 M11: starter templates: eight templates built in code, template.list and file.newFromTemplate, help.gettingStarted 2026-10-13 03:01:19 +07:00
bflatastic 501e4a6737 M9: online files: the app remembers the files it put online
A file put on a server without the server's key stays its owner's after its
tab closes or the app quits. The app records it with the preferences (server,
id, name, owner token, the owner's links, what others join with; at most 200;
validated when read back) and never shows the token: not in command results,
ui.inspect, logs or debug output.

- Home > Online lists your online files: open one as its owner (a new tab, or
  its tab comes to the front), start and stop sharing, copy the code or link,
  rename on the server, remove from the server (confirmed), forget here. One
  request per server when the section opens; offline and gone states.
- collab.files, collab.openFile, collab.forgetFile; collab.manageFile takes
  its server and token from the record and gains share / unshare.
- Server: Admin owned (a compatible addition within protocol version 2) says
  what became of the files a list of owner tokens owns. Older servers leave
  the list as recorded.
- Tabs mark files in a live session (shared, or open only to you); toasts
  about a file that isn't in front name it; after a crash a recorded file
  reopens from its server as its owner.
- Draft catalogs (de, fr, ja, ko, pt-BR, zh-Hans) translate the 71 strings
  and 5 command labels they lacked and the new ones; es and es-419 follow.
- Docs: collab-protocol, server, mcp, control-protocol, parity pages, gaps,
  ROADMAP.
2026-10-13 01:41:14 +07:00
bflatastic def6f84b22 M11: plugin directory on figcraft-server
A self-hosted plugin directory, with no third-party service: `figcraft-server` keeps plugin bundles,
the app browses, installs and updates from it, and `figcraft-cli` publishes to it.

- **Bundles** (`figcraft_plugin::bundle`): a plugin's manifest, module and up to 32 plain-named
  assets in one uncompressed, length-prefixed binary form (nothing can expand beyond the bytes
  sent). Its SHA-256 names it. `Bundle::decode` never allocates beyond its input and refuses
  trailing bytes; `Bundle::check` parses the manifest (refusing a plugin API this build doesn't
  run), requires a version of 1-4 numbers, compiles the module without running it (only the
  sandbox's imports allowed) and checks asset names (no paths, no dot files, no Windows device
  names, unique ignoring case). Manifests gain an optional `author`.
- **Protocol**: a new connection kind next to managing and team-library connections:
  `OpenPlugins {key}` then `Plugins {list | fetch | publish | unpublish}`. Browsing needs no
  credential; publishing and unpublishing need the server key (a wrong key is refused). Older
  servers refuse the connection and the app says the server has no plugin directory. The protocol
  version is unchanged; old clients are unaffected.
- **Server** (`apps/figcraft-server/src/directory.rs`): `<data>/plugins/index.json` plus
  `<data>/plugins/<sha256>.bin`, so no publisher-chosen name reaches the file system. Every publish
  is checked in full and must be a newer version; caps `max_plugins` (versions, 0 turns it off),
  `max_plugin_mb` (<= 32), `max_plugins_mb`. Fetches re-hash the file before sending (a damaged
  bundle is never sent) and share a download budget (~32 MiB/s) so a flood is refused, not queued.
  Damaged index entries are skipped and reported, never deleted; an unreadable index lists nothing
  and refuses writes until the operator fixes it. Operator moderation:
  `figcraft-server plugins [list]` and `plugins remove ID [--version V]` (offline or through the
  running server). Logs name ids and versions, never keys.
- **App**: Plugins > Manage plugins gets Installed / Browse tabs. Browse lists a server's plugins
  (version, author as typed, size, access) with a search; nothing is fetched until Browse is
  pressed. Install / Update open a confirmation listing the permissions asked for, the source and
  the SHA-256; only its button downloads. The download must match the listed SHA-256 (and the one
  shown), then is checked like an imported plugin before anything is written. Installed plugins
  live per user under `<settings>/FigCraft/Plugins/<hash of id>/` and run in the same sandbox;
  Remove deletes the folder.
- **Commands**: `plugin.directory.list {server?, query?}`, `plugin.directory.install {id, version?,
  confirm, server?, sha256?}` (refused without `confirm: true`; never opens a dialog),
  `plugin.directory.publish {path | plugin, server}`, `plugin.directory.unpublish {id, version?,
  server}`. `ui.set {pluginsTab, pluginConfirm}` on the control channel.
- **CLI**: `figcraft-cli plugin publish FOLDER --server ws://host:port/` with the key from
  `FIGCRAFT_SERVER_KEY` (or `?key=`), never printed.
- Docs: plugins.md (directories, publishing, commands, manifest), server.md (operating the
  directory: access, checks, caps, moderation, backups, security), collab-protocol.md, mcp.md,
  cli.md, control-protocol.md, target-app-parity area 18 and Ecosystem, ROADMAP, roadmap.md M11,
  gaps.md gap 6. Plugins & widgets 55%/32% -> 60%/35%, 10-30 h -> 8-26 h.
2026-10-12 05:28:53 +07:00
bflatastic 207373be15 M11: plugin widgets and the plugin panel tail
Widgets for FigCraft's own WebAssembly plugins (plugin API 3; API 1 and 2 plugins run unchanged), plus the panel tail.

- **Widgets.** A manifest with `"api": 3, "widget": true` makes a widget plugin. Plugins > Widgets (or `widget.insert {plugin, x?, y?}`) puts its widget on the page: the plugin is called with `widget: {id, state, event}` and hands back a description through the new `widget_render` import (`{state?, root}`: frames with auto layout, text, rectangles, ellipses and images it carries as base64). FigCraft validates the description (4 MB, 256 layers, 16 deep, finite numbers within ranges, hex colours, text length and control characters, a 16 KB state, images 1 MB each / 2 MB together / 4096 px a side) and builds it as a frame whose layers are locked, so the widget moves as one layer.
- **Clicks.** A plain click with the Move tool on a part the plugin named as a `target` (the cursor turns into a pointing hand there; quick repeated clicks each count) calls the plugin back in a fresh, metered sandbox (same fuel, memory and call limits as runs; nothing kept between calls, so the state and the click alone decide the result) as one undo step, and the widget is redrawn. Dragging moves it; Shift/Cmd/Alt clicks only select. Agents use `widget.click {id, target}` and `widget.list`.
- **State in the document.** The plugin id and the state live in the widget's own layer data (`figcraft.widget*`), so widgets survive save and reload, duplicate/copy and paste (the copy is a widget of its own), undo/redo, export (the built layers draw) and live sessions (synced like any layer data). No plugin can write `figcraft.widget*` data or call `widget.*`; a callback can only rebuild its own widget, and touches other layers only through the commands its manifest permits. A widget whose plugin is missing stays as drawn.
- **Example.** `plugins/examples/counter`: a counter between - and + buttons (read access, no assets). The SDK gained `Input::widget` and `render_widget`.
- **Panel tail.** Panels of several plugins are open at once, side by side (`plugin.ui` without an id returns the one opened last); a colour field's swatch opens FigCraft's colour picker beside the plugin's window (the plugin hears of the colour when a drag ends; also for parameters forms). Swatch grid's palette panel gained a label colour field.
- Docs: plugins.md (ABI v3, widget description, commands, limits), mcp.md, target-app-parity area 18, ROADMAP, gaps gap 6, roadmap M11; Spanish labels for the new commands.
2026-10-12 04:41:28 +07:00
bflatastic 7e467c2b82 M7: dev resources links
- 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.
2026-10-12 02:30:25 +07:00
BFlat 13ab3f150f M6: code connect: link components to real code (#122)
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.
2026-10-11 14:02:37 +07:00
BFlat 7f9f2a0873 M9: branches: create, update from main, review and merge (#119)
Branches live inside the .figcraft file (base and head snapshots, own id space). Update from main and merge are three-way merges over the collaboration change sets; a conflict (same property changed on both sides, or delete vs edit) must be resolved branch-or-main before merging, which is one undo step on main. The review dialog reuses Dev Mode's compare view; branch tabs show a banner; File > Create branch; branch.* commands for CLI and MCP.
2026-10-11 12:45:38 +07:00
BFlat ee82cdd138 M3: extended variable collections, variables import/export, gradient stop and variant bindings (#114)
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.
2026-10-11 10:49:16 +07:00
BFlatandClaude Opus 5.5 435415770f M9: live sessions in the browser; join links and dialog polish (#106)
- Browser client (step 4): the web build joins sessions a desktop app hosts
  through the browser's WebSocket API (web-sys, binary frames, the same
  message encoding). Its connection logic is a target-independent state
  machine (collab-net driver.rs): hello on open, changes and presence held
  until the welcome, throttled presence, keep-alive pings, a silent host
  counted as dropped, reconnect with backoff, final refusals. Tested natively
  against a scripted socket and a real host over loopback. Hosting stays
  desktop-only (LiveLink::can_host; collab.peers canHost). https pages are
  told up front that they can only reach ws:// sessions on this computer.
- Shared join logic (join.rs: GuestConfig, outcomes, taking in host messages)
  and transport limits (limits.rs) used by the native guest and the browser.
- Invite links: `figcraft figcraft://join?...` joins on start (a bad link
  opens the Join dialog with it); file:// arguments are paths. Windows
  registers a per-user figcraft:// opener (HKCU\Software\Classes\figcraft)
  behind a Registry trait (tests use a fake): collab.registerLinks {on?} and a
  Preferences checkbox, default on for installed builds, never for builds
  from source. macOS Info.plist declares CFBundleURLTypes; the Linux desktop
  entry declares x-scheme-handler/figcraft and passes %U.
- Join dialog: Escape closes it and no longer reaches the canvas (selection
  kept); its field has the keyboard when it opens.
- Docs: collab-protocol (browser client, links, mixed content), mcp,
  control-protocol, README, web hosting notes, collaboration wording in the
  progress docs (numbers unchanged).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:10:59 +07:00
BFlatandClaude Opus 5.5 4d7a9187ff M9: live sessions: host from the app, invite links, live cursors and presence (#104)
* M9: real-time collaboration protocol (design)

docs/collab-protocol.md: WebSocket sessions with a server-ordered,
per-property last-writer-wins document sync, client-scoped ids,
fractional positions for children, ephemeral presence (cursors,
selections, follow), per-user undo, hosting from the app first and a
self-hosted server next, and the implementation plan. ROADMAP gains M9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M9: collaboration core: ids, change sets, server ordering, convergence

Step 1 of docs/collab-protocol.md, no networking:

- Client-scoped node ids: Document::set_client_id makes alloc_id hand out
  client << 32 | counter; single-user files allocate as before; the id
  counter is no longer part of document equality.
- crates/collab (layer 2): the synced object/property model (every saved
  Node field, plus document-level styles, comments, variables, images,
  dev snapshots, library data), Change batches, an O(changed) diff, strict
  and lenient apply, fractional sibling positions, snapshots.
- Server (ordering, validation, rejects with fixes) and Client (optimistic
  view, replay under remote batches, author-side repair of derived data)
  as pure state machines; binary (version byte + postcard, JSON values) and
  JSON message forms, fuzzed decoders.
- Engine: normalize as the recompute hook, collab.diff.
- Tests: round trips over random real commands, an N-client convergence
  fuzz test over delayed in-order links, junk batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M9: fmt after merge

* M9: live sessions: host from the app, invite links, live cursors and presence

Step 2 of docs/collab-protocol.md, networking and presence half:

- crates/collab-net (layer 6): WebSocket transport (sync tungstenite, a
  reader and a writer thread per connection, no async runtime), the
  session host the app embeds (runs figcraft_collab::Server as the
  authority, carries its messages), invite links, presence throttling
  (~30 Hz, latest wins), limits (sockets, hello/idle timeouts, message
  size, rate, slow clients), keep-alive, clean close, reconnect with
  backoff; a browser stand-in until the web-sys client (step 4).
- engine: live session state (status, peers, presence, follow) and the
  collab.host/stop/join/leave/peers/follow/cursor commands; the network
  is reached only through the LiveLink the app installs.
- ui-egui: remote cursors (pointer drawn in code, name tag, gliding),
  selection outlines (once documents sync), avatars in the right panel
  with follow, the Share popover (start, invite link + URL with Copy,
  stop), File > Join live session.
- desktop app and MCP headless session install the link; xtask layers
  keeps networking crates out of L0-L5.
- Documents don't sync between apps yet (follow-up).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M9: live sessions: document sync between apps

- The host's session runs figcraft_collab::Server seeded with the shared
  document; every app (the host's too, through the hub without a socket)
  is a collab Client. Guests open the host's file from the welcome as a
  new tab; leaving keeps it as a local copy.
- Engine: after each local action (a gesture once it ends; nothing during
  version or text previews) the session document is diffed into one
  Changes batch; server batches go through Client::receive with
  EngineRecompute and are installed with Session::apply_remote, which
  never touches the undo history. Batches arriving mid-gesture wait.
- Transport: Changes/Reject/Welcome reach the engine as Event::Sync;
  LiveLink::send carries batches in order; hellos are limited to 64 KiB,
  joined clients to the core's message size.
- Tests: engine sync against a real Server (ids in the client's space,
  one batch per gesture, remote edits skip undo, deferral, closing the
  file leaves), three apps over loopback converging (create, move,
  recolour, delete, rename) and a late joiner getting the server's state.
- Docs: collab-protocol, mcp, README, roadmap docs; the UI's
  "cursors only" note is gone, replaced by the per-person undo caveat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:40:59 +07:00
BFlatandClaude Opus 5.5 4924512f4e M9: collaboration core: ids, change sets, server ordering, convergence (#103)
* M9: real-time collaboration protocol (design)

docs/collab-protocol.md: WebSocket sessions with a server-ordered,
per-property last-writer-wins document sync, client-scoped ids,
fractional positions for children, ephemeral presence (cursors,
selections, follow), per-user undo, hosting from the app first and a
self-hosted server next, and the implementation plan. ROADMAP gains M9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M9: collaboration core: ids, change sets, server ordering, convergence

Step 1 of docs/collab-protocol.md, no networking:

- Client-scoped node ids: Document::set_client_id makes alloc_id hand out
  client << 32 | counter; single-user files allocate as before; the id
  counter is no longer part of document equality.
- crates/collab (layer 2): the synced object/property model (every saved
  Node field, plus document-level styles, comments, variables, images,
  dev snapshots, library data), Change batches, an O(changed) diff, strict
  and lenient apply, fractional sibling positions, snapshots.
- Server (ordering, validation, rejects with fixes) and Client (optimistic
  view, replay under remote batches, author-side repair of derived data)
  as pure state machines; binary (version byte + postcard, JSON values) and
  JSON message forms, fuzzed decoders.
- Engine: normalize as the recompute hook, collab.diff.
- Tests: round trips over random real commands, an N-client convergence
  fuzz test over delayed in-order links, junk batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M9: fmt after merge

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:03:03 +07:00
BFlatandClaude Opus 5.5 041b3f4e5d M7: WASM plugins (#101)
Sandboxed WebAssembly plugins whose API is the command registry:

- figcraft-plugin (new crate, L2): the figcraft-plugin.json manifest
  (menu commands with parameters, read/write access, command allowlist)
  and the sandbox: wasmi (pure Rust, no JIT, also in the web build) with
  fuel metering, a memory cap, no WASI and seven host functions (call,
  selection, reply_len, reply_read, log, notify, set_output).
- Engine: plugin.list / plugin.import / plugin.remove / plugin.run,
  plugins.runLast (Alt+Cmd+P; Ctrl+Alt+Shift+P on Windows/Linux) and
  plugins.manage. A run is one undo step (Session::grouped, which
  command.batch now uses too) and is rolled back when it fails. Files,
  other documents, the undo history, dialogs and plugins are off limits;
  read-only plugins can't change the document. Installed plugins live in
  the preferences; development plugins re-read their module every run.
- UI: installed plugins in the Plugins menu, a parameters form drawn
  from the manifest, Manage plugins, Plugins > Development > Import
  plugin from manifest.
- plugins/: a no_std Rust SDK and two example plugins (Tidy names,
  Swatch grid), built for wasm32 by `cargo xtask plugins` (part of
  `cargo xtask wasm`).
- Docs: docs/plugins.md (manifest, permissions, limits, ABI, SDK),
  docs/mcp.md, docs/control-protocol.md, README, ROADMAP, parity.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:42:17 +07:00
BFlatandClaude Opus 5.5 599495eef5 M5: build conditionals in the interaction editor; hotspot hints; inspect in the presenter (#82)
- 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>
2026-10-10 16:00:02 +07:00
BFlatandClaude Opus 5.5 4565c4ab93 M3: reorder paints, effects and layout grids in the Design panel; grid and instance property variable bindings (#81)
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>
2026-10-10 15:21:05 +07:00
BFlatandClaude Opus 5.5 d16878588f M6: Dev Mode component code, annotations and ready for dev (#80)
* 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>
2026-10-10 14:55:33 +07:00
BFlatandClaude Opus 5.5 70ab71bee6 M2: auto layout keyboard reordering and section hide contents (#78)
* 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>
2026-10-10 14:36:02 +07:00
BFlatandClaude Opus 5.5 df2309f5b9 M1: measuring, scale tool handles and selection depth (#77)
Measuring (Alt/Option): distances from the selection to the hovered layer,
its parent (over the selection itself or empty space) and ruler guides;
Cmd+Alt for nested layers; containment both ways, overlaps between matching
edges, gaps with dashed extension lines; rotated layers and several selected
layers by their bounds; the measured box outlined. Alt while dragging shows
live distances to the neighbours (or the parent's edges); Shift+Alt reads out
an auto layout frame's padding and gaps. Labels are placed so they never
overlap each other or the size label and stay the same size at every zoom.
Commands: measure.distances, measure.padding.

Scale tool (K) canvas handles: one factor about the Scale panel's anchor
(tool.scaleAnchor, shared by the panel and the canvas; the opposite handle when
the anchor is on the side being dragged), Alt from the centre, flips past the
pivot, rotated layers and lines along their own axes, one undo step per drag,
a live factor in the size label and the pivot marked. scale_node now also
scales min/max sizes, layout grids, guides, fixed grid tracks, tiled images,
paragraph indent and run letter spacing.

Selection: Cmd/Ctrl-marquee selects nested layers, Shift-marquee toggles,
the context menu's Select layer lists every layer under the click
(select.layersAt), outlines are purple for every layer inside components and
instances, hover outlines show with the Scale tool too. Fixed marquees
selecting layers that only overlapped the marquee on one axis.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:04:17 +07:00
BFlatandClaude Opus 5.5 d3dbc6f495 M3: paint: colour profiles (Display P3), image adjustment presets, style picker (#54)
* M3: colour profiles: sRGB or Display P3 per file, convert or keep values, tagged exports

- color: ColorProfile (srgb, displayP3) with conversions derived from the primaries (shared D65
  white and sRGB curve; out-of-gamut channels clip), table-driven pixel conversion, a generated
  ICC v4 matrix/TRC profile and PNG cICP code points.
- doc: Document.color_profile (serde default sRGB, written only when set); every colour value
  (paints, text runs, effects, grids, page and prototype backgrounds, overlay backdrops, styles,
  colour variables, document colours) can be mapped; set_color_profile converts or reinterprets.
- engine: file.colorProfile {profile?, convert?} (File > Color profile...; without a profile it
  opens the dialog); document.inspect reports colorProfile.
- render: images are taken as sRGB and managed into the file's space; encode_export tags PNG
  (sRGB chunk, or iCCP + cICP for Display P3) and embeds the ICC profile in Display P3 JPG/WebP;
  SVG and PDF export Display P3 files converted to sRGB. The CLI exports the same way.
- ui: Color profile dialog (keep appearance / keep values) and a Color profile row on the page
  panel; the canvas, presenter, thumbnails, swatches and colour picker show a Display P3 file's
  colours converted to the screen's sRGB, and Copy as PNG converts too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: image adjustment presets: built-in, saved in the file, reset

- color: ImageFilters::PRESETS (Vivid, Muted, Warm, Cool, Black & white, Fade, Reset), lookup by
  id or label, and sanitized values.
- doc: Document.image_presets (name + adjustments; written only when there are some).
- engine: image.adjust {ids?, target?, index?, preset?, filters?} sets the adjustments of image
  paints from a preset (built-in or saved) with values on top, one undo step; image.savePreset,
  image.deletePreset and image.presets.
- ui: the colour picker's image section gets a presets menu named after the preset in effect
  ("Custom" otherwise), saved presets with delete, "Save as preset...", a Reset button, and
  aligned adjustment sliders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: style picker: search, folders, list or swatch grid, edit, create, detach

The Fill, Stroke, Effects and Layout grid sections' Styles button opens a popover laid out like
the variable picker (crates/ui-egui/src/style_picker.rs):

- search over style names; styles grouped by the folders of their `/`-separated names, which
  collapse (searching opens them); colour styles as a list or a grid of swatches;
- click applies, a check marks the applied style; the row's edit button (or a right-click) edits
  the name, description and values: colour style paints open the colour picker on the style,
  effect and grid styles update from the selection; delete;
- `+` creates a style from the selection's current values and opens it for naming; detach;
- colour slots still list colour variables to bind;
- the applied style chip opens the picker; new icons Adjust, ViewList, ViewGrid (drawn in code);
- control channel: ui.set {stylePicker, stylePickerEdit, stylePickerView}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:38:17 +07:00
BFlatandClaude Opus 5.5 d53cd60691 M6: Inspect tab: properties with styles and variables, redlines on hover (#41)
The Inspect tab lists the selected layer's properties for handoff:
layout (size, position from the parent, rotation, opacity, radius), auto
layout (direction, gap, padding, alignment), typography, fills, strokes
and effects, each with the style or variable it comes from. Clicking a
value copies it. With the Inspect tab open, hovering measures distances
from the selection without holding Alt.

New command inspect.properties returns the same groups for agents.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:07:48 +07:00
BFlatandClaude Opus 5.5 476cdd95f1 M4: shape builder tool (Shift+M): merge or remove the regions of overlapping shapes (#39)
The selected shapes' outlines split the canvas into regions, computed as the faces of their
arrangement with linesweeper (curves kept, per-shape fill rules, holes and islands handled) and
cached per document version and selection.

- Engine: shapeBuilder.regions / merge / remove / mode commands, ToolId::ShapeBuilder and
  tool.shapeBuilder (catalog: Tools > Shape builder, Shift+M). Merge makes one vector from the
  picked regions styled like the top-most shape over the first; every shape covering a picked
  region loses that area (becomes a vector, or is deleted when nothing is left). One gesture or
  command is one undo step. Session::tool_key routes Enter (finish) and vector edit keys.
- Pathops: faces() (arrangement faces with their covering inputs) and union_faces().
- UI: region edges on the canvas, hovered/collected regions hatched (danger colour to remove),
  no transform box while building, a toolbar pill with Merge/Remove modes, a hint and Done; shared
  pill/tool-cell/divider/Done helpers; original merge/remove icons drawn in code.
- Tests: pathops faces; engine regions, merge, remove, curves, groups, rotation, pointer
  click/drag/Alt, modes, Escape/Enter, cache, junk parameters and degenerate geometry.
- Docs: ROADMAP, README, docs/mcp.md, docs/control-protocol.md, docs/parity.md.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:55:12 +07:00
BFlatandClaude Opus 5.5 360b6df206 M4: local libraries: publish, enable, place and apply, updates with overrides kept (#32)
- 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>
2026-10-10 05:24:46 +07:00
BFlatandClaude Opus 5.5 1c6f04bf62 M3: Variables editor with aliases, scopes, and rename/delete for variables, modes and collections (#33)
The Local variables window becomes a table editor: collections in a
sidebar, variables grouped by name with one value column per mode, typed
value cells (colour swatch + hex + alpha, number, string, boolean), alias
chips with a variable picker and detach, double-click rename, context
menus, and an Edit variable window for description and scopes.

New commands: variable.update (name, description, scopes),
variable.duplicate, variable.renameMode, variable.removeMode,
variable.renameCollection, variable.deleteCollection. variable.set
validates aliases (same type, no loops) and stores them by id; addMode
always picks a fresh mode id. Pickers offer only variables scoped to the
property. ui.set {variables} opens the editor on a collection.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:12:08 +07:00
BFlatandClaude Opus 5.5 076c25f6df M6: Inspect code in CSS, SwiftUI and Jetpack Compose (#15)
- Code generation moves from the UI into the engine (`codegen.rs`): CSS for the layer as before,
  plus SwiftUI and Compose for its subtree (stacks with spacing and padding for auto layout,
  offsets inside a ZStack/Box otherwise, text with font and colour, shapes, fills, strokes,
  shadows, opacity; other layers become image-asset placeholders). String literals are escaped
  per language; deep trees are capped.
- New command `inspect.code {id?, language?}` for the CLI, control channel and MCP.
- Inspect tab: a language picker and Copy for the shown code; Copy as CSS uses the same code.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:45:29 +07:00
BFlatandClaude Opus 5.5 4286a3b74b M2: auto layout depth: min/max UI, ignore auto layout toggle, text baseline alignment, grid tracks and cells (#3)
- Min/max width/height: the W/H sizing menus add min/max (at the current size) and remove them;
  limit rows edit or add the missing bound. The layout clamps Fixed, Hug and Fill children and
  the frame itself; a hugging text wider than its max width wraps at the max. min > max is kept
  ordered by node.set (the other bound follows); in the layout min wins.
- Ignore auto layout (absolute position): a toggle in the Position section; the child stays
  where it is, can't Fill, shows constraints and follows them when the auto layout frame resizes
  (by hugging, filling or a direct resize).
- Text baseline alignment for horizontal layouts ("Align text baselines"; crossAlign baseline):
  first-line baselines line up, other layers align to the top, nested auto layout frames use
  their first child's baseline. Measure gains text_baseline.
- Grid auto layout: column/row tracks (fixed px, hug, fr fill) via columnTracks / rowTracks or
  layout.setTrack, cells anchored by gridColumn / gridRow with gridColumnSpan / gridRowSpan,
  auto placement in reading order, rows grow to fit, hugging grids size to their tracks.
  Panel: column/row gap, track size rows, a child's cell and spans. Old files load unchanged
  (serde defaults; missing tracks are 1fr).
- Canvas handles: wrapped-row gaps and grid column/row gaps are draggable; a selected grid's
  tracks are outlined.
- The layout section moved from right.rs to layout_panel.rs; inspect reports min/max and a
  grid child's resolved gridCell; docs list the new props.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 02:23:43 +07:00
Brandon ThomasandClaude Opus 5.5 3141226a2e Comments: pins, threads with replies, resolve, comments panel, comment.* commands; real timestamps on web
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:06:14 -04:00
Brandon ThomasandClaude Opus 5.5 b36fd771ab M0.9: MCP server + figcraft-cli, PDF export, web app, release/CI packaging, vector edit mode, workspace lint cleanup
- crates/mcp: JSON-RPC MCP server (18 tools, resources, prompts), headless and attached backends, end-to-end agent tests
- apps/figcraft-cli: info/export/convert/render/run/commands/mcp/perf
- crates/pdf: vector PDF export via krilla with embedded text; file.exportFramesPdf
- apps/figcraft-web + packaging/{macos,windows,linux,freebsd,web} + release/ci workflows
- engine vectoredit: edit points/handles/bend/add/delete-heal; canvas overlay and edit strip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:31:53 -04:00
Brandon ThomasandClaude Opus 5.5 56a312ba5f M0.5: layout (auto layout + constraints), format, svg, engine (commands, history, tools, components, styles, variables)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:40:18 -04:00