Add @/ → src/ alias in vite.config.ts and tsconfig.json.
Rewrite all ../ imports across 32 files to use @/ paths.
Same-directory ./ imports kept as-is.
- Move Vector, Matrix, Rect to src/types.ts as shared primitives
- GradientTransform is now a type alias for Matrix
- Consolidate Window API declarations (File System Access,
queryLocalFonts) in src/global.d.ts
- Remove duplicate KiwiNodeChange from fig-export.ts and
clipboard.ts, reuse NodeChange from kiwi/codec.ts
- Add missing fields to codec NodeChange: frameMaskDisabled,
stackWrap, stackPositioning, stackChildPrimaryGrow, vectorData
- Widen codec Paint.type and Effect.type unions to match scene-graph
- Replace inline Rect definitions across 5 files with shared type
- fig-export.ts fillPaints now constructs typed Paint objects
Add [appearance:textfield] alongside the WebKit spinner hide rule.
Firefox ignores ::-webkit-inner-spin-button but respects
appearance: textfield to remove native number spinners.
Move ew-resize cursor and pointerdown handler from inner spans
to the outer div for simpler event handling.
Note: ew-resize/col-resize cursors are broken in Safari 26.2
(WebKit bug 303845, fixed in Safari 26.3 Beta).
Use a real 8px-wide element with negative margins instead of a
pseudo-element on a zero-width handle. The visible line is 1px
centered inside, highlighted on hover and drag. Works in Safari.
ResizeObserver was recreating the surface on every tick. Now
coalesced via rAF so it fires at most once per frame — surface
always matches the canvas size so no skew.
Uses SplitterGroup/SplitterPanel/SplitterResizeHandle. Panels
default to 15% width, min 10%, max 30%. Layout persists across
reloads via auto-save-id. Resize handle highlights blue on hover.
- POLYGON (triangle icon) and STAR (star icon) in the shapes flyout
- Renderer draws regular polygons and stars using pointCount and
starInnerRadius properties on SceneNode
- Flyout dropdown now shows keyboard shortcuts (R, L, O) aligned
right like Figma's context menu
- Polygon defaults to 3 sides, Star to 5 points with 0.38 inner ratio
- Fill, stroke, hover highlight, and selection outline all work
Triggers on version tags (v*) or manual dispatch.
Builds .msi/.exe for Windows x64/arm64 and .dmg for macOS
x64/arm64. Creates a draft GitHub release with all artifacts.
The properties panel has overflow-y: auto which clips absolute
positioned popovers. Expand controls inline below the effect
row instead — simpler and always visible.
- Compact row: color swatch + type dropdown + eye toggle + minus
- Expanded controls in a floating popover anchored to the left
- Labeled rows: Position (X/Y), Blur, Spread, Color with hex + opacity
- Type dropdown in popover header with X close button
- Eye/minus icons hidden until row hover (matches fill/stroke rows)
Support adding, removing, and editing drop shadows, inner shadows,
layer blurs, background blurs, and foreground blurs. Each effect row
has visibility toggle, type dropdown, and remove button. Expanded
view shows offset, blur, spread, and color controls for shadows,
or just blur radius for blur effects.
Option key on Mac transforms e.key into special characters
(e.g. ⌥K → ˚), so modifier+letter shortcuts must check
e.code (physical key) instead of e.key (character produced).
Editing a main component now propagates to all instances:
- Instance children are mapped to component children via componentId
- syncInstances() copies properties from component to instances,
skipping any property marked in the overrides record
- New children added to a component appear in existing instances
- Store calls syncIfInsideComponent after updateNode, commitMove,
commitResize — changes propagate automatically
- Demo creates real instances (Button, Badge) in the App Preview
Tests added: instance creation with child mapping, sync propagation,
override preservation, new child addition, detach.
Multiple state changes between display refreshes (e.g. rapid
trackpad wheel events during zoom) now produce a single render
per frame instead of one per event. Zoom feels identical but
CPU usage drops since redundant intermediate renders are skipped.
Pre-allocate auxFill, auxStroke, and opacityPaint on the renderer.
Reconfigure them before each use instead of new/delete cycles.
Reduces per-frame WASM Paint allocations from 29 to 10 (the
remaining 10 are cold-path: pen tool, rulers, one-off overlays).
Hot paths covered: renderNode opacity layers, hover/drop highlights,
selection handles (called 8-16× per frame), section title pills,
component label icons+text, size pill, marquee, layout indicator,
rotation handle, component set dashed border, drop shadows.
Compute the world-space viewport from pan/zoom and skip nodes
whose bounds don't intersect it. Only culls leaf nodes and
clipped containers — unclipped parents are still traversed since
their children may extend beyond the parent bounds.
Handles rotation by expanding the bounding box to the diagonal.
Section titles and component labels also culled during collection.
Export path (renderSceneToCanvas) uses infinite viewport to render all.
On resize the renderer is recreated with a fresh TypefaceFontProvider.
loadFont() had an early return for cached fonts that skipped registering
with the new provider, so ParagraphBuilder had no fonts to render with.
Children of auto-layout frames were positioned at their createShape
coordinates instead of being laid out. Call computeAllLayouts() after
building the demo tree so everything is properly arranged on load.
Showcases the full feature set:
- Components section with Button/Primary, Button/Secondary, Tag,
Avatar, Card (auto layout + progress bar), Input, Badge
- Color palette swatches
- App Preview section with a dashboard layout: sidebar nav,
stat cards with auto layout, bar chart with gradient fills,
table header with space-between layout
- Gradient cards, typography scale, mixed shapes
1. Hit test: components/instances are opaque containers — clicking
their children selects the component itself. Double-click enters
and selects the child inside (like Figma).
2. Component label hit testing: clicking the purple name label
selects the component, enabling drag-by-title (same as sections).
3. Components created from wrapping have no fill (transparent) —
was incorrectly adding a white background rect.
Reka-ui portals teleport content to <body>, so scoped styles
can't reach them. Instead of unscoped CSS, use Tailwind utility
classes directly — consistent with every other component.
- data-[highlighted]:bg-hover for keyboard/mouse highlight
- data-[disabled]:text-muted for disabled state
- Component items in #9747ff with /12 hover and /40 disabled
- No <style> block at all
- CanvasContextMenu now exposes onContextMenu via scoped slot instead
of wrapping in a <div> (which broke flex layout — the div had no
flex-1, so it collapsed and never received contextmenu events)
- EditorCanvas binds the handler directly on its own flex-1 div
- Fix lucide/diamonds icon not found — use lucide/component for
COMPONENT_SET (the actual 4-diamond component icon)
reka-ui's ContextMenuTrigger calls event.preventDefault() after
await nextTick(), which is too late to suppress the native context
menu in Tauri's webview. Replace with a custom implementation using
@floating-ui/vue for positioning (already a transitive dependency)
and proper ARIA roles (role=menu/menuitem).
- e.preventDefault() called synchronously in our handler
- VirtualElement anchor at click coordinates
- flip/shift middleware for viewport-aware positioning
- onClickOutside + Escape to dismiss
- Submenu hover for 'Move to page'
- Remove redundant contextmenu handler from use-canvas-input
- CanvasContextMenu: reka-ui ContextMenu with dark theme matching editor
- Clipboard: Copy, Cut, Paste here, Duplicate, Delete
- Z-order: Bring to front (]), Send to back ([)
- Move to page: submenu listing all other pages
- Grouping: Group (⌘G), Ungroup (⇧⌘G), Add auto layout (⇧A)
- Components: Create component (⌥⌘K), Create component set (⇧⌘K),
Create instance, Go to main component, Detach instance (⌥⌘B)
— component items styled in purple
- Visibility: Show/Hide (⇧⌘H), Lock/Unlock (⇧⌘L)
- Context-aware: items show/hide based on selection type and count
- Right-click selects node under cursor before showing menu
- Store: bringToFront, sendToBack, toggleVisibility, toggleLock,
moveToPage, renameNode operations
- Keyboard: ], [, ⇧⌘H, ⇧⌘L shortcuts
- FillPicker component: 3 category tabs (Solid, Gradient, Image), gradient
subtype dropdown (Linear/Radial/Angular/Diamond), draggable gradient stop
bar, per-stop color/position/opacity editing with ScrubInput
- FillSection uses FillPicker instead of ColorPicker for full Fill editing
- Default gradient transforms for each subtype so renderer picks them up
- colorToRgba255/rgba255ToColor helpers in color.ts
- Custom SVG icons for fill type tabs (solid square, gradient fade)
fflate's zipSync produces ZIP files that Figma rejects. Move the entire
fig-kiwi container + Zstd compression + ZIP packaging into a single
Rust command (build_fig_file). Zstd now includes content size via
set_pledged_src_size. Web fallback still uses fflate/deflate.
Render the current page to a 400×225 offscreen CanvasKit surface,
fit content with padding, and include as thumbnail.png in the ZIP.
Falls back to a 1×1 placeholder when renderer is unavailable (tests).
Figma's importer requires thumbnail.png to exist in the ZIP archive.
Without it, import fails with 'Internal error during import' regardless
of how correct the Kiwi data is.
Also: STORE compression for canvas.fig (already internally compressed),
and match Figma's default DOCUMENT/CANVAS fields.