- Add colorToHex8 (hex with alpha via culori formatHex8)
- Add colorToCSSCompact (no-space rgba for Tailwind shadow classes)
- Add normalizeColor (partial Color → full Color with defaults)
- Add colorDistance (Euclidean RGB via culori differenceEuclidean)
- Replace colorToCSS body with culori formatRgb
- Change rgba255ToColor to positional args (r, g, b, a)
- Remove duplicate formatColor from svg-export.ts and export-jsx.ts
- Remove duplicate colorDistance from tools/analyze.ts
- Remove duplicate convertColor from kiwi/kiwi-convert.ts
- Replace hand-rolled hex+alpha in HsvColorArea.vue with colorToHex8
- Replace hand-rolled rgba255→Color in HsvColorArea.vue with rgba255ToColor
- Expand color.test.ts from 16 to 34 tests
- Strip tools/schema.ts from 1831 to 65 lines (remove duplicate tool defs)
- Fix tools/index.ts to export ALL_TOOLS from registry instead of schema
- Fix renderJsx → renderJSX typo in tools/create.ts and structure.ts
- Add geometry.ts with shared rotation/coordinate math utilities
- Add isArrayMixed() helper to use-multi-props.ts, deduplicate FillSection/StrokeSection/EffectsSection
- Fix broken test imports: use @open-pencil/core instead of relative paths into packages/core/src/
- Merge duplicate import statements across all test files
- Export profiler internals and style-run helpers from core index.ts
- Add 6 new test files: color, undo, snap, vector, style-runs, text-editor (98 new tests)
- Update renderer-effects test for renderer/ directory split
- Add index signature to NodeChange for Kiwi codec fields
- Add blobs field to FigmaMessage
- Use 'in' type guards instead of Record casts in ai-adapter
- Use Object.fromEntries for Partial<SceneNode> property copying
- Use satisfies instead of as for handle position map
- Type node() to accept { children?: unknown; [key: string]: unknown }
- Remove ext() helper in kiwi-convert (direct property access)
37 occurrences → 1 (Kiwi codec boundary only).
Remove hardcoded manifest link from index.html (VitePWA injects
it in production builds). Catch FontFace.load() rejections from
invalid font data with a warning.
Remove 14 src/engine/ shims and entire src/kiwi/ directory.
Only src/engine/fonts.ts remains (has real Tauri font logic).
Replace isTauri() with IS_TAURI constant.
- / shows empty canvas
- /demo loads demo content (for app.openpencil.dev landing link)
- Title template: '{page} — OpenPencil' via useHead
- 'Try it online' links in README and docs point to /demo
- New useInlineRename composable: shared editing state, commit logic,
and keyboard handling for inline rename across panels
- LayerTree: double-click a layer name to rename inline
- PagesPanel: refactored to use useInlineRename, removing duplicate code
Closes#57
Use one constant spring (stiffness 800, damping 50) for both drag
tracking and snap animation. Eliminates the transition switch that
caused motion-v to reinitialize mid-animation. Extract spring
constants to constants.ts. Fix selectTab not updating targetHeight.
* Add SVG export
Scene graph → SVG serializer with support for rectangles, ellipses,
lines, stars, polygons, vectors (fill/stroke geometry and vector
networks), text with style runs, gradients (linear, radial, angular),
image fills, effects (drop shadow, inner shadow, blur), opacity,
rotation, flips, blend modes, clip paths, and nested groups.
- packages/core: svg-node.ts (minimal XML builder), svg-export.ts
(serializer), ExportFormat now includes 'SVG', computeContentBounds
extracted for reuse
- packages/cli: export command accepts --format svg
- UI: SVG added to ExportSection format picker (scale hidden for SVG),
context menu 'Export as SVG' option
- 47 new tests covering SVGNode builder, geometry blob → path, vector
network → path, and full node export for all shape types
* Update changelog
* Add export_svg tool to ToolDefs
Available via MCP, AI chat, and CLI eval command.
* Replace standalone export items with Copy/Paste as submenu
Figma-style submenu: Copy as text, Copy as SVG, Copy as PNG (⇧⌘C),
Copy as JSX. Keeps Export as PNG (⇧⌘E) as a separate item.
* Remove standalone Export as PNG from context menu
* Update changelog
Fix renderer-effects test: check drawStrokeWithAlign call site instead of
drawNodeStroke helper definition (stroke align refactor moved the method).
Add a step to build.yml that resolves git authors to GitHub usernames via
the compare API and appends a Contributors section to release notes.
Fix renderer-effects test: check drawStrokeWithAlign call site instead of
drawNodeStroke helper definition (stroke align refactor moved the method).
Add a step to build.yml that resolves git authors to GitHub usernames via
the compare API and appends a Contributors section to release notes.
Load a system CJK font (PingFang SC, Microsoft YaHei, Noto Sans CJK, etc.)
at startup and pass it as a fallback in CanvasKit's fontFamilies array.
Falls back to Google Fonts (Noto Sans SC) when no system font is available.
Also fixes loadFont to only cache fonts that successfully register with
CanvasKit's TypefaceFontProvider, preventing invalid font data from being
treated as loaded.
Closes#48
ESM dynamic imports require explicit file extensions for resolution on
Windows + Bun. The extensionless '../render/render-jsx' path worked on
macOS/Linux but failed on Windows with 'Cannot find module' error.
Fixes#43
- Scope hover hit-test to current page (was searching all pages including
internal component page, causing ghost hover outlines)
- Frames/sections without visible fills or strokes are click-through
- Groups are always click-through (only children are hittable)
- Clipping parents reject hits outside their bounds
- Instances/components check children before falling back to fill check
- Switch test fixtures from material3.fig (55MB) / nuxtui.fig (82MB) to
gold-preview.fig (537KB) for fast dev runs, gate heavy fixtures behind
BUN_HEAVY_TESTS env var (enabled in CI)
vectorNetworkToPath drew all segments forward (to seg.end), which
broke when buildChains traversed a segment in reverse. Closed shapes
like parallelograms had degenerate last segments.
Fix: track current vertex through the chain and draw each segment in
the correct direction (forward or reverse), swapping bezier control
points when reversed.
Region loops (addLoopToPath) are left unchanged — Figma stores loop
segments pre-oriented so they should always be drawn forward.
- Replace O(N²) iterative convergence loop with BFS that correctly
handles intermediate DSD-targeted clones (push to queue regardless
of visited, only skip size update for dsdSizeSet nodes)
- Fix preComputedRoot cache lookup: was using childId instead of
child.componentId as key
- Remove stray blank line
findNodeByComponentId used root-matching which returned the first
child when multiple siblings shared the same component root. This
caused all symbolOverride guidPaths targeting different siblings
(e.g., 3 _stepper instances inside a Stepper component) to resolve
to the first sibling.
Fix: separate into 3 passes — exact componentId match first, then
unambiguous root match (only when exactly one child shares the root),
then deep recursion.
Previously only applied CPA from cloned instance sources (via
componentId chain). Instances created directly from kiwi data (e.g.,
_datepicker with boolValue=false for 'Date Range List') never had
their own CPA processed, leaving children visible that should be
hidden.
syncChildrenDeep was syncing all children from source to clone,
overwriting nodes that were directly targeted by symbolOverrides
(e.g., white stroke on a Vector set by _icon-xs SO being overwritten
with dark gray from the check component's default).
Fix: pass the seeds set through to syncChildrenDeep so it skips
nodes that were explicitly overridden.
Replace BFS (which skipped DSD-modified nodes in visited set, breaking
the chain for deeper clones) with iterative convergence loop. Track
dsdSizeSet separately from dsdModified so nodes with only position/
geometry changes inherit size from their source.
Fix transitive override propagation: re-clone from source (not component)
to preserve componentId chain for DSD. Add syncChildrenDeep for deep
property propagation (stroke colors on nested vectors).
Reduces vector overflow in gold-preview.fig from 25 to 0 (visible,
unclipped).
Intermediate clones may already have correct values but their own
clones (deeper levels) may not. Must always push to queue to reach
the full clone chain.
derivedSymbolData guidPaths reference the post-swap tree structure
(e.g., heart Vector inside a swapped home slot), but resolution was
running before overrides modified the tree. After repopulateInstance,
the old children are deleted and replaced with new ones from the
swapped component, making pre-resolved targets stale.
Changes:
- Move DSD resolution to after all overrides (symbolOverrides,
transitive sync, componentProperties) so guidPaths can reach
the swapped-in children
- Pre-compute componentId root map while all internal page nodes
are alive — instance swaps delete intermediate clones, breaking
componentId chains used by findNodeByComponentId
- Use preComputedRoot in findNodeByComponentId to match across
clone levels even when intermediate nodes are deleted
- Propagate DSD changes through clone chains via BFS
Upgrade reka-ui 2.8.2 → 2.9.0 and replace the hand-rolled HSV color
area, hue slider, alpha slider, and hex input with reka-ui's new
ColorArea, ColorSlider, and ColorField primitives.
- ColorAreaRoot + ColorAreaArea handle the 2D saturation/brightness
picker with proper gradient backgrounds and keyboard navigation
- ColorSliderRoot + ColorSliderTrack for hue and alpha channels with
automatic gradient rendering
- ColorFieldRoot + ColorFieldInput for hex color text input with
arrow key increment/decrement support
- Bridge between our Color type (0–1 range) and reka-ui's Color
(0–255 RGB) via hex string serialization
Removes ~110 lines of custom HSV↔RGB conversion math, pointer capture
handling, and CSS gradient computation.