Two fixes for component instances with multi-level nesting:
1. getComponentRoot now falls back to kiwi symbolData when graph
nodes are deleted (internal page cleanup). Resolves the component
chain via symbolData.symbolID so findNodeByComponentId can match
clones whose componentId points to a deleted source node.
2. propagateOverridesTransitively skips nodes that were directly
targeted by applySymbolOverrides. Previously the sync would
overwrite override values (e.g. text 'Health' → 'Badge') with
the source component's defaults.
Use fillGeometry/strokeGeometry blobs from .fig files for pixel-perfect
vector rendering. These pre-computed outlines account for stroke alignment,
caps, joins, and miter limits — eliminating white gaps between adjacent
stroked shapes.
For clipboard paste (where geometry blobs are unavailable), convert
vectorNetwork paths to filled stroke outlines via CanvasKit path.stroke().
Apply derivedSymbolData transforms, sizes, and geometry during import
so instance children render at the correct scale and position. When
derivedSymbolData provides size but no geometry, scale inherited blobs
proportionally.
Three issues fixed:
1. componentPropAssignments inside symbolOverrides are scoped to the
nested instance their guidPath resolves to, not the top-level
instance. Resolve guidPath first, then apply to that subtree.
2. Instance swap values can use guidValue (GUID object) in addition
to textValue (string). Handle both formats.
3. Transitive clone sync was running BEFORE component properties,
causing repopulateInstance to wipe children that had just received
visibility assignments. Reorder: symbolOverrides → transitive sync
→ componentProperties → derivedSymbolData.
Also extracted propagateOverridesTransitively into its own function
and guarded against propagating undefined values that would overwrite
explicitly-set properties.
Figma pre-computes child dimensions for each combination of
component property values and stores them in derivedSymbolData.
Apply these size overrides using the same guidPath resolution
as symbolOverrides so containers shrink correctly when boolean
properties hide children.
Evaluate componentPropAssignments from instances against
componentPropRefs on cloned children. Handles VISIBLE (boolean
toggle) and OVERRIDDEN_SYMBOL_ID (instance swap) property fields.
Walks componentId chain to find propRefs on source nodes.
When an override targets an instance node directly (not a descendant),
the node's componentId matches the remapped target. Check the current
node itself before searching descendants. Also prefer exact componentId
match over root-chain match for more precise resolution.
Extract populateAndApplyOverrides() into instance-overrides.ts shared
by fig-import.ts and clipboard.ts. The clipboard path previously only
handled single-level guid paths and missed nested overrides, instance
swaps, and transitive propagation.
Bun picks the "node" condition pointing to dist/index.js which doesn't
exist in CI or fresh checkouts. Explicit "bun" condition ensures Bun
always resolves to source files.
- Resolve symbolOverride guidPaths by overrideKey instead of guid
- Handle overriddenSymbolID for instance component swaps
- Propagate overrides transitively through the componentId clone chain
- Preserve internalOnly flag on canvas nodes during export
- Scope computeAllLayouts to current page for paste/undo/font-load
Profiler modules in packages/core/src/profiler/:
- FrameStats: rolling 120-frame buffer tracking FPS, CPU/GPU times, node counts
- GpuTimer: EXT_disjoint_timer_query_webgl2 wrapper for GPU-side timing
- DrawCallCounter: WebGL context proxy counting draw calls per frame
- PhaseTimer: Chrome DevTools Performance panel custom tracks via User Timing API
- HudRenderer: in-canvas overlay with stats text and frame time bar graph
- CaptureStack: per-node profiling with speedscope JSON export
- RenderProfiler: orchestrates all layers with zero cost when disabled
Integration:
- SkiaRenderer.render() instrumented with phase markers for scene, picture
recording/replay, section titles, component labels, selection, rulers, flush
- Node counting and viewport culling tracking in renderNode()
- WebGL2 context passed through for GPU timer and draw call counter
- Shift+P keyboard shortcut to toggle HUD overlay
- toggleProfiler() added to editor store
Figma .fig files store instance overrides (text, fills, visibility,
layout props, corner radii, etc.) in symbolData.symbolOverrides on
each INSTANCE node. Each override targets a descendant via guidPath
(chain of original Figma GUIDs).
Implementation:
- convertOverrideToProps() in kiwi-convert.ts maps raw kiwi override
fields to SceneNode properties (reuses existing conversion helpers)
- resolveOverrideTarget() walks the cloned instance tree to find the
target node by following componentId chains
- getComponentRoot() traverses componentId references to handle
multi-level cloning (instance of instance of component)
- 49K+ overrides applied across 8.8K instances in Preline UI file
Instance population can create scene graph children that don't
have corresponding Yoga nodes (e.g. when the Yoga tree was built
before cloned children were fully resolved). Skip null yogaChild
instead of crashing.
During .fig import, instance nodes have no children — Figma expects
them to be cloned from their component. The componentId references
used original Figma GUIDs that didn't match our generated node IDs.
Fix:
- Track original GUID → new node ID mapping during import
- Remap all instance componentId references after node creation
- Iteratively populate empty instances from their components
(multiple passes needed since cloning creates new instances)
Checkbox item in File > Auto-save to local file. Enabled by default.
When disabled, the 3s debounced auto-save to the opened .fig file
is skipped — user can still save manually with ⌘S.
- Add destroyed flag to SkiaRenderer.destroy() to prevent double-free
- Check destroyed flag in renderNow() to skip rendering after unmount
(loadFonts promise could resolve after component unmounts)
localeCompare is locale-dependent and produces different orderings
across runtimes (Bun vs browser, Safari vs Chrome). Figma's
fractional indexing uses base-95 ASCII which sorts correctly by
code point.
- Extract font key collection from editor store into
collectFontKeys() in packages/core/src/fonts.ts
- 18 tests: styleToWeight, weightToStyle, markFontLoaded/isFontLoaded,
collectFontKeys (nested nodes, style runs, deduplication, default
font filtering, invalid IDs)
- Make loadFontsForNodes async and await it in openFigFile so the
loading overlay stays visible until all Google Fonts are ready
- Load fonts when switching pages (previously only loaded for first page)
- Paste still fire-and-forgets font loading for instant feedback
- Remove opacity ScrubInput from color row (matches Figma layout)
- Constrain ColorInput with min-w-0 flex-1 so it doesn't push buttons off
- Add shrink-0 to eye/remove buttons to prevent squishing
- Move side selector from header to details row (right of weight input)
- Use 4-dot grid icon for the side selector button (matches Figma)
- Highlight button border with accent color when independent weights active
- Use 3-line stroke weight icon instead of filled rect
- Header now only has section label + add button
Global * scrollbar styles caused scrollbars to appear on the
menu bar and other elements. Replace with scrollbar-thin utility
applied only to panel scroll containers.
Fetch TTF files from Google Fonts when local and bundled fonts
are unavailable. Uses the Webfonts API for direct TTF URLs per
variant. Font file listings are cached per family; failed lookups
are recorded to avoid retries.