Typing `5. ` at the start of a plain paragraph starts a numbered list whose first item reads 5 (Figma has no start-number field); the next item goes on with 6. Digits only, 1 to 9999; `0. `, `12a. ` and `2)` stay text (not measured), `1)` works as before. The measured restart rows (plain paragraph, bullet item) are no longer marked silent.
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 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.
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>
* **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
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>
* 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
On a fixed-size axis a Fill (1fr) track now splits the free space by its fraction, after gaps and padding, and a fixed-size or hug-size child alone in it asks nothing of the track, so a child bigger than its share overflows into the next track. Measured in Figma web: frame 300 x 200, rows 1fr / 1fr, children 100 x 150 and 100 x 20 give rows 100 / 100 (second child at y 100); with a row gap of 10, rows 95 / 95 (y 105); with the frame height on Hug the rows fit their content, 150 / 20 (frame 170). This replaces the 2026-10-11 rule from the documentation (a track never gets less than its content). Unchanged: a Fill child's own minimum, spanning children, Hug and Fixed tracks, the Hug-axis path.
Three behaviour cases flipped to the measured rule and three added with the measured numbers; the two parity docs are updated.
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 closing pass moves out of reopen_loops (back to main's body) into close_bridged_loops, run only by remove_segments and delete_vertices when the segment or point count changed. Every loop takes what closing_segment returns against one snapshot, so two fills that shared a deleted border both close through the bridge, as rounding already draws them. The owner map is gone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 decided; Sonnet 5.5 wrote at medium
Flatten used the vector's network as stored, so a rounded point kept its radius and the box kept the cut-off sharp vertex. It now takes the network as the layer draws it (VectorNetwork::rounded_smoothed through geometry::drawn_network): each rounded point becomes the arc's end points joined by curves, every radius reads 0, and the layer's box is re-fitted to the baked geometry. Shapes' own radii were already baked; a rounded point beside a curved segment keeps today's behaviour.
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
Checking a loop's closing segment against every closing already accepted was quadratic in the
number of open loops. A map from closing segment to the chain that took it first answers it in
one lookup; behaviour is unchanged. A test closes 50 independent chains through their own
segments.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
Two regions over one open chain both got the closing segment when rounded, but reopen_loops gave it to the first loop only, so after an unrelated deletion the second region's curved closing segment dropped out of its fill. The pass now takes the segment for every loop that walks the same chain (same segment set); a different chain still can't share it.
closing_segment no longer scans every segment and every loop per call: rounded_smoothed and reopen_loops build one LoopIndex (endpoint pair to segments, and which segments are in a loop) per pass and pass it in.
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 Vietnamese row said "cấu hình của tệp" (the file's configuration)
for the file's profile. It now says "hồ sơ màu của tệp", the term the
Color profile label uses.
An export setting set to sRGB still writes sRGB files in a Display P3
document, so the note no longer says every exported PNG, JPG and WebP is
tagged Display P3. Follows #1759.
A Pen bridge between two degree-2 points leaves the region alone, and Delete on the point between them then rotated the loop open with the bridge in no loop, so even an unrounded fill ran straight across a curved bridge. reopen_loops now pushes the one segment in no loop that joins an open chain's ends onto the loop's end (winding kept, loop by loop), as connect does. This also covers removing segments and the eraser.
closing_segment is public, derives its own start point, walks strictly (a loop out of walking order is left alone) and gives up when two segments qualify. The render fix stays as the fallback for files already saved in that state.
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
The reporter's file has a corner cut off by a segment drawn first, with the corner point then deleted with Delete. The region's loop is left as the open chain of the other four segments, and the cutting segment, which joins the chain's two ends, is in no loop. The stroke follows the network, so it rounds both ends of that segment; the fill and the selection outline follow the loop.
Cause: VectorNetwork::rounded_smoothed rebuilds each loop by inserting a corner's arcs where the loop arrives at the point, so the point the loop starts at is never cut (in a closed loop it is arrived at last), and the open loop's closing edge ran straight to it. closing_segment now finds the segment joining an open loop's two ends when no loop holds it, and rounding walks it as the loop's last segment, so every corner of the fill and outline matches the stroke. Closed loops and the cases #1991 fixed are untouched.
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 frame's Swap orientation button and the selection's quick-action buttons (24 pt) still
painted their glyphs at the old 14 pt inset beside Fit to contents' 16 pt; they use
`icon_glyph_rect` now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
A live check showed the header's eye still smaller than a fill row's: `icons::button`, which
draws the fill rows, paints the icons' native 16 px glyph in a 24 pt button, while the shared
inset gave 14. The inset is now 4, so section headers, `icon_button_at` and the panels' Close and
Swap buttons draw 16 pt glyphs like the rest. The test compares the header's eye with both
`icon_button_at` and `icons::button`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
A row's fill copied through the control channel, then a stroke copied by
a command, left the fill off the engine clipboard; a programmatic paste
then fell through to the layers copied before the fill. It now does
nothing, as a user's paste already did.
Tests: the programmatic case above, an effect paste replacing the
highlighted effect above the bottom one (index 1), and a property copied
in one window pasting in another while the highlight stays in its window.
Section header buttons painted their glyph in a 12 pt box inside the 24 pt button while icon_button_at used 14 pt. Both now use icon_glyph_rect (ICON_BUTTON_INSET 5 pt), as do the 24 pt Close and Swap buttons in the assets and prototype panels. Test compares the painted Eye bounds of a header and an icon button.
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 tolerance that drops the sweep's slivers came from the whole path's bounding box, so a
contour a million units wide welded away the real half-unit edges of a separate small contour.
Each closed subpath now sets its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
A boolean group, polygon, star or vector draws its first radius. Per-corner values a script or a
file left behind made its field read Mixed, and a relative edit started from the largest. The field
now shows radii[0] for those kinds, and a relative edit starts from it; rectangles and frames are
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
The field test's "no field per corner" check passed for any shape, since corner fields are built
only while the individual-corners popover is open. A boolean group's button now has its own test
beside the polygons' (shared helper): it is named Corner smoothing, opens no corner fields, and
sets the smoothing in one undo step. ROADMAP's status line reads minor, as the other docs do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote; Fable 5.1 reviewed
A boolean group takes a corner radius (and corner smoothing): the Design panel shows the field for it, `node.set cornerRadius` writes it, and the renderer rounds every sharp corner of the group's result after the fold, convex and concave alike. Layers inside keep their own radius and do not stack with it, a radius larger than the edges allow shrinks to fit while the field keeps the typed value, Flatten bakes the arcs, and a mixed selection reads Mixed. Measured in Figma's web app on 2026-11-10.
The rounded result is cached apart from the fold (keyed by the folded path, radius and smoothing), so scrubbing the radius never redoes the boolean steps. The .fig and REST importers and the .fig exporter now carry the radius and smoothing of a boolean group, and paste properties and radius variables reach it. Docs: ui-parity, target-app-parity, gaps, file-format-parity, ROADMAP, mcp.
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
Add figcraft_pathops::round_corners(path, radius, smoothing): every vertex of a closed subpath that is still sharp rounds with a circular arc, convex and concave alike. The sweep's noise is cleaned first (short pieces welded, straight runs merged), smooth joins are not corners, and a corner shrinks to what the edges beside it can hold (half an edge when both ends round, the whole piece beside a smooth join). Corners between straight edges use smoothed_corner; corners beside a curve cut both sides and join them with one cubic. A subpath with nothing to round comes back unchanged.
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
A style's fontStyle binding now goes through set_font_style, so it drops the stale wght axis a weight binding left behind. Also: a test and doc line for the token_roles weight role, the apply-order comment, the weight chip opening the weight picker, junk weight values pinned, fixture assertions fixed, and the ROADMAP and parity status lines.
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
A text style takes a Number variable on `fontWeight` (`style.bind`): the style gets the face nearest the number and sets the `wght` axis, so static and variable fonts both follow. A bound weight and a bound face name replace each other, and the weight wins in a file carrying both. The style editor's weight menu offers number variables beside string ones, and a variable bound to a weight is Unitless in the CSS export. A number bound to a line height stays pixels, as the reference tool applies it; a test pins that.
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
* 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>
The reporter's case: the pane sits in a frame over another layer. Exported on its own it keeps
the blurred layer beneath it and the frame's fill behind it, opaque, as on the canvas.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
An export of a layer draws only that layer, so a background blur or glass in it read an empty
backdrop. The export now renders the page's layers below the first exported layer, with the margin
the effects reach, as the backdrop (render_area_over), clipped to the export area; only the exported
layers are drawn on top. Layers without those effects, and every other export path, are 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
From the pre-ready review: a sticky or shape item on a non-frame layer is ignored with the warning; the author drops control characters as board.sticky does; dump_samples writes a whiteboard's board items; a doc link made plain text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote; Fable 5.1 reviewed
On a FigJam whiteboard the mapper builds a sticky with complete generated records as a native board sticky: the saved colour, size, place, opacity and shadows, the note and the author line as text layers, no background vector. It writes a board field on the REST JSON, which the REST importer reads (junk values are ignored with a warning). Damaged stickies and stickies in a .fig keep the generated layers.
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
Cover inherited Boolean bindings and independent same-component slot content. Strengthen text, HTML and stored-property assertions, split default and own-content cases, and document ownership-based binding resolution.
Cursor::Crosshair was the system's crosshair, whose colour follows the pointer style and
was white on Windows, lost on the default white frame. It is now Glyph::Crosshair, painted
from lines like the pen: a plus with 2 px open at the centre, a 3 px white edge under 1 px
dark lines. Prototype connections still set the system crosshair directly.
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 canvas drew an image shown smaller than its pixels from a 4096 or 8192 px
copy, and an image of 4096 px or less from itself, whatever its size on screen.
A page of a few hundred photos zoomed out then asks the decoded-image cache
(512 MB) for several GB: every render decoded them again, and a zoom out took
seconds with the canvas empty around the old frame.
The copies now also come at 256, 512, 1024 and 2048 px, the smallest that holds
the size shown. Measured on a file with 237 images (median 5.8 MP), canvas
options, 2352 x 2090 view, M4 Max:
| zoom | before | after, first | after, again |
|---|---|---|---|
| 10% | 0.25 s | 0.12 s | 6 ms |
| 4% | 0.88 s | 0.46 s | 7 ms |
| 1% | 3.4 s | 0.9 s | 4 ms |
Peak memory over the run went from 3.9 GB to 2.3 GB. Exports and views at
100% and closer still draw from the image itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured in Figma: a layer's rotation field shows its rotation on the page, also inside a rotated frame or group, and Shift snaps that value. A test pins it; the doc comment on shared_rotation_degrees says so. Review item on #2096.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote; Sonnet 5.5 measured in Figma
The eyedropper rendered its 1x1 sample with default render options, without the canvas's
magnified-image pixels, so on an image shown past 2x it took a bicubic blend of a 1 px line and
its neighbours: the reporter's #3D444D border beside a darker background came out darker. The
sample also began at the pointer instead of being centred on it, half a screen pixel off. It now
renders as the canvas does (magnified_image_pixels) and centres the pixel on the pointer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 found the cause from the reporter's values and wrote; Sonnet 5.5 investigated the flat cases at medium
Measured in Figma web app 2026-11-09: a selection holding a layer with children (frame, group, component, instance) shows Selection colors from one distinct colour; layers without children need two. The section now shows when there are two colours, or one colour and a selected layer with children. Tests in selection_colors_tests; ui-parity updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and briefed; Sonnet 5.5 measured in Figma and wrote at medium
Review follow-ups, tests only.
- The Pencil test and the `vector.simplify` test measure how far the result
strays from the samples (at most 1.5), not only its box.
- The Pencil test also draws with the Marker and the Highlighter, each stroke
in a fresh session, and takes the layer that stroke made.
- The `vector.simplify` test runs on an open stroke, a filled shape, a region
with its own fill, and a stroke beside a filled shape.
- The geom tests run at accuracies 0.1, 0.75 and 8.0, on quadratic and cubic
inputs as well as lines, and check both ways round how far the result and
the input are apart.
- A path that turns straight back is checked to be kurbo's fit of each side on
its own, with a point at the turn.
- Empty paths, lone moves and segments after a close are covered.
With a fill, stroke or effect row highlighted, Cmd/Ctrl+C (or Edit ▸
Copy) copies that paint or effect instead of the layers, through the
same `fill.copy`, `stroke.copy` and `effect.copy` the row's menu runs.
Cmd/Ctrl+V then pastes it onto every selected layer with `fill.paste`,
`stroke.paste` or `effect.paste`, one undo step: on top of each layer's
list, or in place of the highlighted row when it is of the same kind.
- A copy keeps its kind. A stroke goes to the strokes and an effect to
the effects whatever row is highlighted, and the last row copied is
the one pasted.
- A solid colour goes to the system clipboard as its hex code, anything
else as a marker. A paste takes the property only while the system
clipboard still holds that copy and no layers were copied since, so
text or an image from another app, or a later layer copy, pastes as
before. The row menus' Copy writes the same, so Cmd/Ctrl+V pastes it.
- A programmatic copy never writes the system clipboard, and forgets a
property mirror it replaced. A mirror whose engine copy is gone pastes
nothing rather than the layers copied before it.
- The copy is shared by every window, as the layer copy is.
Tests: property_rows_tests (15 more, 22 in all); the replace index and
the Escape key were each removed once to check the tests fail.