Commit Graph
4092 Commits
Author SHA1 Message Date
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 75f54e0ae9 View: the arrow keys pan 64 px, 320 with Shift, as measured in Figma
With nothing selected, one arrow press panned 20 px (200 with Shift); Figma pans about 64 px and five times that with Shift (measured at 100%, about +/-3 px). New engine test pins both steps; no existing test pinned the old values.

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-07 23:59:04 -05:00
Trevor BrownandClaude Sonnet 5.5 d14c637de8 Text: tests for the layer-level wrap style, the Type settings control, old files and inspect (#2094 review)
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 23:47:15 -05:00
Trevor BrownandClaude Sonnet 5.5 0fec344f3c M8: copy, cut and paste in vector edit mode act on the selected points and segments (#1875)
Measured in Figma: in vector edit mode Copy takes only the selected segments (a lone point, the segments touching it) and a paste outside makes a new vector in place; Cut removes them, splitting the path in the same layer and staying in edit mode; Paste while editing adds the clipboard's segments or a copied layer's outline to the vector's network in place, selecting them. FigCraft copied the whole layer, Cut left nothing to paste, and Paste while editing did nothing. 16 new engine and UI tests cover copy, cut, paste, undo and junk parameters; the whole engine suite passes bar the known font test.

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 23:38:58 -05:00
Trevor BrownandClaude Sonnet 5.5 ad7a886e3c Panels: the sidebars' width limits are Figma's (left 240 to 450, right 240 to 500)
Measured in Figma's web app: left sidebar 240 default, 240 min, 450 max; right
240 default, 240 min, 500 max; a double-click on the edge resets. FigCraft had
left 200 to 480 and right 240 to 480. Widths saved under the old limits are now
clamped when the app starts, not only when drawn. The narrow-window give-way
(canvas keeps 160 px) is unchanged, with the new 240 floor.

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 23:17:00 -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 9c3949fc92 Variables: Edit variables is offered only when it can open; tests for the bulk editor's paths (#2089 review)
The bulk menu already offers Edit variables only for two or more editable, non-inherited variables, so the editor's early close is just a safety net (now commented); a test pins that inherited selections get no entry. A checkbox is already keyboard-operable (Tab, then Space or Enter), pinned by a test. tri() of an empty list is now unchecked. New tests: All supported properties on and off, clearing a checked scope, booleans sharing no scope, mixed-type common scopes, and the painted dash versus tick.

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:50:11 -05:00
Trevor BrownandClaude Sonnet 5.5 253e0c3df3 Components: Create component set refuses nested layers and is translated (#2095 review)
component.createSet now refuses a selection where one layer is inside another, as component.combineVariants does, before changing the document (the nested layer was silently dropped from the set). Adds the component.createSet label to the 22 locale catalogs that list component.createMultiple.

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:49:04 -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 a375ac0689 Format (#2092)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 22:15:41 -05:00
Trevor BrownandClaude Sonnet 5.5 34c44e6063 Text: a font loaded with text.loadFont wins over the built-in and installed faces of the same family and style (#2092)
FigCraft chose the first face added for a family and style, so the embedded Inter 4.x kept Regular, SemiBold and Bold after Inter 3.19 was loaded and only Medium refitted. A loaded font is the counterpart of a font uploaded to the organization, which Figma's help center ranks first ("Manage conflicting fonts"). The test builds its faces from any embedded or installed font (skipped when there is none) and checks a loaded face beats a built-in one, the latest load beats earlier ones, and other styles resolve as before.

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:15:22 -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
Trevor BrownandClaude Sonnet 5.5 8a78214b48 Widgets: a mixed checkbox is announced as mixed (#2089 review)
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 21:44:01 -05:00
Trevor BrownandClaude Sonnet 5.5 524ad8b1dd Components: Create component set in the component button's menu (#2088)
The third entry of the menu several selected layers get: each layer becomes a component and they are wrapped in a component set in place, 20 px around, named as Figma names them (measured): "Property 1=<layer name>" in left-to-right order, prop=value names kept, and layers that all share a name give a set of that name with "Default" variants. Engine tests cover the three measured cases, geometry, selection, one undo and the refusal of instances; the UI test checks the three menu entries in order and that the third makes one set.

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 21:38:30 -05:00
Trevor BrownandClaude Sonnet 5.5 c4302902d2 Menus: the sweep test checks the submenu shown is the hovered row's (#2093 review)
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 21:36:45 -05:00
Trevor BrownandClaude Opus 5.5 071cc86bdf Docs: the .fig note on text styles leaves out the wrap style (#2094 review)
components-parity.md said a text style's wrap style is kept by .fig files; the .fig mapping is still open (ui-parity.md, mcp.md and the PR say so), so the row now says so too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
2026-11-07 21:30:39 -05:00
Trevor BrownandClaude Sonnet 5.5 a1ac22f12f Transform: Shift while rotating lands on multiples of 15°, not 15° steps from the start
Measured in Figma: a layer at 7° (or -4.27°) Shift-rotated by its corner ends at 15°; FigCraft rounded the turn since the drag began, so a layer at 7° landed on 7 + k*15. The rotate gesture keeps the selection's shared rotation and Shift rounds base + turn; a selection with different rotations keeps rounding the turn. Test: a layer at 7° Shift-turned by 37° ends at 45°, by 3° at 15°, and a free turn 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
2026-11-07 21:23:25 -05:00
Trevor BrownandClaude Sonnet 5.5 f5a52ea528 Components: several layers selected, the component button offers Create multiple components (#2088)
With two or more layers selected, the Design panel header's component button opens a menu with Create component and Create multiple components, as Figma's does; one layer keeps the plain button. Headless UI tests cover both cases (three frames: menu order, names kept, all selected, one undo; one frame: plain click), and an engine test pins createMultiple's names, positions and single undo step.

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 21:20:56 -05:00
Trevor BrownandClaude Sonnet 5.5 136ca4cda7 M4: a wrap style per paragraph and in text styles, overriding the layer's (#1827)
"You can apply a wrap style at three levels: Text layer, Paragraph, Text style". The layer level was done in #1871; this adds the other two, following the per-paragraph text direction (#1830) and the list style in text styles (#1876).

ParaList::wrap (None follows the layer) with para_wrap_style, set_wrap_style (the layer level, clears overrides), set_para_wrap_styles and common_wrap_style; restyle_breaks takes a style per paragraph; ParagraphStyle::wrap_style; text.wrapStyle and node.set wrapStyle with start/end; inspect lineWrapStyles; CSS text-wrap only when the paragraphs agree; the Wrap style control follows the paragraph; a read-only row in the style editor. The .fig mapping is left for a later change.

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
2026-11-07 21:13:39 -05:00
Trevor BrownandClaude Sonnet 5.5 35ac3a92d5 Menus: a submenu opens as soon as its row is hovered, as measured in Figma
Figma's main menu opens a submenu on the first frame its row is hovered (median 11 ms) and switches between rows in about 13 ms; FigCraft waited 0.15 s. The hover delay is now zero (the constant stays, so the pending-row bookkeeping the aim uses is untouched); the safe-triangle aim stays until the maintainer decides, since Figma has none.

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-07 20:56:31 -05:00
Trevor BrownandClaude Sonnet 5.5 a20415e154 Variables: Edit variables edits Hide from publishing and scopes of several variables at once, with mixed states (#2082)
With several variables selected, the bulk menu's Edit variables opens the reference's compact editor: Hide from publishing and the scopes, each shown mixed when the selection differs, each change applied to every selected variable in one undo step.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and briefed from a Chrome measurement; Sonnet 5.5 wrote at medium
2026-11-07 20:47:09 -05:00
Trevor BrownandClaude Sonnet 5.5 3f39154a9f Export settings: the round-trip test checks every field's value and the snake_case form
The test now compares the stored setting with a constructed expected value (every multi-word field, print and quality at a non-default value) after export.add and after the node.get -> node.set round trip, and feeds the snake_case keys to node.set directly, so a field dropped by a write cannot pass and the PrintSettings aliases are exercised.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and briefed; Sonnet 5.5 wrote at medium
2026-11-07 20:42:19 -05:00
Trevor BrownandClaude Sonnet 5.5 6c3ac0c55b Export settings: node.set takes back what node.get returns (snake_case aliases)
node.get reports an export setting's two-word fields in snake_case, which node.set refused, so a setting could not be read and written back unchanged. ExportSetting now accepts those names as serde aliases; the camelCase names stay the canonical form. Follow-up the maintainer suggested on #2008.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and briefed; Sonnet 5.5 wrote at medium
2026-11-07 19:44:51 -05:00
Trevor BrownandClaude Opus 5.5 0c1fb11c48 Render band test: the comments say what row_stats divides by and what the artifact case measures (#2080)
row_stats sums the differences of the pixels above noise and divides by the whole row's channel count; the doc now says so. The artifact case changes only the colour channels, so its mean is 2.25, which the earlier allowance let through, and its comment now says it fails on the mean while its count stays within a twentieth of the row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote at high; no brief
2026-11-07 19:37:16 -05:00
dotCooCoo 97f086e091 Docs: the Display P3 export sentence names RGB PDF and says CMYK PDF ends as DeviceCMYK
The colour profile paragraph in docs/mcp.md said SVG and PDF export
Display P3 files converted to sRGB, without the RGB qualifier the export
tool text now carries. It now says SVG and RGB PDF convert to sRGB, and
PDF with `cmyk` converts to sRGB the same way and then to DeviceCMYK.
The comment above the export match in file.rs says the same.
2026-11-07 16:36:59 -08:00
Trevor BrownandClaude Fable 5.1 0774e44fa8 Render band test: only small antialiasing differences are forgiven, a real band artifact still fails (#2080)
Pixels whose largest channel differs by 24 or less are noise and left out of the row mean, which keeps its original 0.25 limit; the differing-pixel limit is unchanged. A unit test shows a 44-pixel row off by 60 still fails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-07 13:41:25 -05:00
Trevor BrownandClaude Fable 5.1 4118be890c Ellipse: the four-cubic contour is closed, and the band test allows apex antialiasing (#2080)
The band test compared row 10, the ellipse top, where the cubic now ends exactly at its level extreme; 8 pixels antialias slightly differently between a short band and the whole view. The row mean limit now grows by 0.05 per such pixel; whole-row artifacts still fail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-07 12:42:44 -05:00
Trevor BrownandClaude Fable 5.1 b33da8a7b0 Ellipse: an unsliced ellipse edits as four anchor points, not six (#2080)
A whole ellipse and each ring contour are built as four cubics through the extremes instead of a tolerance-subdivided arc. Sliced ellipses are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-07 11:52:52 -05:00
NetterSilverandClaude Haiku 5.5 386c6c1d3e Reject negative and fractional index in image.savePreset
Found while testing crates/engine/src/cmd/image.rs:239 (save_preset, `index = num(p, "index").filter(..).map(|k| k as usize)`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 13:45:31 +01:00
Davit Matchakhelidze fbd313f4e7 i18n(ka): Georgian for 126 new strings 2026-11-07 12:50:18 +04:00
dotCooCoo 12ade9c78c Docs: the 2026-10-31 export entry keeps the JPG tagging it had on that date 2026-11-06 23:31:02 -08:00
Helen Kwok d252d35f27 figcraft-web: inspect and openFile go through craft-web-api, so they time out and cancel like the other functions
inspect is docInspect under its earlier name, and openFile calls the crate's execute with file.open, so a call the app does not answer within 30 seconds rejects and a request the app has not started is cancelled.
2026-11-07 15:30:01 +08:00
Patrik LermonandClaude Opus 5.5 bc2984a998 i18n(sv): translate 69 new strings
Brings the Swedish catalog up to date with upstream main (3635c230).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 07:55:28 +01:00
dotCooCoo 297426c595 Export: color profile text states which formats are tagged and that CMYK PDF is DeviceCMYK
The export tool and command descriptions, docs/mcp.md and the file-format
table said PDF is always sRGB, that sRGB JPG is untagged and that every
exported file is tagged with its profile. RGB PDF is sRGB and PDF with cmyk
is DeviceCMYK; JPG embeds an ICC profile for sRGB and Display P3; WebP embeds
one for Display P3 only. The JPG quality (1 to 100, default 90) now has one
row in the format table.
2026-11-06 22:21:51 -08:00
BFlat 3635c23070 Merge pull request #1729 from avhn/fix/553-rail-tab-icon-size
M7: the left rail and tab strip use larger icons (#553)
2026-11-07 14:00:36 +08:00
bflatastic 2c09046a27 Merge main into #1729 2026-11-07 12:47:51 +07:00
BFlat 4cc4079731 Merge pull request #1879 from TrevorBrown-dev/feat/pick-select
M1: pick select: Tab while the keyboard selection box shows walks the layer tree with a pink outline, Space selects (#1836)
2026-11-07 13:47:06 +08: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
BFlat dd9ec40b5f Merge pull request #1873 from TrevorBrown-dev/fix/scrub-speed
M1: scrubbing a field faster or slower by the pointer's height, with a notice of the speed (#1837)
2026-11-07 13:28:57 +08: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
BFlat 808e60eeae Merge pull request #1772 from SoldierPePe/i18n/rtl-window-mirror
M10: a right-to-left UI language mirrors the window
2026-11-07 13:11:40 +08:00
bflatastic 1546d29a66 Merge main into #1729 2026-11-07 12:07:03 +07:00
BFlat 58a46033f0 Merge pull request #1774 from TrevorBrown-dev/fix/geom-bounded-sizes
M8: shape builders cap sizes, radii and angles at 1e9, so an absurd value cannot build millions of segments
2026-11-07 13:06:11 +08:00
BFlat 5cbc8207fd Merge pull request #2065 from helenkwok/web/page-api
figcraft-web: window.figcraft takes its request functions from craft-web-api
2026-11-07 13:03:10 +08:00
BFlat 31c54bfc99 Merge pull request #2068 from Silvertree2010/fix/h7240-react-tailwind-export-breaks-the
Fix Tailwind export breaking JSX when font family has quotes or brackets
2026-11-07 13:00:53 +08:00