A click on Save takes the keyboard from a focused panel field, which commits next frame: the files
save then, as the field-save shortcut does. OsStr::to_str as a method reference (SonarCloud).
* chore: resolve SonarCloud maintainability issues
- Define SHA256_FMT constant in packaging script to remove duplicated string literal (shelldre:S1192)
- Remove redundant 'static lifetime annotations from const items (rust:S8863)
- Replace closures with method and function pointers (rust:S1612)
- Extract helper functions in synthetic document generation, bezier polyline flattening, and component restoration to reduce cognitive complexity (rust:S3776)
* fix(packaging): use verify_sha256 helper function to avoid shellcheck SC2059
* fix(engine): remove duplicate closing brace in large_document
* fix(engine): take &mut Document in restore_id_map; keep 'static on associated consts
restore_id_map allocates ids, so it needs a mutable document. Elided
lifetimes in associated consts trip elided_lifetimes_in_associated_constant,
so those annotations are restored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Assign positional parameters to locals in sha256 verification
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
On a device, a presented frame bigger than the screen now scrolls through
its whole size whatever its overflow, as Figma documents for top-level
frames. Before, "No scrolling" frames only clipped, so the landing page
template's long Phone frame couldn't be scrolled in Present. Content past
a frame's own edges still scrolls only on the axes its overflow allows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Board files (File > New board) with sticky notes, shapes with text, connectors that follow what they join (straight or elbow, arrowheads, labels), marker, highlighter and eraser, and stamps; a board toolbar and a contextual bar; the Design panel hidden on boards, Prototype and Dev Mode refused there. Boards are ordinary FigCraft files built from ordinary layers, so saving, undo, export, MCP and live sessions work unchanged. File tabs, recent files and Open files show a pen nib for design files and a note for boards (original icons drawn in code).
Import lines for connected components inside generated layer trees; Compare changes against any saved version; a Dev Mode behaviour suite with an inspect-only canvas (menus, shortcuts, paste and Revert to saved refuse design edits; a drag under way is dropped on entering), Changed in the ready list, statuses only on top-level frames and sections; read-only plugins' panels from the Inspect tab.
* Dev profile: line tables for workspace crates, no debug info for dependencies
* Tests: one integration-test binary per crate with several test files
* xtask ci: nextest, doctests, tests without examples, --keep-going; add xtask test
The "On this page" swatches were indented by a single add_space, so once they wrapped the later rows started at the picker's left edge and ran past its right edge. The test fills a page with 40 distinct colours, opens the picker and checks every painted swatch lies at least 16 pt inside both sides of the picker window. Without the fix the rightmost swatch ends 3 pt inside the edge instead of 16.
Review: after a Save as, closing called request_close, which asked the
question again while another file still had changes. It now carries on
with save_then_close. Save is the default answer: Enter picks it when no
button has the keyboard.
Closing a window with unsaved changes asked "Close the window and lose
them?" with only Close without saving and Cancel. It now asks
Don't Save / Cancel / Save, as PhotoCraft and VectorCraft do. Save saves
each file of the window with unsaved changes and closes; a file without
a path (or saved as .fig) gets Save as first, and closing goes on once
that saved it. A failed save or a cancelled Save as keeps the window
open with its changes. The two changed strings are updated in every
catalog.
With the keyboard in a Design panel field (X, W, a colour…), the
shortcut handler returned before looking at any key, so Cmd/Ctrl+S and
Shift+Cmd/Ctrl+S did nothing: no save, no message, the tab kept its
unsaved dot. Those two now work from a field: the field lets go of the
keyboard, which commits what was typed as clicking away does, and the
save runs the next frame.
Change to rebuilt the instance from the new variant, and set variable
and set variable mode re-applied bindings, but the player never laid the
result out: no text sizing, no auto layout. A hugging button with a
longer label than its main showed the other variant at the main's width,
its label clipped, and a text bound to a variable kept its old size.
These actions now run the engine's normalize on the player's document
afterwards, the pass that follows every edit on the canvas: text resizes
to what it shows, instances follow and auto layout reflows.
A variable mode chosen on a layer inside an instance (the icon inside a
Close button, recoloured through an icon colour collection) wasn't an
override group. A full rebuild of the instance starts each layer over
from its main and carries only override groups, so the mode fell back
to the main's. Library import always rebuilds, as do main edits such as
a new auto layout direction, so a modal's recoloured close icon came
into other files in its default colour.
"modes" is now an override group: setting a mode inside an instance
records it, rebuilds carry it, rebasing a nested instance finds it, the
component panel lists it, and resetting it brings back the main's mode.
A top-level instance's own modes stay its own and record no override,
like its resizing.
`utc_date` (collab) and `date` (ui-egui) took `u64` seconds, which the old
hand-rolled civil-from-days code converted for the whole `u64` range. The
chrono rewrite used `from_timestamp(..).unwrap_or_default()`, so any instant
past chrono's own year limit (262143) silently became the epoch — changing
existing output. `u64::MAX` seconds, for example, used to read
`584554051223-11-09 07:00 UTC` / `Nov 9, 584554051223`, not 1970.
Fix: add a `utc_parts(days, sod, nanos)` helper per crate that moves the
instant into chrono's range by whole 400-year Gregorian cycles (146_097 days),
lets chrono do the month/day/time, and restores the real year afterwards.
chrono stays the only calendar logic; the cycle count just shifts the year, and
every `u64` day count fits in `i64` so the conversion is always exact (no epoch
fallback). Split seconds with `u64` arithmetic first to avoid the `i64` seconds
overflow near `u64::MAX`, matching the old code.
Tests now pin the old output instead of the epoch:
- collab utc_date(u64::MAX) == "584554051223-11-09 07:00 UTC"
(and utc_date(253_402_300_800) == "10000-01-01 00:00 UTC", already pinned)
- ui-egui date(253_402_300_800) == "Jan 1, 10000"
date(u64::MAX) == "Nov 9, 584554051223"
Replaces the two hand-rolled Howard Hinnant days-from-civil copies with chrono
as a workspace dependency:
- crates/collab/src/message.rs: utc_date (YYYY-MM-DD HH:MM UTC)
- crates/ui-egui/src/home_recents.rs: date (Mon D, YYYY, UTC; keeps the
translated month names and trf formatting)
Output is unchanged. Neither function had a test, so each gains one pinning the
exact strings the old code produced (leap days included). chrono uses only std,
so wasm stays safe. AGENTS.md/CLAUDE.md now say time goes through chrono.
Follow-up to storytold/cadcraft#1785
Panel dropdowns (prototype device, Preferences, blend modes, versions…)
draw their rows with `right::menu_row` on the theme's popup fill, which is
white in the light theme. Since 5a4b7c27 those rows painted their labels in
`menu_text`, white in both themes because it belongs to the dark menu bar,
so the options were invisible until hovered.
Rows now take the theme's text (`t.text`, shortcuts `t.text_secondary`) and
white only on the green hover highlight, as before that commit. The menu
bar, tool flyout and style/variable pickers paint their own rows on
`t.menu` and are unchanged.
Regression test: a dropdown opened headless in both themes keeps its labels
at 4.5:1 and shortcuts at 3:1 against the popup fill.
The colour picker's Window area is keyed by egui as Id::new(Some(title)), but
the style picker looked it up as Id::new("color_picker"), so the lookup always
missed and any click in the picker counted as an outside click that closed the
editor (and the picker with it). Give the window a fixed id and query it through
colorpicker::window_id(), with a regression test.