Commit Graph
660 Commits
Author SHA1 Message Date
BFlat 60ea32f171 Merge pull request #1465 from phonedu/mcp-agent-feedback
M12: MCP comment feedback and live change events
2026-10-29 10:51:24 +07:00
bflatastic 0cc380c228 Merge main into #1466 2026-10-29 10:08:11 +07:00
bflatastic 6abddb0f68 Merge main into #1465 2026-10-29 09:55:55 +07:00
BFlat 7f646aac24 Merge pull request #1468 from samhyun/perf/web-zoom-settle
M8: the browser refines a zoom only once it has paused
2026-10-29 09:49:26 +07:00
BFlat 4a0bd79797 Merge pull request #1488 from ChristopherLinnett/issue/1086-image-validation
fix(collab): image entries must match their hash and sane bounds (#1086)
2026-10-29 09:49:20 +07:00
BFlat 5804c0e2cc Merge pull request #1489 from ChristopherLinnett/issue/857-confirm-quit
fix(app): ask before ⌘Q quits with unsaved changes (#857)
2026-10-29 09:41:19 +07:00
bflatastic e83f9691b3 Merge main into #1466 2026-10-29 09:32:26 +07:00
bflatastic 170f1fdf32 #1466: gaps and ROADMAP list the Arabic draft as shipped 2026-10-29 09:26:15 +07:00
Christopher Linnett 57cf1bec48 fix(collab): keep in-use images and resync clients after filtered history (#1086)
- An image a layer or style still shows can't be removed: removing it and
  putting other bytes under its key would swap the picture that layer
  shows, which giving the key other bytes directly can't. A batch made by
  the diff stops using an image (node changes, then styles) before it
  removes it; an undo keeps the changes that apply, so its image may stay.
- Restore and replay leave out the image entries history written under
  older rules can't apply, using one method shared with the undo history
  (Replica::apply_kept, extracted from history.rs). It is exact for every
  image rule, including removals in use and replacements, and leaves out
  only image entries: any other change that doesn't apply is still an
  error. The image-only filter it replaces is gone.
- A server whose restore or replay left image entries out, or whose saved
  log puts images, catches every resuming client up with its snapshot,
  including one at the same sequence number.
- Tests: an_image_in_use_cannot_be_removed,
  an_image_no_longer_in_use_can_be_removed, undoing_an_image_layer_still_works,
  a_restart_replays_past_a_logged_removal_of_an_image_in_use,
  a_resume_after_history_was_left_out_catches_up_with_the_snapshot,
  a_resume_after_a_clean_restart_catches_up_with_the_log,
  a_snapshot_with_a_broken_layer_change_still_fails_to_open.
- Docs: collab-protocol.md, collaboration-parity.md.
2026-10-29 10:25:16 +08:00
bflatastic 6c18fe916b Merge main into #1466 2026-10-29 09:23:22 +07:00
bflatastic 9d42833008 Merge main into #1465 2026-10-29 09:18:39 +07:00
Christopher Linnett 7cce115ad6 fix(collab): check old images against the state they apply to (#1086)
- without_refused_images judges each image entry against the document it
  applies to and the batch's earlier entries (image_for, shared with
  apply): a logged put of other bytes under a key the replay state holds
  is left out, so a restart no longer stops there and drops the rest of
  the log.
- Server::restore keeps its saved log only when the log puts no images:
  a resuming client may already hold other bytes under a logged key, which
  the restored state can't check, so clients catch up with the checked
  snapshot instead.
- Tests: a_restart_replays_past_a_logged_image_replacement,
  a_client_resuming_past_a_refused_image_in_the_saved_log_catches_up,
  a_client_resuming_past_a_replaced_image_in_the_saved_log_catches_up.
- Docs: collab-protocol.md, collaboration-parity.md.
2026-10-29 09:37:33 +08:00
Christopher Linnett 949d74f7ca fix(collab): image checks keep documents shareable and history replayable (#1086)
- The entry size cap is figcraft_doc::ImageData::MAX_BYTES (256 MiB), the
  cap the .fig, .sketch and REST importers already read images with (now
  one constant), instead of 32 MiB: an imported image larger than 32 MiB
  made the document unjoinable and its server snapshot unrestorable.
- History written before the checks (a restored or joined snapshot, a
  replayed server log, a file published to a server) applies without the
  image entries they refuse (docprops::without_refused_images), instead of
  the file not opening or a restart dropping every edit after the image.
- A key that already holds an image can't be given other bytes: producers
  key images by their bytes (the REST importer's placeholder keys always
  hold the same picture), and merge treats a key's image as never
  changing.
- REST import: an unresolved imageRef shaped like a content hash shares
  the rest-image-missing placeholder (the placeholder's bytes don't hash to
  it); ImageData::claims_content_hash is shared with collab.
- Tests: collab image_tests (4 new, size test uses a small cap), format
  image_tests (1 new).
- Docs: collab-protocol.md, collaboration-parity.md, file-format-parity.md.
2026-10-29 08:55:03 +08:00
Christopher Linnett 0e508fd316 fix(app): a second quit before the window draws counts once (#857)
The app's logic runs while the main window is minimised, without the
frame that acts on a close. Two quits there queued the main window's close
twice: the first let it take over another window, and the second then
quit the app over that window's unsaved changes, as answered.

- close_current queues a window's close only once.
- Test: quitting_twice_before_the_window_draws_closes_it_once.
- docs/hardware-parity.md revision history.
2026-10-29 08:50:00 +08:00
Christopher Linnett 7660877845 fix(present): nothing opens or releases behind an open link notice (#1096)
- finish(): a viewer-fired link opens directly (notice skipped) only while
  no notice shows; any other link that fires while one shows is dropped.
- finish(): raising a notice ends the press under way, so its release
  doesn't fire the prototype's mouse-up actions (a link, a navigation)
  behind the notice, which takes no input while it shows.
- The mail notice says the link opens the mail app to write a message
  (opening a mailto: link doesn't send anything).
- Tests: nothing_opens_while_a_notice_shows_with_the_notice_skipped,
  a_press_that_raised_a_notice_doesnt_release_behind_it.
- docs/prototyping-parity.md row and revision history.
2026-10-29 08:50:00 +08:00
Christopher Linnett f1c2ee81e4 fix(collab): image entries must match their hash and sane bounds (#1086)
Image entries applied bytes to any key with no checks (audit COL-9): a
collaborator could poison the shared content-addressed image store, claim
absurd dimensions, balloon memory, or name an image `../../x`, which a
saved file then wrote as the archive member `images/../../x`.

- Every key must be 1–128 ASCII letters, digits, `-` or `_`:
  `figcraft_doc::ImageData::is_valid_key`, promoted from the format
  crate's private `is_valid_hash` so the file reader and collab share one
  rule. The reader now also takes `-` and `_`, so the REST importer's
  `rest-image-missing` placeholder survives a save and load.
- The REST importer's side of the same attack: an unresolved `imageRef`
  that can't legitimately be an archive member name (path text, an
  over-long ref) shares the `rest-image-missing` placeholder instead of
  being keyed by its raw name
  (`image_tests::a_path_like_rest_image_ref_never_names_an_archive_member`).
- A key with the shape of a content hash (64 hex chars) must be exactly
  the lowercase sha256 of the bytes, the form `image_hash` writes; an
  uppercase look-alike is refused. Other keys (svg FNV-128, REST
  placeholders) claim no content address and only meet the rules above.
- A side is at most 65 535 px; decoded bytes at most 32 MiB, with the
  base64 text capped before decoding. 0×0 stays valid: the format crate
  stores it for sizes it can't probe.
- Refusals are Error::Invalid and reject their batch, so every replica's
  store stays clean; catch-up snapshots apply through the same check.
- Docs: collab-protocol.md, collaboration-parity.md and
  file-format-parity.md (rows, status lines, histories).
- Tests: collab image_tests (8), format image_tests: round-trip and the
  REST placeholder gate.
2026-10-29 08:11:13 +08:00
Christopher Linnett 73398cedf1 fix(app): ask before ⌘Q quits with unsaved changes (#857)
winit's default macOS app menu ends the process through AppKit's
`terminate:` at once, so ⌘Q and Quit from the menu bar never asked about
unsaved changes (the recovery copies were all that survived).

- Install craft-libs' app menu (craft-menu, same rev as the other
  craft-libs pins; macOS only). Its Quit sends the main window a close
  request, the same path as the window's close button, so
  windows::on_close_request cancels the close and the unsaved-changes
  question asks first. Failure to install keeps winit's menu and logs it.
- windows_tests::a_quit_with_unsaved_changes_waits_for_the_answer pins
  the contract the menu relies on: a close request with unsaved changes
  sends CancelClose and asks; Don't Save then closes.
- Like the close button it closes the main window only: with several
  windows open, each quit closes one (documented).
- docs/hardware-parity.md macOS row, status line and revision history.
- Cargo.lock: craft-menu, craft-quit, craft-keymap, muda and their deps;
  cargo's own minimal resolution also moves a few windows-sys/getrandom
  edges to versions already in the lock.
2026-10-29 08:11:13 +08:00
Christopher Linnett 53f47706ec fix(present): prototype links open only behind the notice or a viewer's click (#1096)
An Open link action carries a URL from the document and fires from whatever
trigger the prototype sets, so an unattended presentation could open web links
and mail messages one after another: a mail link bypassed the leaving-the-
prototype notice entirely, and a skipped notice let any trigger open any
number of links.

- finish() routes every openable URL, mail included, through the notice.
- Skipping the notice ("Don't show this again") opens at once only a link the
  viewer's own click, press, drag or key fired, one per frame. A delay, the
  pointer passing over a layer (enter, leave, hover) and a timed mouse
  trigger that fires after the gesture is over still show the notice, which
  then offers no checkbox: it couldn't skip that notice anyway.
- The player tags each queued link with whether the viewer fired it
  (Presenter::fire_from); an OpenUrl outside an interaction defaults to not.
- The notice covers mail links (new locale string, mail wording).
- Engine prefs/command doc text follows the rule; behavior untouched there.
- present_links_tests: shared setup helpers for the link tests; new
  tests for the skip rule (delay, hover, delayed mouse-up, key, one link per
  click, checkbox only for clicked links); the scheme-filter test now clicks
  with the notice skipped so the filter alone keeps those URLs out.
- docs/prototyping-parity.md row, status line and revision history updated.
2026-10-29 08:11:13 +08:00
Brandon ThomasandClaude Opus 5.5 cb227c9715 Web: raise the wasm size gate to 25 MiB - 512 KiB
The 0.12.0 release's Web job failed at 25,319,327 bytes against the 24 MiB (25,165,824-byte) gate. opt-level z, fat LTO and wasm-opt -Oz are already in place and extra wasm-opt passes save only ~20 KB, so the gate moves to 25,690,112 bytes, still under Cloudflare's 25 MiB per-file cap. The wasm grew ~2 MB in three days (egui 0.37, new features); bringing it back down is follow-up work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:36:52 -04:00
samhyunandClaude Fable 5.1 3773a60801 M8: the browser's wait after a preview never exceeds the render it may save
With a fixed 120 ms wait, a page whose full renders take 15–100 ms was held coarse for 120 ms
after a key zoom or a page switch, where before it was refined in the next frame. The wait is
now the last full render's time when that is shorter than `SETTLE_MS`: a wait never costs more
than the render it may save, and on a heavy page (seconds a render) it is 120 ms as before.

Also from review: the test that the wait counts from the preview no longer times a preview
render (a margin of a few milliseconds under load); a sleep between the request and the poll
stands in for the render, with the whole settle as margin. `Inline::in_flight` says that a
preview's wait counts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-28 22:53:07 +09:00
phonedu 8e6b3bf449 M12: MCP comment feedback and live change events
Comments become the task queue for an attached or headless MCP client: comment.prompt returns a bounded Markdown brief, the Comments panel gains Copy for AI, and the MCP server exposes fix_comments, watch_comments, figcraft://comments and wait_for_changes. Mutating comment.* commands append to a bounded in-memory event log; the control channel's events.poll long-polls it without the UI thread, and a successful batch commits its events only if the whole batch lands. No model, network dependency, asset or file-format change.
2026-10-28 21:42:31 +08:00
samhyunandClaude Fable 5.1 8c951810ab M8: a test for same-zoom requests before a zoom refines; the gaps status line
From the review of #1468: a pan or an edit at the new zoom before its full render has run
renders from scratch too, so it shows a preview and restarts the wait instead of running a
blocking full render between it and the next step; once refined, requests at that zoom build
on the kept image and are refined at once. The new test pins both down. The status line of
`docs/gaps.md` now carries the date of the gap 7 entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-28 22:29:55 +09:00
samhyunandClaude Fable 5.1 f24a8312f7 M8: the browser refines a zoom only once it has paused
The browser renders on the UI thread. A zoom showed its coarse preview and then, in the very
next frame, ran the full render, so every wheel step of a continuous zoom had a blocking full
render between it and the next: on a heavy page each one took seconds, and the zoom stuttered
for as long as it went on.

The browser's worker (`Inline`) now refines a preview only once no newer request has come for
`SETTLE_MS` (120 ms) after the preview was shown: the zoom has paused. Each wheel step still
shows its preview at once, and the worker asks for a repaint when the wait is over. Pans and
edits have no preview and are refined at once, as before. The desktop has a render thread of
its own and is unchanged. A zoom passing through a scale also no longer spends a full render
there.

The wait counts from the preview being handed over, not from the request: rendering the
preview can itself take longer than the wait (the page is blocked meanwhile, so no newer
request could have come), and counted from the request the full render would follow in the
very next frame, as before.

Measured in Chrome on a synthetic page of 20 images, 60 shadowed rectangles and two
background blurs (a full render takes about 3.7 s, a preview about 250 ms), 20 wheel steps in
and 20 out, 50 ms apart, three runs each: the frame interval's p95 went from 3.7 s to 240 ms,
the frames over 100 ms from 79–80 (one full render per step) to 39–41 (one preview per step),
and the zoom took 10–12 s instead of 141 s. The one full render after the pause still takes as
long as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-28 22:10:27 +09:00
hussain-hz 636096fad3 M10: add Arabic draft interface catalog
Translate 2508 interface entries from English, keep count-neutral plurals, and gate the web catalog on Arabic font readiness. Record AI assistance and pending native review.

AI-assisted-by: OpenAI Codex
2026-10-28 15:16:31 +03:00
aiongg 60c671da66 docs: update source paths for the layout moves 2026-10-26 20:36:51 +00:00
bflatastic 5e7cd52f6d Merge main into #827 2026-10-27 01:06:45 +07:00
BFlat cac54fd552 Merge pull request #840 from StrajnarFilip/feat/vector-drawable-export
M6: export as Android Vector Drawable (XML)
2026-10-27 01:06:21 +07:00
BFlat 740cc2a2c6 Merge pull request #726 from ijashuzain/feat/variables-dart-export
M6: export variables as Dart constants for Flutter
2026-10-27 00:53:10 +07:00
BFlat cac991334d Merge pull request #320 from tylerjenningsw/perf/zoom-preview-cold
perf(canvas): preview cold zoom and page transitions once they run long (M8)
2026-10-27 00:50:49 +07:00
BFlat 29c89f7d5b Merge pull request #650 from JimKnoxx/overlay-anchor-trigger
M5: overlay positions relative to the layer that opens them
2026-10-27 00:49:52 +07:00
BFlat 9fdd226509 Merge pull request #664 from phonedu/feat/vue-code-target
M6: a Vue code target (single-file components)
2026-10-27 00:48:53 +07:00
bflatastic c16fd68242 Merge main into #840 2026-10-27 00:38:47 +07:00
BFlat b6fc2fd1ab Merge pull request #1152 from ricardoavilesfoto-collab/pr/fig-export-for-figma
.fig export: files Figma imports, with component set properties
2026-10-27 00:35:31 +07:00
bflatastic 502098e02a #840: the status lines without the merge's conflict markers 2026-10-27 00:32:35 +07:00
bflatastic 2aae395310 Merge main into #840 2026-10-27 00:31:32 +07:00
bflatastic fd0f4d8b60 Merge main into #664
Main's class_name takes the node kind (#1372); Vue's slot names pass slot.
2026-10-27 00:30:42 +07:00
BFlat 83c7940293 Merge pull request #1384 from muamerETS/fix/1370-drag-unfilled-selection
M1: a selected unfilled shape drags over a filled layer stacked below it (#1370)
2026-10-27 00:25:59 +07:00
BFlat 7cb369db8c Merge pull request #1372 from lileeei/1235-transliterate-class-names
Transliterate non-ASCII layer names into code-export class names
2026-10-27 00:23:36 +07:00
bflatastic b3016f7089 Merge main into #1152 2026-10-27 00:14:07 +07:00
BFlat e8656be10e Merge pull request #824 from youngeuibae/page-api
web: window.figcraft, the control channel for the page
2026-10-27 00:13:31 +07:00
BFlat dd5b5c0dc0 Merge pull request #1374 from Ridzzz0Alam/fix/1330-save-disk-changes
fix: Issue #1330 Save no longer overwrites changes another app made to the open file
2026-10-27 00:13:15 +07:00
bflatastic 632f2cef4b Merge main into #840
# Conflicts:
#	docs/roadmap.md
2026-10-27 00:12:27 +07:00
BFlat 4392fe50d3 Merge pull request #1373 from Ridzzz0Alam/fix/1338-instance-swap-scroll
fix: Issue #1338 an instance-swap property's default menu scrolls
2026-10-27 00:11:16 +07:00
bflatastic 15c6834114 Merge main into #1384 2026-10-27 00:10:34 +07:00
bflatastic 4176400e09 Merge main into #664
# Conflicts:
#	crates/engine/src/code_connect.rs
#	crates/engine/src/codegen.rs
#	crates/engine/src/component_codegen.rs
#	crates/engine/src/lib.rs
#	crates/engine/src/playground.rs
#	crates/ui-egui/locales/bn.toml
#	crates/ui-egui/locales/cs.toml
#	crates/ui-egui/locales/de.toml
#	crates/ui-egui/locales/en.toml
#	crates/ui-egui/locales/es.toml
#	crates/ui-egui/locales/fi.toml
#	crates/ui-egui/locales/fr.toml
#	crates/ui-egui/locales/id.toml
#	crates/ui-egui/locales/ja.toml
#	crates/ui-egui/locales/ka.toml
#	crates/ui-egui/locales/ko.toml
#	crates/ui-egui/locales/nl.toml
#	crates/ui-egui/locales/pl.toml
#	crates/ui-egui/locales/pt-BR.toml
#	crates/ui-egui/locales/ru.toml
#	crates/ui-egui/locales/sv.toml
#	crates/ui-egui/locales/tr.toml
#	crates/ui-egui/locales/zh-Hans.toml
#	crates/ui-egui/src/dev_mode.rs
#	docs/dev-mode-parity.md
#	docs/gaps.md
#	docs/mcp.md
#	docs/target-app-parity.md
2026-10-26 23:52:51 +07:00
bflatastic 3beea93715 Merge main into #827
# Conflicts:
#	ROADMAP.md
#	contributors/people.toml
#	crates/ui-egui/src/colorpicker.rs
#	docs/ui-parity.md
2026-10-26 23:38:20 +07:00
bflatastic 6145409859 Merge main into #726
# Conflicts:
#	ROADMAP.md
#	crates/engine/src/cmd/variables_io.rs
#	crates/engine/src/variables_io_tests.rs
#	docs/components-parity.md
#	docs/mcp.md
2026-10-26 23:28:15 +07:00
bflatastic bf9783f30f #650: mark relative overlay positions as an extra beyond the reference tool 2026-10-26 23:24:36 +07:00
bflatastic de826e14cf Merge main into #650
# Conflicts:
#	crates/ui-egui/locales/es.toml
#	crates/ui-egui/locales/pl.toml
#	docs/prototyping-parity.md
2026-10-26 23:24:22 +07:00
bflatastic e469cc8135 Merge main into #840
# Conflicts:
#	apps/figcraft-cli/src/files.rs
#	apps/figcraft-cli/src/main.rs
#	crates/engine/src/cmd/file.rs
#	crates/engine/src/lib.rs
#	crates/ui-egui/locales/en.toml
#	crates/ui-egui/locales/es.toml
#	crates/ui-egui/src/export_panel.rs
#	crates/ui-egui/src/export_preview_tests.rs
#	docs/file-format-parity.md
#	docs/mcp.md
#	docs/roadmap.md
#	docs/ui-parity.md
2026-10-26 23:23:07 +07:00