StyleRun model: array of {start, length, style} with
CharacterStyleOverride (fontWeight, italic, textDecoration, etc.).
Renderer uses ParagraphBuilder.pushStyle/pop for mixed-style text.
TextEditor delegates paragraph building to renderer.buildParagraph().
⌘B/I/U apply to selection range when text is selected, or toggle
whole-node style when no selection. Style runs adjust on
insert/delete to preserve formatting across edits.
Fig roundtrip: import characterStyleIDs + styleOverrideTable from
TextData, export back with deduped style table.
B/I/U/S toggle buttons in TypographySection.
TextEditor class in core — cursor positioning, selection, word
boundaries, line navigation via CanvasKit Paragraph API
(getGlyphPositionAtCoordinate, getRectsForRange).
Renderer draws selection highlights and blinking caret directly
on the canvas. Blue outline around text node during editing.
Phantom textarea captures input, IME composition, clipboard,
arrow keys, Home/End, word/line movement (⌥/⌘ modifiers).
Replaces the old visible <textarea> overlay.
Rust: list_system_fonts and load_system_font Tauri commands using
font-kit crate for cross-platform system font access.
TS: font shim detects Tauri runtime and uses invoke() for font
listing/loading instead of queryLocalFonts (unavailable in WKWebView).
- Column per mode (Name | Mode 1 | Mode 2 | ...)
- Color swatch inside value cells, not next to name
- Search bar in top-right header area
- Single '+ Create variable' button at bottom
- Wider dialog (800px) for multi-mode tables
Three collections with multi-mode support:
- Primitives (Light/Dark): 9 color variables with dark mode values
- Semantic (aliases): Background, Text, Border, Accent, Success, Error
- Spacing (Default/Compact): 8 number variables for space + radius tokens
Binds variables to dashboard sidebar, header, frame, and titles.
Match Figma's pattern: Variables section in page-level properties
panel with settings icon that opens a full dialog (reka-ui Dialog +
Tabs + Editable). Remove sidebar panel approach.
Move engine (scene-graph, renderer, layout, codec, kiwi) into
packages/core/ as a Bun workspace package. The app's src/engine/
files become thin re-export shims so all existing imports still work.
- Bun workspace with packages/core
- Zero DOM dependencies in core
- Headless-friendly CanvasKit init (locateFile option)
- Constants split: engine constants in core, UI constants in app
- All 76 unit tests pass, lint + typecheck clean, vite build works
renderVersion bumps on every pan/zoom/hover — it's the canvas redraw signal.
sceneVersion only bumps when the scene graph actually changes.
Panels (properties, layers, context menu, pages, export preview) now
depend on sceneVersion, so they don't re-render during navigation.
The default CanvasKit WASM build lacks JPEG/WEBP encoders. Instead of
switching to the full build (+1MB), render PNG via CanvasKit then
re-encode to the target format using OffscreenCanvas.convertToBlob().
Extract renderThumbnail and renderNodesToImage into render-image.ts,
reusing the same offscreen CanvasKit surface approach that fig-export
used for thumbnails.
- ExportSection in properties panel with scale/format selectors,
multi-export support, and live preview with checkerboard background
- Export via context menu and ⇧⌘E shortcut
- Tauri save dialog, File System Access API, and download fallback
- JPG renders with white background, PNG/WEBP with transparency
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
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.