Implement the analyze overlaps CLI command, RPC endpoint, and ToolDef
to enable heuristic detection of visual overlaps, parent overflows, and
overlay patterns. Geometry computation now uses world-matrix visual
bounds to handle nested ancestor rotations and clipping frames correctly.
Update file-backed CLI commands to use Node fs/promises instead of Bun
runtime file APIs. This is required for the published CLI to function
when installed and executed in a standard Node environment. Add the
package.json subpath export to the core package for metadata consumers.
- Use twirlwind for CSS-to-Tailwind class conversion instead of
450 lines of manual lookup tables and formatting functions
- Build a CSS style object from scene node properties, pass to twirl()
- Remove design-jsx/tailwind.ts (pxToSpacing, colorToTwClass, etc.)
- Remove unused Tailwind-specific helpers from jsx/helpers.ts
- Update tests for twirlwind's v4-native output format
Replace hand-rolled regex heuristic with proper acorn JS parsing.
The AST correctly identifies ExpressionStatement as the last node
regardless of trailing semicolons, comments, or complex syntax.
Fixes edge cases like '42;' which the regex wrongly treated as a
statement.
Co-authored-by: stiff <v.stiff@gmail.com>
Co-authored-by: Jais Pedersen <jais@pedersens.net>
- Upgrade @open-pencil/yoga-layout to include free() and freeRecursive()
- freeYogaTree() in layout.ts now properly releases WASM memory
- FinalizationRegistry remains as a safety net for missed cleanup
Fixes#183
- Move clipboard-text-outlines.ts → text/opentype.ts
- Move kiwi/serialize-clipboard-v4.ts → text/clipboard-derived-text.ts
- Delete types-opentype.d.ts in favor of @types/opentype.js
- Add opentype.js to core package dependencies
- Add Vite alias for opentype.js ESM resolution
- Update all imports across core, kiwi, clipboard, and tests
- Merge loadLocalFont + tryLoadLocalFont into findLocalFont with
optional style param — eliminates duplicated queryLocalFonts logic
and adds consistent variable font filtering for CJK fallback
- Replace nested ternary font matching with a simple loop over
family name candidates
- Extract fetchBundledFont helper for browser/Node font resolution
- Ship Inter-Regular.ttf in packages/core/assets so headless CLI
can resolve the bundled fallback via import.meta.url instead of
fetch('/') which fails in Node/Bun (fixes#121 scenario)
- Use IS_BROWSER constant instead of repeating typeof window checks
- Replace calc.ts (150 lines) with expr-eval library
- Replace rgbLuminance with culori wcagLuminance
- Use @iconify/utils iconToSVG for viewBox normalization in iconify.ts
- Extract describe-shared.ts: findAncestorBackground, looksLikeButton, CONTAINER_TYPES
- Extract icon-render.ts: shared createIconFromPaths for create.ts and renderer.ts
- Inline codegen prompt as TS constant, remove runtime fs.readFile from MCP server
- Fix blur effect color alpha (a:1 → a:0)
- Encapsulate module-level mutable state in src/ai/tools.ts into RunState class
- Add network check to iconify tests (skip gracefully offline)
- Remove Caddyfile.dev and components.d.ts from tracking, add to .gitignore
- Track per-run step count, warn AI when 5 steps remain via _warning
field injected into tool results
- Show 'Continue' button in chat when step limit (50) is exhausted
- Add step budget section to system prompt so AI plans ahead
- Reset run step counter on each new agent call via prepareCall
- Add nanoevents to @open-pencil/core, emit typed events from
SceneGraph mutation methods: node:created, node:updated,
node:deleted, node:reparented, node:reordered
- Subscribe to node:updated in editor store for renderer
invalidation (vector path + node picture cache) instead of
inlining it in updateNode()
- Re-subscribe to graph events after file open/reload (graph
instance is replaced)
- Replace monkey-patching of graph.updateNode in use-collab with
event subscriptions for node:created, node:updated, node:deleted,
node:reparented — fixes sync gaps for create/delete/reparent that
the monkey-patch missed
- Clean up event subscriptions on collab disconnect
* Add multi-provider AI support (Anthropic, OpenAI, Google AI, OpenAI-compatible)
- Add AI_PROVIDERS registry with per-provider model lists, key placeholders, and URLs
- Refactor use-chat composable: provider factory creates the right AI SDK model
- Per-provider API key storage in localStorage with automatic legacy migration
- New ProviderSetup.vue replaces APIKeySetup.vue with provider selector
- New ProviderSettings.vue popover accessible from gear icon in chat input
- OpenAI-compatible provider with custom base URL and model ID fields
- Install @ai-sdk/anthropic and @ai-sdk/google dependencies
- Update E2E tests for new provider setup flow
* Fix reactivity, key masking, missing model ID field, and chat reset on model change
- Use ref instead of computed for apiKey — computed getter over localStorage
wasn't triggering reactivity when the key was set
- Load stored key for new provider in providerId watcher
- Reset chat on modelId and customModelId change, not just provider switch
- Add custom model ID field to ProviderSetup.vue for OpenAI-compatible
- Simplify ProviderSettings key field — empty input with contextual placeholder
instead of fragile dot-masking that broke on partial edits
- Use data-test-id locator for model selector in E2E test
* Use useLocalStorage from vueuse instead of manual ref + watch + localStorage
Replaces 5 hand-rolled ref/watch/localStorage sync pairs with useLocalStorage.
API key uses a computed storage key that rebinds when the provider changes.
* Use useLocalStorage for collab name persistence
* Review fixes: changelog placement, remove dead class, cursor-pointer
* Extract ProviderSelect component and uiInput helper
- ProviderSelect.vue: shared select dropdown for AI providers
- ui/input.ts: shared input styling (sm/md sizes)
- Remove 7 duplicated input class strings across chat components
* Use uppercase acronyms: keyURL, customBaseURL, supportsCustomBaseURL
* Match select dropdown width to trigger via --reka-select-trigger-width
Move min-w-[var(--reka-select-trigger-width)] into selectContent base
style so all selects get it by default. Remove per-component overrides.
* Uppercase acronyms in variable names, improve setup form layout
- providerID, modelID, customModelID, setAPIKey, AIProviderID
- Keep apiKey lowercase at start (standard JS convention)
- Compact setup form: single column, full-width Connect button
- Fix grammar: 'a OpenRouter' → 'an OpenRouter'
- Shorter promo text
* Allow text selection in chat panel
* Fix chat E2E test: Save → Connect button text
* Disable mermaid in chat markdown renderer
Alias mermaid and beautiful-mermaid to empty shims in Vite config,
preventing vue-stream-markdown from attempting to load them.
* Interleave text and tool calls in chat messages
Render message parts in order instead of grouping all tool calls
first then all text. Use SDK's isToolUIPart/isTextUIPart/getToolName
instead of custom type guards. Fix error state: output-error, not error.
* Implement figma.viewport.scrollAndZoomIntoView()
Figma Plugin API method that centers the viewport on given nodes.
Reuses the same bounding box logic as the viewport_zoom_to_fit tool.
* Catch tool execution errors and return them to the AI
Instead of crashing with an unhandled exception, tool errors are
caught and returned as { error: message } so the AI can retry or
explain the failure. UI detects error outputs and shows them in red
with the error message when expanded.
* Replace esbuild with sucrase for JSX transform
Sucrase is a pure JS transform (201 KB / 46 KB gzip) that works in
both Node/Bun and the browser. Replaces esbuild (13 MB WASM) which
only worked in Node/Bun.
- buildComponent() and renderJSX() are now synchronous
- render tool works in browser AI chat (no more 'esbuild required')
- Handles full JS expressions (map, ternaries, Array.from, etc.)
* Update system prompt to prioritize render tool with JSX
Document available tags, props, layout, text, and sizing options.
Instruct the AI to use full JS expressions in JSX for complex layouts.
* Fix zoom shortcuts to match Figma
Cmd+0: Zoom to 100% (was incorrectly mapped to Zoom to fit)
Cmd+1: Zoom to fit
Cmd+2: Zoom to selection
Shift+1 / Shift+2: same as Cmd+1 / Cmd+2
Add zoomTo100() and zoomToSelection() to editor store.
Refactor zoomToFit() to use shared zoomToBounds() helper.
* Refactor keyboard shortcuts to useMagicKeys
Replace manual keydown handler with VueUse useMagicKeys + whenever
for declarative shortcut registration.
- mod() helper for cross-platform Meta/Control shortcuts
- plain() helper for modifier-free keys
- Proper modifier exclusion (⌘G vs ⌘⇧G no longer conflict)
- Add E2E tests: duplicate, zoom (⌘0/⌘1/⌘2/⇧1/⇧2), auto-layout (⇧A)
- All 26 keyboard shortcut tests pass
* Fix scrollAndZoomIntoView to actually zoom
Match Figma Plugin API behavior (equivalent to Shift-1): compute
zoom level that fits all nodes with padding, capped at 100%.
Previously only set center without adjusting zoom.
* Switch to @open-pencil/yoga-layout with CSS Grid support
Use our fork (open-pencil/yoga, grid branch) which cherry-picks the
upstream CSS Grid PRs (#1893–#1898) onto current main. The JS bindings
are ported from the old embind approach to the new wasm_bridge.c API.
npm:@open-pencil/yoga-layout alias keeps all imports as 'yoga-layout'.
Also handle FinalizationRegistry change (upstream #1908) — node.free()
no longer exists, nodes are garbage collected automatically.
* Add CSS Grid layout mode
Scene graph:
- LayoutMode gains 'GRID' option alongside HORIZONTAL/VERTICAL
- GridTrack type (sizing: FIXED/FR/AUTO, value) for track definitions
- GridPosition type (column, row, columnSpan, rowSpan) for children
- New node props: gridTemplateColumns/Rows, gridColumnGap, gridRowGap,
gridPosition
Layout engine:
- buildGridTree() sets Display.Grid and maps GridTrack[] to Yoga's
setGridTemplateColumns/Rows API with FR/Points/Auto track types
- Grid children use gridPosition for column/row placement with span
- Flex layout path unchanged
Store:
- setLayoutMode('GRID') auto-creates NxM track grid based on child
count (sqrt heuristic), defaults to 1fr tracks
UI (LayoutSection.vue):
- Grid button (grid-2x2 icon) added to flow direction row
- Columns/Rows track editors: ScrubInput for value + AppSelect for
sizing mode (Fill fr / Fixed px / Auto), add/remove buttons
- Separate column gap and row gap ScrubInputs
- Wrap button hidden when grid is active
- Flex alignment grid hidden when grid is active
Kiwi serialization skips GRID mode for now (no .fig codec support).
* Add grid layout integration tests
12 new test scenarios covering:
- Basic 2x2 grid, fixed columns, mixed fr/fixed, unequal fr weights
- Column gap, row gap, both gaps combined
- Padding offsets
- Explicit gridPosition placement, column span, row span
- Absolute children skipped
- Hidden children collapsed
- Nested grid inside flex parent (computeAllLayouts)
Also fix: grid frames as children of flex parents now correctly use
Display.Grid via configureChildAsGrid() instead of falling through
to configureChildAsAutoLayout() which set FlexDirection.
* Deduplicate grid yoga configuration
Extract configureAsGrid() and createGridChildNode() shared by
buildGridTree (root-level grid) and configureChildAsGrid (grid
nested inside flex parent).
* Add grid layout to changelog
* Add grid support to JSX and Tailwind CSS export
OpenPencil format: grid → columns/rows/columnGap/rowGap props,
child colStart/rowStart/colSpan/rowSpan.
Tailwind format: grid grid-cols-N grid-rows-N gap-x-* gap-y-*,
mixed tracks use arbitrary values (grid-cols-[200px_1fr_auto]),
child col-start-*/row-start-*/col-span-*/row-span-*.
Both formats: padding emitted for grid frames (shared with flex),
flex-only props (justify/items/wrap/gap) scoped to isFlex.
* Remove duplicate gridTrackToTw, use shared formatTrack
Also drop unused GridTrackSizing import from LayoutSection.vue.
* Fix grid icon: use layout-grid instead of grid-2x2
unplugin-icons can't resolve icon names with digits after hyphens
(grid-2x2 → grid2x2 which doesn't exist).
* Polish layout UI and fix auto-layout behavior
- Replace text labels with compact icons: ↔/↕ for gap, ☐ for uniform padding, T/R/B/L for per-side padding
- Direction-aware gap icon: ↕ for vertical, ↔ for horizontal
- Pin +/− padding toggle button right of gap input
- Fix alignment grid axes for vertical layout (transpose primary/counter)
- Fix grid switch: set FIXED sizing, compute frame size from children
- Remove hardcoded white fill from Shift+A wrap
- Auto-detect horizontal vs vertical from selection bounds
* Add flex-to-grid switch integration test
Verify HUG frame expands and children are placed in 2x2 grid
when switching from vertical flex to grid layout.
* Update changelog for grid layout
The render tool needs esbuild to transform JSX → createElement calls, but
esbuild only runs in Node, not in the browser where the automation server
executes commands. The bridge now:
1. Intercepts render tool calls with JSX
2. Uses Vite's ssrLoadModule to import @open-pencil/core (handles TS source)
3. Transforms JSX → tree on the Node side via buildComponent + resolveToTree
4. Sends the tree to the browser which calls renderTreeNode directly
Also fixes @open-pencil/core exports: remove "node" condition that pointed
to non-existent dist/ during development. Published consumers get dist/ via
publishConfig.exports override.
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.