Commit Graph
3117 Commits
Author SHA1 Message Date
BFlat f2c593ff6e Merge pull request #2117 from Silvertree2010/fix/h11295-node-reparent-silently-turns-a-n
Reject negative or fractional index in node.reparent
2026-11-12 09:20:42 +07:00
BFlat 0fd45a2eb2 Merge pull request #2107 from TrevorBrown-dev/fix/escape-deselects-measured
Selection: Escape clears the selection instead of stepping out to the parent, as in Figma
2026-11-12 09:18:27 +07:00
BFlat d3138da22f Merge pull request #2120 from Silvertree2010/fix/h11642-exportsize-parse-accepts-a-sign
Reject signs and exponents in ExportSize::parse
2026-11-12 09:17:41 +07:00
BFlat 736a1efe1a Merge pull request #2148 from TrevorBrown-dev/feat/scale-tool-focus-2146
Scale tool: K focuses the Scale field, so K, 2, Enter scales by two (#2146)
2026-11-12 09:15:53 +07:00
Trevor BrownandClaude Opus 5.5 20f02bb7c1 M12: Boards: mind maps in the document and engine (#2226, step 1) (#2228)
* M9: Boards: mind maps in the document and engine (#2226)

Part of #2226, step 1 of 4. A mind map node is a board object of its own (BoardItem::MindNode: a frame hugging its text, no box) and a branch is a connector marked `branch`; the tree is read from the branches alone. `board.mindMap` places a first node with three children and `board.mindMapAdd` adds a child or a sibling, each one undo step. The normalize pass (`board::settle_mind_maps`, `figcraft_doc::mind_layout`) lays a touched map out again from its first node: 88 between columns, 48 between siblings, each stack centred on its parent; a branch that no longer joins two layers of its page is removed. Branches are curved, stroke 4, #757575, round caps. Numbers are from a FigJam map saved and decoded. No toolbar entry, shortcuts or import yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 designed; Sonnet 5.5 wrote at medium

* M12: Mind maps: a map around its own container stays put; a moved branch re-flows its old map (#2226)

A map holding a layer and something inside it is skipped by the settle pass (it never settled). Every touched branch now stirs its old ends, so re-attaching a branch's start re-flows the map it left. Boards without branches return early, a map over MIND_MAX_NODES is left whole, shift skips non-finite moves, and the doc tests share one six-node helper.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 reviewed; Sonnet 5.5 wrote at medium

* M12: Mind maps: FigCraft's own starter texts (#2226)

A new map's starter children read "Idea 1", "Idea 2" and "Idea 3", as the design specified,
instead of the incumbent's wording, which the first commit had copied from the measurement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote

* M12: Docs: the mind-map revision rows go in the revision history (#2226)

The 2026-11-10 rows sat in ROADMAP.md's Headline numbers table and docs/roadmap.md's Alpha gate
table; each now opens its file's revision history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 00:47:05 +01:00
Trevor BrownandClaude Opus 5.5 0da027292e Merge upstream/main into fix/escape-deselects-measured
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:55:27 -05:00
Trevor BrownandClaude Opus 5.5 3a4b6f0e1f Merge upstream/main into feat/scale-tool-focus-2146
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:49:15 -05:00
Dylan M. Taylor 3b9bf8c9a2 batch: bound how much referenced data one step's params copy (#2257)
* batch: bound how much referenced data one step's params copy

* batch: check a list's size before queuing its elements
2026-11-11 15:55:11 -05:00
24cf70329b M5: Locked layers follow Figma's rules: no nudge, a locked parent's children stay locked (#1560) (#2219)
* M5: Locked layers follow Figma's rules: no nudge, a locked parent's children stay locked (#1560)

Pins Figma's locked-layer behaviour (measured in Figma's web app, 2026-11-10; help center "Lock and unlock layers") in a new lock_behaviour_tests suite. Fixes the divergences: transform.nudge and transform.nudgeSize skip locked layers, object.toggleLock without a value counts a child of a locked parent as locked (so it never locks the child on its own), and the Layers panel padlock on such a child is inert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 briefed; Sonnet 5.5 wrote at medium

* Layers panel: a padlock sweep skips the children of a locked layer (#1560)

A padlock drag sweep no longer collects rows with `locked_above`, so
dragging across a locked frame's children leaves their own flags alone
and draws no swept value on them. An eye sweep is unchanged.

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

* M5: Lock/Unlock leaves a locked layer's child alone in a mixed selection (#1560)

With a locked frame's child and an unlocked layer selected, Lock/Unlock set both own flags, so
the child stayed locked once the frame was unlocked. With no explicit `locked` value, the command
now skips a layer whose parent is locked, as its padlock does; an explicit value (scripts) still
sets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-11 21:06:56 +01:00
Dylan M. Taylor 898bfc2e60 Fix Flatpak updates and bump to 0.17.1 (#2267)
* fix(flatpak): allow release checks and self-update helpers

* test: keep Flatpak permission parsing within strict Clippy rules

* chore: bump version to 0.17.1 for the Flatpak update fix
2026-11-11 15:01:36 -05:00
Dylan M. Taylor 554ad0ea77 pathops: budget the closed faces input, share the dash allowance, index point incidence (#2249)
* pathops: check closed inputs before faces normalises, share the dash allowance across subpaths, index point incidence once

* pathops: leave out move-only subpaths when closing, budget dashes by the fitted pattern, index only join points
2026-11-11 14:49:49 -05:00
Dylan M. Taylor 81371f4a53 render: seed and mix the node-id cache hash, skip the cell allocation past the pattern limit (#2254)
* render: seed and mix the node-id cache hash, skip the cell allocation past the pattern limit

* render: use hash_one in the bucket test
2026-11-11 14:49:35 -05:00
Dylan M. Taylor 9032dc3e67 svg export: embed each image's bytes once per file (#2250) 2026-11-11 14:49:20 -05:00
Dylan M. Taylor 64e91207f1 svg import: bound the layer stack a filter chain builds (#2252) 2026-11-11 14:49:06 -05:00
Dylan M. Taylor 6e9582c0a0 codeConnect.import: index components by name and describe each once (#2265) 2026-11-11 14:48:52 -05:00
Dylan M. Taylor 6bc8bbaef1 inspect: work out a grid's cells once per inspection (#2260) 2026-11-11 14:48:38 -05:00
Trevor BrownandClaude Opus 5.5 cf17c66a13 Merge upstream/main into feat/scale-tool-focus-2146
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:39:49 -05:00
Trevor BrownandClaude Opus 5.5 e829590fdf Merge upstream/main into fix/escape-deselects-measured
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:39:38 -05:00
rumangerst 7bf098d75d fix(ui): paste uses the event's clipboard text instead of re-reading it (#2021) (#2243)
The paste handler read the system clipboard synchronously (2 s timeout) even
though the paste event already carries that text, and discarded the read's
result. On the desktop, the clipboard bridge delivers `event_text` that it read
while the window thread was free; re-reading from the paste handler blocks that
same window thread - and when the app itself owns the clipboard (a self-paste),
its own data source can only be served by that very thread, so every self-paste
froze the window for the full 2 s read timeout on Linux/Wayland.

Prefer the event's text and only fall back to the synchronous read when the
platform handed over none (chords, image-only clipboard, context-menu paste).
A regression test pins that a paste event carrying text never triggers the
synchronous read.
2026-11-11 15:37:20 +01:00
Trevor BrownandClaude Sonnet 5.5 2dd1b5a53a Menus: the main menu and its submenus stay inside the window and scroll when taller (#2217) (#2220)
Submenus of the main menu were not constrained to the window (constrain: false, so they sit
beside their row), so the Object submenu ran past the bottom edge with its lowest commands out of
reach. Menus and submenus are now held within the window's height (moved up, never above the top,
8 pt margins, still beside their row so menu-aim is unchanged), and a panel taller than that
scrolls with the wheel; the keyboard's focus scrolls into view.


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>
2026-11-11 15:32:21 +01:00
Pavel Ivanov 9e815546ba M8: switching back to a tab shows its page at once (#2075)
* **No blank canvas** when switching back to a document tab or to a page in a file: the image it had when last shown is drawn from the first frame while it renders again
* Images of up to four recently shown files and pages are kept; closing a tab frees its image right away

Refs: #2074
2026-11-11 15:05:04 +01:00
Trevor BrownandClaude Sonnet 5.5 401f449729 M8: zoom previews: scan system fonts before the zoom-preview tests render (#2230)
pans_and_edits_get_no_preview_and_build_on_the_kept_image failed in full lib runs with "a pan after a previewed zoom renders strips only: Full". The render kept from the zoom is stale once the global font generation changes (retained.rs prepare: last.fonts == layout_generation()), and the once-per-process system font scan that many other tests start bumps it (fontdb.rs scan_system_fonts -> clear_layout_cache). When that scan landed between the test's zoom and its pan, the pan redrew in full instead of patching. Unrelated to the clock or the threshold. document() now runs scan_system_fonts() first (a concurrent caller waits for the scan), so the generation is settled before any render. Test-only change; the shipped staleness check is intended behaviour.

Stress, full lib binary with 16 test threads: 3 of 6 runs failed before, 0 of 25 after.


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>
2026-11-11 15:02:45 +01:00
Dylan M. Taylor 0f08269067 fix(collab-net): never send the server key over plain ws:// to another machine (#1923)
* fix(collab-net): never send the server key over plain ws:// to another machine

Publishing a plugin, uploading or sharing with the server's key accepted a
ws:// address for a remote host, so the reusable administrative key went
out in clear text. The key now goes only over wss://, or over ws:// to
this machine (loopback or localhost); browsing without a key is unchanged.

* address review: connect only to loopback addresses for a plain ws://localhost endpoint
2026-11-11 08:36:29 -05:00
amorSoftLabsandbflatastic 2af535c132 fix(ui): access preferences from home screen via shortcut and sidebar (#956) (#962)
* fix(ui): access preferences from home screen via shortcut and sidebar (#956)

* fix(ui): handle preferences shortcut before popup guard

* docs: add docstrings for modified functions to satisfy coverage check

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-11-10 17:58:20 +01:00
Trevor BrownandClaude Sonnet 5.5 7a8d422dec M8: the Scale field only takes the keyboard when the tool is chosen or the panel returns, and Ctrl/Alt+Enter keep the tool (#2148 review)
A stale "field had the keyboard" flag no longer ends the tool on the first frame back from another tab. A selection appearing after K with nothing selected no longer moves the keyboard from the canvas to the Scale field. Ctrl+Enter and Alt+Enter are not the tool's exit. Tests pin the full text selection, the 2x and 1.5 factors, the field letting go after Enter and Escape, idle frames after Tab, and a leftover edit dropped on return.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 triaged and briefed; Sonnet 5.5 verified and wrote at medium
2026-11-09 14:47:06 -05:00
skylar taylor-barrick a2e87433d8 Naming: Developer Mode follow-ups (#2165)
* Naming: record Developer Mode in the feature-names doc and the stage paragraph

docs/feature-names.md gains the entry for the rename: the old label, the new one, Figma's
US registration for DEV MODE (7574348) as the reason with its TSDR record, the identifiers
that keep their names, and the translations that moved from a generic "dev mode" to the
developer mode. The stage paragraph in ROADMAP.md listed the mode as Dev Mode.

* Actions palette: put each search doc comment back on its function

search_text had grown a second doc line under matches's, so the sentence about the query sat
above a function that takes none. search_text now says what it reads and matches keeps its own.
2026-11-09 15:44:02 +01:00
Trevor BrownandClaude Opus 5.5 197a390a84 M8: the Scale field refocuses after another tab, and Enter elsewhere keeps the tool (#2148 review)
The focus request was reset only while the Design panel drew, so choosing the tool again on the
Prototype tab left the field without the keyboard; it now follows the frame the section last
showed in. Whether the factor field had the keyboard was read before the widgets ran, so an
Enter right after a click into W or H could end the tool; it is read after them now. A number
field's leftover edit from when it was last shown no longer swallows the focus request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
2026-11-09 03:40:27 -05:00
Trevor BrownandClaude Opus 5.5 6856437479 M8: the Scale panel keeps FigCraft's origin; the centre default is a follow-up (#2146)
The panel set the origin to the centre once per app. The origin is the Scale tool's drag pivot
too (scale_factor), so that also changed handle drags, unmeasured against Figma, while the
engine and its tests keep top-left. This PR keeps to the focus and the Enter and Escape
endings the issue asks for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
2026-11-09 03:03:26 -05:00
Trevor BrownandClaude Sonnet 5.5 92145e6f8d M8: K focuses the Scale field, so K, 2, Enter scales by two (#2146)
The Scale tool now opens its factor field focused with its text selected (once per activation), the origin starts at the centre, and Enter or Escape in the field returns the tool to Move with the selection kept. Measured in Figma web, 2026-11-09. Docs: ui-parity Scale tool row and revision.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and briefed; Sonnet 5.5 measured and wrote at medium
2026-11-09 02:59:53 -05:00
skylar taylor-barrick 3839fe4a0c Naming: use Developer Mode instead of Figma's Dev Mode (#2135)
Figma holds a US registration for DEV MODE (US 7574348). Display text only: the view.devMode label, Inspect tab, shortcut row, refusal messages, CLI help, docs and all locale catalogs now say Developer Mode. Command ids, preference keys and saved fields are unchanged; "Dev Mode" stays a search alias in the Actions palette. Adds a naming guard test.
2026-11-09 06:24:44 +01:00
FredApps 72cfe7be30 fix(naming): use direct FigCraft feature descriptions (#2131)
Drops the "Figma-class" self-description from the MCP instructions and the "Accessibility at Figma" aside from pref.uiScaleUp help. Help text only; ids and preference keys unchanged. Adds a display-name guard over mcp/src and engine/src/cmd string literals.
2026-11-09 06:22:48 +01:00
Trevor BrownandClaude Opus 5.5 92cf984902 Selection: Escape tests for a top-level layer, several layers and the focused frame (#2107 review)
One Escape selects nothing for a top-level frame, a top-level layer, two layers in one group,
layers in different parents, and a nested layer with a top-level one. With the focused frame
itself selected, one Escape leaves the focus view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
2026-11-08 17:38:13 -05:00
dotCooCoo 892cda8e34 Updates: the updater reports installing, ready, installed and failed, and installs at exit
craft-update moves to a craft-libs branch that merges storytold/craft-libs#687 and #696.

- The check result window shows for every status but idle and stays
  closed while the startup prompt is open, so the first start after an
  update reports that it installed or why it failed. Restart to Update
  calls Updater::restart_to_update.
- on_exit calls Updater::start_pending_update before the process ends,
  which starts the install of an update waiting for the app to close.
2026-11-08 13:32:01 -08:00
FredApps d54439be8b M6: scale imported vector point radii with their instance row (#2097) 2026-11-08 16:10:54 +01:00
FredApps a07a21b697 M6: bound exported instance factors to the supported range (#2097) 2026-11-08 16:08:34 +01:00
FredApps 23ed778562 M6: preserve scaled instance export overrides (#2097) 2026-11-08 15:23:36 +01:00
NetterSilverandClaude Haiku 5.5 6222fce8d1 Reject signs and exponents in ExportSize::parse
Found while testing crates/doc/src/props.rs:1394 (ExportSize::parse, `num.parse::<f64>()`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 12:22:45 +00:00
NetterSilverandClaude Haiku 5.5 10d97e2716 Reject negative or fractional index in node.reparent
Found while testing crates/engine/src/cmd/edit/props.rs:248 (reparent, `index = num(p, "index").map(|i| i.max(0.0) as usize)`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 11:00:14 +00:00
Trevor BrownandClaude Sonnet 5.5 a285fd293b Selection: Escape clears the selection instead of stepping out to the parent, as in Figma
Measured in Figma: with a layer inside a group inside a frame selected (from the canvas, from the Layers panel, or after Tab), one Escape selects nothing; FigCraft selected the parent first. Escape still leaves text and vector editing, ends gestures and the pen first. Three tests updated to the measured rule (selection_behaviour, devfocus, proto_editor); the rest of the suites pass.

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 low
2026-11-08 00:11:49 -05:00
Trevor BrownandClaude Sonnet 5.5 9303b73991 .fig import: the round-trip test asserts the scaled size; text on a path takes the instance's size (#2100 review)
The scaled-instance round-trip test now asserts the scaled width and height instead of printing them. text_path read the layer's raw size, so a text on a path inside a scaled instance kept the main's path length; it now uses the scaled or fitted size.

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
2026-11-07 22:58:07 -05:00
Trevor BrownandClaude Sonnet 5.5 c7c5a4f87d .fig import: a nested instance's own rows take its scale; blur positions and swapped styles are not rescaled (#2100 review)
- A nested instance's override and derived rows (and its text property values) are written in the enclosing main's space: OverrideRow carries the factor of the layer that supplied it and View::with takes (row, scale) pairs. Test: a_nested_instances_own_rows_are_at_its_enclosing_mains_scale.
- Progressive blur startOffset and endOffset are unit-box fractions, not lengths: only offset and noiseSize scale. Test: a_progressive_blurs_positions_are_not_lengths.
- An effect or grid style takes the factor of the layer that links it (1 when an instance row swaps it), not the main's. Test: an_effect_style_takes_the_scale_of_the_layer_that_links_it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 reviewed; Sonnet 5.5 wrote at high
2026-11-07 22:23:39 -05:00
Trevor BrownandClaude Sonnet 5.5 62d761a7bc M6: .fig import draws a scaled instance's layers at its scale factor (#2091)
An instance scaled with the Scale tool in Figma stores `symbolData.uniformScaleFactor`; the importer never read it, so the layers of a 128 px instance of a 256 px component came in at 256 px with the main's radii, strokes and spacing. Layers of an instance now take the main's lengths times the factor (composed through nested instances), the same values the Scale tool scales, unless an override or derived row gives the value, which is already at the instance's scale. Eight tests: the reproducer, the round trip, a derived size row not scaled twice, override rows, auto layout gap/padding/text, nested composition, factor 1 or none, and junk factors ignored.

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 high
2026-11-07 22:06:35 -05:00
bflatastic 2c09046a27 Merge main into #1729 2026-11-07 12:47:51 +07:00
bflatastic 4106c26e8c Merge main into #1729 2026-11-07 12:30:05 +07:00
bflatastic 70821e3645 Merge main into #1879 2026-11-07 12:29:18 +07:00
bflatastic 3f6aa299ea Merge main into #1873 2026-11-07 12:15:09 +07:00
bflatastic 9278ee5451 Merge main into #1729 2026-11-07 12:14:41 +07:00
bflatastic 12293e2e55 Merge main into #1879; shortcut counts with #1858's row 2026-11-07 12:14:30 +07:00
BFlat 05e1bcebf8 Merge pull request #2061 from storytold/carry/1952
fix(cli,ui): escape control characters in document and plugin text sent to the terminal
2026-11-07 13:13:29 +08:00
BFlat 9bec55c05a Merge pull request #1911 from TrevorBrown-dev/fix/layers-component-contrast
Layers panel: component rows in the theme's component tint, at AA contrast (#1903)
2026-11-07 13:12:56 +08:00