- "Past year" was "Viimeinen vuosi" (the last calendar year); the filter means
the last 12 months: "Viimeiset 12 kuukautta" (CodeRabbit on #1993).
- 40 UI strings (pick select, field scrubbing, live session reconnect, text
direction, padding, variable scopes), 7 command labels, 3 menu wordings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut with nothing to copy (only points that touch no segment) is Disabled and changes nothing, as Copy now is (Copy used to fall through to the whole layer); paste borrows the clipboard instead of cloning it; a partial copy or cut mirrors its SVG to the system clipboard; x without y (or y without x) is refused; the junk test asserts refusals change nothing and accepted results are finite; the edit.copy, edit.cut and edit.paste specs state the behaviour and the mode-specific paste response.
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
Found while testing crates/engine/src/cmd/file/export.rs:89 (export; same pattern at :332 in export_layers)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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
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
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
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
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
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
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
- 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
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
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
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
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
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
"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
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
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
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
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
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
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.
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>
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>
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>
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>
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.