* feat(app): show atomic document loading progress
- Preserve the existing full-canvas pencil loader while adding phase, detail, accessible status, and honest determinate progress
- Keep one generation-safe load owner across FIG decoding, graph preparation, page population, fonts, fallbacks, layout, viewport fitting, and first-render fade
- Prevent nested page setup and viewport cleanup from revealing partially prepared documents
- Cover obsolete sessions, font-resolution ownership, and staged loader UI
* refactor(app): scope editor preparation per tab
- Replace the shared loading boolean with one reactive preparation snapshot and one imperative controller per editor store
- Keep Core page work progress-only and inject canvas suspension from the app boundary
- Route FIG, storage, recovery, DOM import, and page switching through reusable tab-local preparation handles
- Abort only the closing tab's operation and cover generation safety, multi-tab isolation, progress UI, and disposal
* fix(editor): commit prepared pages atomically
- Prepare population, fonts, fallbacks, and layout without changing the visible page
- Reject cancelled and stale prepared pages before committing viewport, selection, and page events
- Keep the preparation overlay until the committed scene version is presented
- Cover call order, cancellation, stale generations, and presentation acknowledgement
* fix(app): stage imported documents before commit
- Prepare imported graphs in an isolated Core editor before replacing the live document
- Share font loading while keeping live selection, graph, renderers, and history untouched during staging
- Preserve the previous graph when staging is cancelled or fails and remove the duplicate pre-font layout pass
* refactor(app): namespace preparation UI
- Move canvas and tab preparation presentations into focused subfolders with concise component names
- Share progress and phase presentation helpers across preparation surfaces
- Show tab-local preparation status without covering the active canvas for background work
* fix(app): cancel preparation work at source
- Publish typed per-store preparation lifecycle events with explicit completion, cancellation, and failure outcomes
- Propagate tab-local AbortSignals through FIG parsing, population workers, and browser font downloads
- Keep cancellable font requests outside shared in-flight caches while retaining globally completed font registrations
- Stop FIG manifest previews from replacing the live graph before atomic document commit
* fix(app): cancel storage and DOM preparation
- Propagate preparation signals through S3 downloads, byte progress, local-cache boundaries, and DOM/CSS conversion checkpoints
- Reuse merged diagnostics and localized toasts for document, storage, and presentation failures
- Replace manual font concurrency and presentation timers with es-toolkit limitAsync and withTimeout
- Guard stalled first presentation and fix the merged recovery dialog title bindings
* fix(app): stage reload and font retry
- Prepare reload graphs in isolation and preserve the current document on read, decode, font, or layout failure
- Restore page and viewport state only after atomic graph commit with cancellable reload reads
- Run font Retry as a tab-local preparation with cache reset, final layout, picture invalidation, and presentation acknowledgement
- Keep completed document pixels visible while Retry reports activity in the tab
* fix(app): enforce exclusive preparation outcomes
- Complete document, storage, recovery, and DOM preparations only after successful commit
- Keep failed and cancelled handles terminal so lifecycle events cannot report contradictory outcomes
- Preserve external AbortError identity across storage timeouts and cancel streamed readers without returning partial bytes
- Cover credential-free pre-abort, mid-stream cancellation, progress cutoff, and terminal outcome exclusivity
* feat(diagnostics): record preparation outcomes
- Persist completed, cancelled, and failed preparation lifecycles through the validated diagnostics recorder
- Store only operation kind, outcome, cancellation or failure category, terminal phase, and coarse duration bucket
- Exclude document subjects, font families, storage identities, URLs, raw durations, messages, and stack traces
* chore(app): keep browser font tests with typography split
- Remove the browser font transport test inherited from a mixed cancellation commit; the source and coverage remain on the typography branch and safety snapshot
* test(vue): assert injected render suspension
- Exercise shouldSuspendRender instead of removed Core loading state\n- Preserve the contract that rendering resumes without a version change
* test(app): complete atomic preparation contracts
- Acknowledge first presentation in headless file-open tests\n- Assert the cancellable font-loading signature at the Tauri fallback boundary
* fix(app): preserve preparation cancellation
- Stage imported graphs before mutating live tabs and propagate aborts through page, DOM, font, and storage work\n- Use the accessible progress primitive and clamp determinate values\n- Cover fallback-font cancellation and yield pending-open test polling to the task queue
* test(text): await fallback font request cancellation
Start the mocked remote font request before aborting so the test proves that the active request receives the preparation signal.
- Derive default instance placement from transformed component bounds
- Convert the target point into the destination parent coordinate space
- Preserve explicit drag-and-drop coordinates and undo snapshots
* refactor(editor): separate canvas view state
- Classify shared and view-local editor state explicitly
- Let canvas surfaces render supplied view state and report their viewport
- Preserve the existing one-canvas behavior by default
* fix(canvas): preserve loading render state
* refactor(editor): group state ownership modules
- Move shared and view state into the editor state namespace
- Model the partition with explicit interfaces and default factories
- Derive runtime view keys from the default view object
- Rename first-party API, RPC, JSON, CORS, SVG, JSX, and related identifiers to preserve acronym casing
- Keep upstream and serialized boundary names unchanged
- Add a lint guardrail and migration notes for exported APIs
- Resolve the active graph at action time and map edit geometry through full world transforms
- Regenerate live path fills and discard stale imported stroke outlines after edits
- Preserve fill and stroke geometry through resize previews, undo, and redo
- Cover graph replacement, nested transforms, edit history, resizing, and CanvasKit output
Co-authored-by: Rob Coenen <753704+rcoenen@users.noreply.github.com>
- Add secure Recraft and fal.ai clients with centralized credentials and Media settings
- Replace image rectangles with editable vector frames in one undoable operation
- Bound desktop proxy responses, timeouts, and redirects for provider downloads
- Cover provider parsing, placement, settings, context-menu visibility, and undo
Co-authored-by: Rob Coenen <753704+rcoenen@users.noreply.github.com>
- Match numeric keypad input by generated key so NumLock-off navigation remains available
- Coalesce buffered opacity updates into one undo interaction
- Add end-to-end coverage for digit composition, modifiers, and undo
- Complete placement and undo registration before awaiting image hydration
- Bound Figma image requests with timeouts and use shared Tauri detection
- Localize missing-image notifications and cover stalled requests
Add /^\d+$/ guard to opacityFromBuffer so strings like '12x', '5abc',
or '3.5' return 100% instead of silently parsing the numeric prefix.
Add unit tests for partial, decimal, scientific, negative, and
whitespace-prefixed buffers.
Add Number.isFinite guard to editor.setOpacity so NaN, Infinity, and
-Infinity are rejected silently instead of corrupting node opacity.
Tighten opacityFromBuffer to use Number.isFinite for the same reason.
Add 3 unit tests covering NaN, Infinity, and -Infinity rejection.
Extract opacityFromBuffer as a pure function in core/editor and test
it directly. Covers Figma semantics: 0→100%, 5→50%, 28→28%, 00→0%,
100→100%, clamp at 100%, NaN guard. Refactor the keyboard handler to
reuse the shared function instead of inlining the buffer math.
- Move setOpacity to core editor action (packages/core/src/editor/nodes.ts)
so it is testable directly without duplicating logic in tests
- Replace external window listener with tinykeys bindings in
registerKeyboardShortcuts, reusing shared input/focus guards
- Add 20 bindings for Digit0-9 and Numpad0-9 with modifier guards
- Implement Figma-style multi-digit buffer: 2→20%, 28→28%, 00→0%, 100→100%
with 400ms timeout and auto-apply at 3 digits
- Simplify selection.setOpacity command to delegate to editor.setOpacity
- Rewrite unit tests to call editor.setOpacity directly
- Add ofetch to dependencies (was imported but missing from package.json)
- Update CHANGELOG
Add opacity keyboard shortcuts matching Figma: pressing Digit1-Digit9
sets the selected layers to 10%-90% opacity, and Digit0 sets 100%.
- Add 'selection.setOpacity' EditorCommandId with registry metadata
(shortcut display '1-9, 0') so it appears in the command cheatsheet
- Implement the command in selection.ts using editor.undo.runBatch +
updateNodeWithUndo, following the toggleVisibility/setBlendMode pattern
- Add setOpacityTarget closure in useEditorCommands so the external
keyboard handler can inject the digit value before runCommand
- Add canSetOpacity capability (enabled when there is a selection)
- Add bindOpacityKeys handler in src/app/shell/keyboard/opacity.ts,
following the nudging.ts external-handler pattern since the opacity
value comes from event.code and cannot be expressed as a fixed
keybinding in the registry
- Guard against text editing, number field focus, and modifier keys
- Add i18n label 'setOpacity' across all 8 locales
- Add unit tests for single/multi selection, undo batch, and no-op
- Normalize auto-layout text imported from Figma when fixed bounds match derived text layout
- Recompute auto-size text bounds on typography metric changes
- Clear stale Figma derived text layout caches when text sizing changes
- Cover FIG import text sizing and line-height resize regressions
* fix(resize): scale children when resizing GROUP or BOOLEAN_OPERATION
Resizing a group (or boolean operation) only moved the bounding box and
rescaled the selection outline — the rendered fill stayed at its original
size, so the blue outline and the filled content visibly diverged.
Two root causes, both in the resize preview path:
1. During drag, children were scaled in the preview graph via
updateNodePreview(), which mutates nodes without emitting a node:updated
event. The renderer's vector/geometry caches (vectorPathCache,
fillGeometryCache, strokeGeometryCache) were therefore never invalidated,
so fills kept drawing from stale, unscaled cached paths while outlines
drew live from the new dimensions. Fixed by calling
renderer.invalidateVectorPath(childId) after each child preview update.
2. On commit, the final scaled values were written with updateNode() but no
repaint was requested, leaving the canvas frozen on the last mismatched
drag frame until the next interaction. Fixed by calling requestRepaint()
after commitGroupResize().
collectDescendants() now also collects children for BOOLEAN_OPERATION nodes,
which render purely from child geometry (no node-level scale), so scaling the
children is the only way to scale the result — same mechanism as GROUP, no
double-transform. Group resize verified visually; boolean resize verified in
-browser via the editor automation API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(editor): preserve vector undo during group resize
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
- Update EffectType schema assertion for modern Figma enum values
- Treat unsupported OpenType font buffers as unavailable
- Skip CJK outline fallback coverage when the LFS font fixture is not materialized
- Move codec, NodeChange conversion, and instance overrides under kiwi/fig
- Flatten the codec entrypoint to kiwi/fig/codec
- Keep schema-runtime as the generic Kiwi parser/compiler boundary