Commit Graph
1248 Commits
Author SHA1 Message Date
NetterSilverandClaude Haiku 5.5 ccddaa74cd Clamp overflowing colour channels to 1 instead of 0
Found while testing crates/color/src/lib.rs:63 (clamp01, used by Color::new via parse_css at :155-166)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 12:27:23 +00:00
justinjohn0306andClaude Opus 5.5 6a163721dc M3: explicit edits of bound scalar fields detach their variable (#809)
An explicit edit of a bound width, height, opacity, gap, padding, corner
radius, stroke weight, arc or text field reported ok, but normalize wrote
the variable's value straight back. Now the edit sticks and that field's
binding is dropped, as paint edits already do.

- `styles::bound_before` / `detach_edited`: compare each layer-level
  bound field's stored value before and after the edit (`bound_field`),
  so keys written unchanged keep their bindings and fields moved along
  with another (proportions, min/max) lose theirs. A binding the edit
  itself made (`boundVariables`) stays. List and component property
  bindings are left to their own rules.
- Hooked into node.set, transform.resize, object.opacityKeys, text.style
  and `Session::preview` (canvas drags of the selected layers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:33:29 +05:30
NetterSilverandClaude Haiku 5.5 b7c9c712b1 Reject server addresses whose port is not all ASCII digits
Found while testing crates/collab-net/src/invite.rs:valid_host (around line 205)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 11:21:34 +00:00
AganzeFelicite 6e7b63bc16 fix(pages): Delete that removes an interaction ends the Pages list focus 2026-10-19 12:50:43 +02:00
AganzeFelicite 3e83ae44b7 fix(pages): a prototype gesture on the canvas ends the Pages list focus 2026-10-19 12:50:43 +02:00
dependabot[bot] 40d7ce14ce ci(deps): bump actions/checkout
Bumps the actions group with 1 update in the / directory: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 10:18:38 +00:00
AganzeFelicite e17fccd7ab fix(pages): a second Delete after deleting layers keeps the page
After a click in the Pages list, layers selected since (Select all, say)
took the first Delete, which left nothing selected, so a second Delete
deleted the page. Delete that doesn't take the page now ends the Pages
list's hold on the keyboard.
2026-10-19 11:50:07 +02:00
AganzeFelicite 9f895a37d5 fix(pages): Delete deletes no page once the Pages list is off screen
The Pages list kept the keyboard after a switch to the Assets tab or to
another open file, so Delete with nothing selected deleted a page there.
It now deletes a page only while the list that was clicked is on screen
(File tab, list expanded, panel and interface shown) and its file is the
one open.
2026-10-19 11:48:35 +02:00
AganzeFelicite 7af44512e7 refactor(pages): remember which file the Pages list focus belongs to 2026-10-19 11:46:47 +02:00
YarilaFox 2af54a26fb M7: keep tab strip usable with many files 2026-10-19 14:23:04 +05:00
YarilaFox d2dfe2ef20 M7: preserve active tab and support tab reordering 2026-10-19 14:21:52 +05:00
AganzeFelicite 139e0ba308 chore(contributors): add AganzeFelicite to people.toml 2026-10-19 10:53:34 +02:00
AganzeFelicite 5d40b3f42f fix(pages): Delete deletes the page picked in the Pages list (#859)
Clicking a page row (or the add-page button) gives the Pages list the
keyboard: Delete or Backspace with no layers selected now deletes the
current page instead of failing with "nothing is selected". No
confirmation; a "Page deleted" toast says so and Undo brings it back.
The context menu's Delete page shows the same toast. Selecting layers,
clicking a layer row or pressing on the canvas gives Delete back to the
layers.
2026-10-19 10:53:34 +02:00
slyimandClaude Opus 5.5 be212f00e6 #826: the stroke-width test covers the Frame and Text marks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:25:34 +03:00
AJ Richardson 3b7b28c03c improve page search functionality to close with Escape key even when unfocused 2026-10-19 16:37:52 +10:00
AJ Richardson caf0f44d90 #855: The Search button for the pages doesn't work as expected 2026-10-19 16:08:26 +10:00
MortenHustedandClaude Opus 5.5 6f90717e71 M3: effects refuse keys they don't read instead of dropping them
node.set refuses unknown properties, but the effects inside it were read
by serde, which drops keys it doesn't know. A drop shadow given
offsetX/offsetY (the names variable bindings use for its offset) was
stored with the default offset [0, 4] and the call said ok. effect.add
and effect.set dropped them the same way.

The three now refuse what the effect doesn't read and say what it
takes: "effects[0]: unknown properties: offsetX, offsetY (a dropShadow
takes blend, color, offset, radius, showBehind, spread, type, visible)".
A key counts as read when setting it to null makes the effect fail to
parse or come out different, so serde's aliases count as read without a
list of them here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:53:05 +02:00
MortenHustedandClaude Opus 5.5 147a374350 M3: a refused slot names component.makeSlot
component.makeSlot adds its slot property through add_prop, which named
itself in every error: making a second slot called "Content" said
"invalid parameters for `component.addProperty`: a property named
`Content` already exists", a command the caller never ran. add_prop now
takes the command errors name, the fix #807 makes for wrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:53:04 +02:00
MortenHustedandClaude Opus 5.5 f97789ade1 M7: CSS borders follow individual stroke sides
A layer with individual stroke sides (a header with only a bottom divider,
strokeSides [0, 0, 1, 0]) came out of the CSS, HTML, React and Leptos
code as `border: 1px solid ...` on all four sides, because the border
was written from the stroke weight alone. With individual sides it is now
a border-top/right/bottom/left for each side that has a weight, and a
side bound to a variable names it (border-top: var(--border-top, 3px)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:53:04 +02:00
MortenHustedandClaude Opus 5.5 544b2455c9 M8: a path in a root reached through a link is told why it is refused
When a path can't be resolved (a link that leads nowhere, `..` below a
missing folder), the reason is given only for paths inside a root.
That check compared the path as typed with the roots resolved, so a
root reached through a link (`/tmp/work` on macOS is
`/private/tmp/work`) called its own dangling links "outside the folders
that may be written: /private/tmp/work", and a link out of a root still
had its reason told. The existing part of the path is now resolved
before the comparison, as the roots were.

This is also why `links_that_lead_out_of_a_root_are_outside` and
`outside_paths_are_refused_alike_whatever_exists_there` failed on
macOS, where the temp folder is under `/var` -> `/private/var`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:53:03 +02:00
Muamer MustaficandCursor 63dc418cc2 M7: a web file picker closed without a cancel event settles on refocus (#759)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 23:50:27 +02:00
Muamer MustaficandCursor 79ac16a256 M3: doc comments for the structure commands and binding re-apply (#810)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 23:40:35 +02:00
Muamer MustaficandCursor 85c5286d39 M1: doc comments for the menu invoke and SVG clipboard export (#808)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 23:40:33 +02:00
skylar taylor-barrick a0d1162976 Desktop: refuse unknown command-line options instead of ignoring them (#836) (#844)
A mistyped or unsupported flag (e.g. --automation-write-roots) used to be
dropped silently, so the app started unconfined. It now exits 2 with
'unknown option', like a bad automation root. A bare -- ends the options.

Adds a desktop control-channel test that file.save / file.saveAs outside the
write root are refused and create nothing, while saves inside succeed, and
documents that the root flags arrived in 0.7.0 (0.6.0 ignores them).
2026-10-18 15:20:15 -06:00
NetterSilverandClaude Haiku 5.5 23d0d9e49e Accept --control=PORT like --control PORT
Fixes #837

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 21:14:08 +00:00
Hamza 35cc685e90 cli: an unconfined write still follows a symbolic link
Routing the CLI's writes through `file_access` changed what happens when the
output path is a symbolic link. `file_access::write` stages the bytes and
renames them into place, and a rename replaces the link with a regular file,
where `std::fs::write` followed it and wrote to its target. A render to a link
reported success while the target kept its old contents.

Unconfined writes go back to `std::fs::write`. Confined ones keep the staged
write, which is right there: `Roots::check` resolves the path first and has
already refused a link leading out of the root, so the write lands on the
resolved target inside it.
2026-10-19 00:52:40 +05:00
NetterSilverandClaude Haiku 5.5 39f30d63c8 Refuse empty mode lists and blank names in variable.createCollection
Found while testing crates/engine/src/cmd/styles.rs:119-124

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 21:27:31 +02:00
Dylan M. Taylor 7fcb193f2a ci: configure Dependabot for Cargo and GitHub Actions (#825)
* ci: configure Dependabot for Cargo and GitHub Actions

* ci: add Cargo update entry for /plugins workspace in Dependabot

* ci: consolidate Cargo Dependabot directories in figcraft
2026-10-18 15:10:42 -04:00
Hamza c3cb8c3e0e cli: confine info, export, convert, render and run to automation roots
`figcraft-cli mcp` takes `--automation-read-root` / `--automation-write-root`
to confine the files an agent's commands touch, but the file subcommands took
neither: `figcraft-cli run script.json --out /anywhere/x.figcraft` read and
wrote as the user who started it. The README invites agent use, and a script
or agent driving `run`, `export` or `convert` had no way to say which folders
it may reach (#833).

`info`, `export`, `convert`, `render` and `run` take the same two flags, with
the same semantics: repeatable, the folders must exist, reads inside a read or
write root, writes only inside a write root, everything else refused -- so
giving no write root denies writing altogether. Without the flags they behave
as before.

The flags would have been decorative on their own: `figcraft_engine` enforces
roots through `file_access`, which the engine's own loads already go through,
but the CLI wrote its output with `std::fs` and read its script the same way.
Those three calls now go through `file_access` too, so the confinement reaches
the files the CLI itself touches.
2026-10-18 23:48:54 +05:00
slyimandClaude Opus 5.5 54458cfb20 #826: tests for the inset Frame and Text marks and the connected Slice knife
frame_and_text_marks_are_inset fails if either mark reaches the box edge
again; slice_is_one_connected_knife fails if the handle stops short of the
blade's base or the icon goes back to separate marks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:52:39 +03:00
Muamer MustaficandCursor 309bac6185 M7: the web build opens files: File ▸ Open, Import and dropped files (#759)
File ▸ Open, Import and Place image did nothing in the browser (the pickers
return a path, which a browser never has) and dropped files were ignored
(eframe hands their contents over asynchronously).

- Services::pick_open_contents: a platform can pick a file's name and
  contents; the web build uses an <input type="file">. Open and Import pass
  them to file.open / file.import as data + name.
- A dropped file without a path waits behind the opening card until its
  contents are read (bytes_async on wasm), or says it couldn't be read.
- Canvas drops still import, as on the desktop.

Closes #759.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 19:32:46 +02:00
NetterSilverandClaude Haiku 5.5 72990373c4 Keep multiline layer names inside the SwiftUI/Compose too-deep comment
Found while testing crates/engine/src/codegen.rs:865 (swift_layer) and :1038 (compose_layer)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 19:27:45 +02:00
slyimandClaude Opus 5.5 7a5bd57611 #826: the Slice handle overlaps the blade's base so the knife joins at every size
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:11:38 +03:00
slyimandClaude Opus 5.5 7558ac0444 #826: doc comments for draw and main_toolbar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:06:21 +03:00
slyimandClaude Opus 5.5 f03fc476c8 M4: toolbar marks share one size and stroke weight; a simpler Slice icon (#737, #738, #739)
Every mark in the bottom toolbar is now painted in one 18-point box. The
mode switcher drew its marks in a 14-point box, so Draw, Prototype and
Inspect read smaller than the tools. The flyout carets move from a
10-point box, whose stroke rounded to one physical pixel thinner than the
row's, to a 14-point box that rounds to the same width at 1x, 1.5x and 2x.

Frame and Text are inset by a unit: their long pixel-hinted strokes read
heavier than the anti-aliased outlines beside them. Slice is a knife (a
slim blade and a solid handle), one silhouette instead of a box crossed
by a diagonal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:59:56 +03:00
Muamer MustaficandCursor b778ba17cd M1: copying text layers puts their text on the clipboard (#808)
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>
2026-10-18 16:08:12 +02:00
Muamer MustaficandCursor 4506f49ad4 M3: removing auto layout drops its gap and padding bindings (#810)
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>
2026-10-18 15:21:28 +02:00
Dylan M. Taylorandbflatastic d8ff1441b1 docs: drop references to competing commercial software (#786)
* 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>
2026-10-18 19:34:53 +07:00
MortenHustedandClaude Opus 5.5 d71f24741a #807: test the object.mask and vector.outlineStroke refusals too
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>
2026-10-18 14:18:25 +02:00
MortenHustedandClaude Opus 5.5 c7261cb907 M3: an explicit paint edit detaches the variable bound to what it changed
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>
2026-10-18 14:14:38 +02:00
MortenHusted d3327d79f3 Contributors: add my name for the About window 2026-10-18 13:58:13 +02:00
MortenHustedandClaude Opus 5.5 4707cb1579 M1: wrap errors name the command that was run
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>
2026-10-18 13:58:13 +02:00
BFlat 018b8166b2 M3: a bound colour variable's alpha applies once, not squared (#802) (#804)
Fixes #802.

Fixes #802.
2026-10-18 18:50:19 +07:00
BFlat 86e91d0b71 Merge pull request #801 from phonedu/fix/silent-document-errors
M1: vector.create honours width/height by scaling the path
2026-10-18 18:37:38 +07:00
bflatastic d1dad2e1c0 Merge #801's latest commit (negative sizes: covered by the size check above)
# Conflicts:
#	crates/engine/src/cmd/create.rs
#	crates/engine/src/vector_behaviour_tests.rs
#	docs/mcp.md
2026-10-18 18:18:51 +07:00
phonedu fec902b7d5 M1: refuse negative vector sizes
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.
2026-10-18 19:00:25 +08:00
bflatastic 057bf9a289 #801: create_node and mcp.md say width/height scale a vector's path 2026-10-18 17:54:25 +07:00
bflatastic f1d336249a #801: vector.create accepts 0 on an axis the geometry has none of and refuses a flattening size; points scaling tests 2026-10-18 17:54:24 +07:00
bflatastic 55aa0165fd #801: keep a component set's outline as its real stroke (reverts the export change) 2026-10-18 17:41:11 +07:00
bflatastic 836d6f2fa6 Merge main into #801 2026-10-18 17:40:54 +07:00