A bare pipe inside the code span `WS_CAPTION | WS_BORDER` split the table row; the two style
names are separate code spans 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
The canvas takes the smallest copy that holds the size an image shows in
device pixels, whatever the canvas zoom, as #1483 did for images over
4096 px. The canvas row said the copy was no larger than the image shows,
and both changelog rows said views at 100% and closer draw from the image
itself; a 3000 px image fitted into a 600 px layer draws from a 1024 px
copy at 100%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FigCraft draws its own title bar on Windows and opens undecorated windows, but winit 0.30 still gives them WS_CAPTION | WS_BORDER. On some systems the desktop compositor reserves that caption, so the content is drawn about 30 px low (a black band above it) while clicks keep the client coordinates and land above what is drawn. Fixes#1343; the same report as #1422 and #2233.
A per-frame step (apps/figcraft/src/window_frame.rs, run from raw_input_hook) clears WS_CAPTION and WS_BORDER from every winit window of the process (the main window and the other windows alike), keeps WS_THICKFRAME and the window buttons so edge resizing and snap work, and calls SetWindowPos(SWP_FRAMECHANGED) only when a style changed. It uses safe winsafe calls (no unsafe), Windows only; other platforms and the browser are unchanged. The maximize-to-work-area trim of DesignCraft's fix is left for a later change.
Ports the fix of DesignCraft #2139 and PhotoCraft #2401. Tests: the style arithmetic (caption and border cleared, thick frame and buttons kept, a clean style unchanged) and the per-frame hook (runs every frame, a no-op when unset).
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 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.
Fill, stroke and effect rows in the Design panel couldn't be selected.
A click on a row's reorder grip (the strip left of its fields) or on its
background between its controls now highlights it in the selected-row
colour, one row at a time, as the help center's "Copy and paste
properties between layers" describes ("Click to highlight the property
on the right sidebar").
- The row is a click target under its controls (`row_target`), and the
grip of these lists also takes a click (`ListDrag::row_clicked`).
Neither is focusable, so Tab still goes from control to control.
- The highlight belongs to the selection, file and Design tab it was made
on. Escape lets it go before it deselects; so does a press on the
canvas or elsewhere in the Design panel, or a frame its row isn't
drawn in (an undo took its paint away). It follows its paint or effect
when rows are reordered or removed.
Tests: property_rows_tests (7).
A junction, which any account can make, leads out of the root; the path
through it and back with `..` is the file in the root, to the check and to
Windows when it opens it.
Review of the fix for #1094:
- A path that can't be made absolute is refused instead of going on to be
resolved.
- A name on the way that can't be looked at for a reason other than nothing
being there (no permission, for one) is refused: it may be a link that
resolving would follow. Only "not found", "not a directory" and "invalid
filename" count as nothing there.
- Tests: the link part of the Windows test is its own test, so the spelling
and device checks no longer share a test with a part that is skipped where
links can't be made; the timing assertion is gone (the messages tell the
cases apart); link chains of 39, 40 and 41 links; a root on a share against
every network spelling, in both of the root's spellings; devices for read
and write; `..` after a link; paths that aren't UTF-8.
- Docs brought in line with the code.
Tests for the -129 leftward boundary, the vertical boundaries both ways, a drag with no move along the flow, and several moving layers using their combined box. Doc comments say whose edge is "leading" and what the caller passes; the parity row names the unchanged cases and the web-app measurement source.
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
Tests only. Ctrl+wheel joins Cmd+wheel; panning (plain wheel, Cmd/Ctrl+wheel and Shift+wheel with Scroll wheel zoom on) is compared between invert on and off; the pinch runs with Scroll wheel zoom on and off and with factors above and below 1; the pref's off value is saved and reloaded.
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
tool.annotation validates a named id or ids before changing the tool: a page or unknown layer is an error, and a named layer is always selected. Choosing the tool with tool.set (the toolbar) asks for the selected layer's note like Shift+T. annotation.new only sets the focus request for a selected, non-page layer, so it cannot wait and take the keyboard later. ToolId::works_in_dev_mode is the single list behind the pointer routing, the command allowlist and the toolbar. The press never picks the page. Tests for the invalid targets, the toolbar path, a drag, the re-focus, and a request for an unselected layer.
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
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
Choosing Crop in the colour picker starts crop mode only for one layer's
own image fill. The new test chooses Crop for a stroke, for two layers'
fills at once, and for a region's fill in vector edit mode (the layer's
own fill an image too). In each case it checks that the paint takes the
Crop mode, crop mode does not start, and the picker stays open. It also
checks that crop_on_canvas gives nothing for a solid fill, an index with
no fill, the page background and a style.
Resizing a board table by its bottom edge, a corner or a height command left it at a fixed height, so its rows stayed at 40 with blank space below them (or were clipped when shrunk) and typing in a cell no longer grew the table. Measured in FigJam's web app (2026-11-09): a table has no lock-proportions control and never scales proportionally; a bottom edge grows only the last row, a corner drag shares the change equally over every row and column, text keeps its size, and a cell has a minimum size.
board_table::settle_resize runs first in the normalize pass (so a drag, node.set, transform.resize, MCP and live-session peers all agree): a table left at a fixed height gets the change in its rows' minimum height (the last row, or every row when both axes changed), is kept at least columns x 40 wide, and hugs its rows again. New cells get a minimum width of 40. FigJam's floor is about 52 and FigCraft's is 40; there are no per-column widths yet.
Docs: boards-parity Tables row, revision history and status line; the board_table module doc.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and briefed; Fable 5.1 (work seat) scoped; Sonnet 5.5 measured in FigJam and wrote at medium
* 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.
The growth test scans the system fonts, as the other text-measuring engine tests do (#333), and
asserts the note grows taller unconditionally instead of only when text happened to measure.
docs/boards-parity.md no longer lists a sticky's text size among what the normalize pass derives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 checked the findings and wrote
A set in a group has no top-level frame, so a drop on its background fell through to frame_at
and made a Navigate to the section around it. target_at now returns nothing for any point
inside the source's set that misses a sibling variant, as Figma does; red-first UI test with the
set in a group in a section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 checked the finding and wrote
FigJam's sticky text is 18 (Small) and never changes as the note fills; the note keeps its width and grows taller (measured in the web app, 2026-11-09). A new sticky's text is STICKY_TEXT_SIZE (18) and the normalize pass no longer resets a sticky's text size by its length, so saved stickies keep the size they have. No size menu yet.
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
A connection dragged from a variant (or a layer in it) onto a sibling variant of its set now creates Change to that variant (On click, Instant) and outlines the sibling while dragging, as measured in Figma. The set background and the variant itself add nothing; a frame outside the set still gets Navigate to.
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
Adds ta.toml with every UI string, command label and menu wording, marked draft.
Desktop draws Tamil with an installed face (#2018); the web picker leaves Tamil out
until craft-fonts has Noto Sans Tamil, like Georgian and Arabic, with a test.
docs/translating.md gains Tamil's entry, typography notes and a Tamil draft section.