The picker's model menu offers LCH and OKLCH beside Hex, RGB, HSL, HSB
and CSS, on desktop and web. Lightness, chroma and hue fields; an edit
outside sRGB lowers chroma and keeps lightness and hue (CSS gamut
mapping). The CSS field and Color::parse_css read lch() and oklch().
edit.copy and edit.cut mirror a selection made only of text layers to the system clipboard as plain text, one layer per line, instead of SVG. Other selections stay SVG; pasting back into FigCraft still pastes the copied layers.
Closes#808
Co-authored-by: Cursor <cursoragent@cursor.com>
layout.remove and node.set {layout: null|"none"} now drop the bindings of auto layout fields (gap, crossGap, padding...), as frame-to-section already did for some of them (it now drops all of them). Re-applying a binding no longer creates an auto layout on a frame without one: the binding waits for the next auto layout.
Closes#810
Co-authored-by: Cursor <cursoragent@cursor.com>
Polish integer plural rules use Plural::One strictly for n == 1, while
higher counts ending in 1 (e.g. 21, 101) use Plural::Many.
Also adds unit tests for Polish integer plural coverage.
Co-Authored-By: Antigravity <noreply@google.com>
Escape closed the whole Preferences dialog even while a menu was open over
it. That held while Preferences had no menu of its own; the Interface size
menu is the first, so one Escape threw away the dialog with the menu.
Dialog keys now leave Escape to an open menu, and the next Escape closes the
dialog as before.
Adds a keyboard test of the Interface size menu: Tab reaches it, Enter opens
it, the arrows move through the sizes, Enter picks one and closes the menu,
and Escape closes only the menu.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Draws the interface the way a real window does (a fixed number of physical
pixels, its size in points being that divided by the scale drawn now) over
10 window sizes, 6 system scales and every interface size. Guards the rule a
fixed size promises: above 100 % the window still holds the smallest layout,
the panels leave room for a canvas, and the size settles without flicker.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Preferences > Interface size and the `pref.uiScale` command make the whole
interface larger or smaller. Auto keeps the system's display scale; a fixed
size from 75 % to 300 % is absolute (100 % is one screen pixel per point on
every system). The size is held back while the window is too small for it.
Strings are added in en, es and fr; the control protocol, MCP, UI parity and
roadmap docs are updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#783 completed the French catalog to every current string, superseding
the translations added in #681. This keeps only the ~50 UI strings and
8 command labels whose wording a native French speaker corrected during
that review (keyboard focus, "Off" as "Désactivé", blend modes, stroke
join, spotlight, the no-break space before : ; ? !…), applied on top of
#783. The draft status is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: drop references to competing commercial software
* docs: clarify parity checklist coverage in status callout
* #786: say interface design once in the tagline
---------
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Add full Polish localization for FigCraft UI, commands, and menus.
Includes Polish CLDR integer plural rules (one, few, many, other)
and eliminates all untranslated strings and borrowings.
Co-Authored-By: Antigravity <noreply@google.com>
Both reach wrap() (a page in a mask, a section outlined into a group)
and named `object.group` before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fill.set, stroke.set and node.set fill/fills/stroke/strokes reported ok
while normalize put the bound colour straight back, and node.set
{fills: []} brought the bound paint back. They now drop a paint's
colour, opacity or stop binding where the edit changed that value, as
selection.setPaint already did (its comparison moves to
lists::rebind_edited_paint, which also lets stop bindings follow a
removed stop). boundVariables applies last in props::apply, so a
binding given with the paints in one call stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Grouping, framing, auto layout, masks, outline stroke and the boolean
commands share wrap(), which named `object.group` in every refusal
(`layout.add` on a page: "invalid parameters for `object.group`").
wrap() now takes the calling command's id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A negative width or height was clamped to zero by the scale helper, producing a layer smaller than asked for; vector.create now says so. Regression test covers width -10 with height 8.
Review follow-up. A file written by this version carries editorOutlines, so a stroke someone chose is kept even when it matches the old decoration; only files without the marker get the one-time legacy cleanup. vector.create now errors when a requested side cannot scale the geometry or when both axes would collapse, instead of creating an invisible layer.
A path's intrinsic box replaced the width and height asked for, so an icon imported at 24 stayed on its source grid. Scale the geometry to the requested size; with one side the other keeps its proportion. A path or point set that draws nothing is an error now, not an invisible 0x0 layer.
Node::new gave every component set a purple dashed stroke, so export and screenshot drew the editor frame into the image. Sets now start with no paint; the canvas draws the dashed frame itself, and files written by earlier versions lose exactly that stored decoration when opened. A stroke someone chose is kept.
Collect the folders whose lock is held, wait 100 ms once and retry only those,
instead of up to 100 ms per held folder. Recovery::start takes its folder without
waiting again (a held name already falls through to another). Windows doesn't
pass the lock to child processes, so the wait and its test are Unix-only; the
test holds the lock 10 ms against the 100 ms wait.
Fills, strokes and effects already merged item by item, so both sides
changing different items of one of those lists was no conflict. Every other
list conflicted as a whole: both sides editing different export settings, or
different guides, or different prototype interactions, or different
accessibility annotations or Dev Mode links, each had to be resolved by hand
for the whole list, though the items stand on their own.
Export settings, guides, prototype interactions, accessibility annotations and
the Dev Mode code links and resources now merge item by item, with the same
rule: the same item changed both ways, an item removed on one side, or
different items added on both sides still conflict.
Properties that are keyed maps rather than item lists (bound, varModes,
overrides, propRefs, pluginData) are deliberately left out: their items line up
by key, not by position. Text runs are left whole too: they live in the node's
text data, so merging them item by item means splitting a property the change
format keeps whole.
Replaces the test that documented the gap (other_lists_still_conflict_as_a_whole)
with one that checks each new list merges, and adds one for the same item
changed both ways still conflicting.
A child process forked before its exec inherits the run's open descriptors,
and a flock lasts while any descriptor for the file is open. A crashed run
whose folder lock was still held by such a child read as a run that was still
going, so recover_all skipped it and its unsaved documents were not opened at
the next start. The desktop app's tests failed intermittently for the same
reason whenever another test spawned a process in parallel (the reduced-motion
read spawns gsettings on Linux and the BSDs): recovery_tests failed in 23 of
60 runs of the whole binary and never on its own.
Taking a folder's lock now retries try_lock briefly, so a lock that frees
itself is no longer read as a live run while a run that really is running
still keeps its folder.
Regression test: a_lock_freed_a_moment_later_is_not_a_run_that_is_still_going
holds the crashed run's lock on a second descriptor, the way a forked child
does, and frees it shortly after; the copies come back. The recovery tests
that collide with a spawning test went from 8 failures in 10 runs to 0 in 10.