Bugs fixed:
- Export response now wrapped in {ok, result} like all other commands
- btoa(String.fromCharCode(...data)) replaced with loop to avoid
RangeError on large images
- JSX export uses dedicated export_jsx RPC command instead of eval
with undefined selectionToJSX reference
Issues fixed:
- connectAutomation cleanup on component unmount via onUnmounted
- Bridge state encapsulated in startAutomationBridge closure instead
of module-level lets
- Previous browser WS closed on new connection to prevent leaks
- walkNodes returns boolean for early termination, find stops at limit
- Unused imports removed from node.ts (fmtList, nodeToData)
- Hand-rolled hex in node.ts replaced with colorToHex from core
- makeFigmaFromStore extracted to shared figma-factory.ts, used by
both ai/tools.ts and automation/server.ts
- Unused limit param removed from AnalyzeColorsArgs
- Unused groupBy param removed from AnalyzeTypographyArgs
- Bridge was double-wrapping responses ({ok, result: {ok, base64}}) — now
forwards the server's payload directly, stripping only type/id fields
- Compute layouts after mutations so auto-layout frames position correctly
- Export createElement and resolveToTree from core for bridge JSX transform
- Fix package.json exports: remove node condition that pointed to non-existent
dist/, use publishConfig.exports for npm consumers
renderTreeNode and mutating tools create auto-layout frames but didn't
trigger Yoga layout computation, leaving children stacked at (0,0).
Call computeAllLayouts after mutations so frames size and position correctly.
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.
The render tool needs esbuild to transform JSX, but esbuild only runs in
Node — not in the browser where the automation server executes. The bridge
now intercepts render tool calls, converts JSX to a tree on the Node side
using buildComponent + resolveToTree from core, and sends the tree to the
browser which calls renderTreeNode directly.
Also exports createElement and resolveToTree from core for bridge use.
- config.ts: og:site_name, og:image (+w/h/alt), twitter:card/site/image
- config.ts: transformPageData — per-page canonical, og:url, og:locale,
og:locale:alternate, hreflang for all 6 locales + x-default,
og:title/description and twitter:title/description from frontmatter
- config.ts: sitemap with xhtml:link alternates for all 174 pages × 6 locales
- SchemaOrg.vue: SoftwareApplication JSON-LD on EN homepage only
- HomeLayout.vue: render SchemaOrg in home-features-after slot
- index.md + de/fr/es/it/pl index.md: add translated title to frontmatter
bridge.ts is bundled by esbuild as a Vite plugin — it can't resolve
workspace package entries (@open-pencil/core) at config time because
dist/ is not built yet. Use relative import to the source file instead.
- Use colorToHex/colorDistance from color.ts instead of hand-rolled hex math
in rpc/commands.ts (removes toHex, hexToRgb, colorDistance duplicates)
- Extract AUTOMATION_HTTP_PORT/AUTOMATION_WS_PORT to core constants, import
in bridge.ts, server.ts, app-client.ts instead of duplicating magic numbers
- Add requireFile() helper in app-client.ts, replace all file! non-null
assertions across 12 CLI command files
- Document same-machine trust security model in bridge.ts header comment
- Replace (globalThis as any).Bun with typed isBunRuntime() helper
- Add changelog entry for CLI-to-app RPC bridge feature
Bridge now uses Hono for the HTTP server and ws for WebSocket —
works on both Bun and Node.js. Uses @hono/node-server as fallback
when Bun runtime is not available.
- layers-panel: navigate to /demo so demo layers exist
- app-menu zoom-to-fit: zoom in first so zoom-to-fit has something to change
- playwright.config: force workers=1 and colorScheme=dark for consistent
WebGL/SwiftShader snapshot rendering across runs
- Update empty-canvas snapshot for current dark-mode canvas appearance
- 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
- Extract shared RPC command handlers into packages/core/src/rpc/
(info, pages, tree, find, node, variables, analyze colors/typography/spacing/clusters)
- Add automation bridge (src/automation/bridge.ts) that runs in Vite/Bun process,
listens on localhost:7600 (HTTP) and :7601 (WebSocket)
- Add browser-side handler (src/automation/server.ts) that connects via WebSocket,
executes commands against live EditorStore with requestRender()/flashNodes()
- Supports eval, tool execution, and image export via RPC
- Refactor all CLI commands: file arg is now optional — omit it to connect
to the running app instead of loading a .fig file
- Bearer token auth generated per session, exposed via /health endpoint
- Vite plugin starts the bridge automatically during dev
- 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.