Commit graph

51 commits

Author SHA1 Message Date
Danila Poyarkov f3dbf2c271 Release v0.11.7 2026-04-22 20:31:33 +03:00
Danila Poyarkov 33fc19a593 fix(layout): bump yoga-layout to 3.3.0-grid.3 with Node.free() support
- 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
2026-04-13 12:09:45 +03:00
Danila Poyarkov a130d9ce1d fix(core): correct version to 0.11.6 in package.json 2026-04-08 22:28:00 +03:00
Danila Poyarkov f8f4af5937 Release v0.11.6 2026-04-08 22:04:22 +03:00
Danila Poyarkov 2c3191cf96 Release v0.11.5 2026-04-08 21:38:44 +03:00
Danila Poyarkov 26a7d2d454 Release v0.11.4 2026-04-08 20:52:09 +03:00
Danila Poyarkov 92efd70c62 refactor(core): move clipboard text helpers into text/ domain
- 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
2026-04-08 20:45:43 +03:00
Danila Poyarkov a20d170956 Release v0.11.3 2026-04-08 16:06:37 +03:00
Danila Poyarkov 07674d1919 Restructure @open-pencil/core into domain modules
Move 30+ loose files at src/ root into domain directories:
- scene-graph/ — SceneGraph class, instances, hit-test, copy, snap, undo
- color/ — parse/format, color management, OkHCL
- text/ — text editor, style runs, direction, fonts
- vector/ — vector network encode/decode, bezier math
- figma-api/ — FigmaAPI class, FigmaNodeProxy
- icons/ — Iconify API client, icon rendering
- canvas/ — SkiaRenderer (was renderer/)
- design-jsx/ — JSX-to-design renderer (was render/)

Also:
- Move fig-compress.ts into io/formats/fig/compress.ts
- Delete re-export shims (headless-render.ts, svg-export/)
- Convert all self-referencing @open-pencil/core/* imports to relative
- Clean up package.json exports (26 subpaths, no internal leaks)
- Update AGENTS.md subpath table

Fixes #179
2026-04-06 14:39:53 +03:00
Danila Poyarkov d73caafc8a Add 14 missing subpath exports to @open-pencil/core
Fixes #179
2026-04-06 14:24:35 +03:00
Danila Poyarkov 33d293b221 Release v0.11.2 2026-03-30 18:38:36 +03:00
Danila Poyarkov 9be3514f2b Release v0.11.1 2026-03-30 17:24:00 +03:00
Danila Poyarkov 911bed3ede Release v0.11.0 2026-03-30 16:48:27 +03:00
Danila Poyarkov e0878c1e68 Rename format files to match io folders 2026-03-28 14:33:28 +03:00
Danila Poyarkov decee0216e Move format-specific code under io/formats 2026-03-28 14:30:02 +03:00
Danila Poyarkov 0001802dda Add shared IO format registry 2026-03-28 01:58:05 +03:00
Danila Poyarkov 99d2703583 Extract createEditor() to packages/core/src/editor/ folder
Split the 2505-line god store into 13 domain modules sharing an
EditorContext interface:

  editor/types.ts       — EditorState, EditorOptions, Tool, EditorToolDef
  editor/create.ts      — createEditor() assembler
  editor/viewport.ts    — screenToCanvas, applyZoom, pan, zoom*
  editor/selection.ts   — select, clearSelection, marquee, snap, hover
  editor/pages.ts       — switchPage, addPage, deletePage, renamePage
  editor/shapes.ts      — createShape, pen tool, adoptNodesIntoSection
  editor/structure.ts   — group, ungroup, reorder, reparent, z-order
  editor/components.ts  — component/instance/detach/componentSet
  editor/clipboard.ts   — duplicate, copy, paste, delete
  editor/undo.ts        — commitMove/Resize/Rotation, snapshot
  editor/text.ts        — startTextEditing, commitTextEdit
  editor/nodes.ts       — updateNode, setLayoutMode

Move design constants (fills, strokes, zoom) from app to core.
Largest file is structure.ts at 394 lines (was 2505).
App store is now a thin Vue wrapper (747 lines) delegating to
the core Editor — ready for @open-pencil/vue SDK consumption.

All 955 tests pass, 0 lint errors, 1.41% duplication.
2026-03-16 15:14:50 +03:00
Danila Poyarkov 15f80d6d62 Refactor font loading: DRY up local font lookup, fix headless fallback
- 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
2026-03-16 13:38:16 +03:00
Danila Poyarkov f4c68f7f41 Release v0.10.0 2026-03-15 19:27:37 +03:00
Anton A S d688d04bef Address PR #94 review: dedup, replace hand-rolled code, cleanup
- 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
2026-03-13 14:46:18 +03:00
Anton A S 733321fbb5 Add step budget warnings and Continue button for AI chat
- 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
2026-03-13 10:18:20 +03:00
Anton A S 2a740a01ff Add subpath exports to @open-pencil/core
12 domain-specific subpath exports for targeted imports:
scene-graph, kiwi, tools, renderer, render, rpc, figma-api,
canvaskit, layout, color, render-image, profiler.

- Create kiwi/index.ts barrel (codec, fig-file, fig-import, protocol)
- Route main index.ts kiwi re-exports through the new barrel
- Add exports map + publishConfig mirror for npm consumers
- Mark package as sideEffects: false for tree-shaking
- Document subpath exports in AGENTS.md
2026-03-12 22:10:36 +03:00
Danila Poyarkov fe755fbb85 Release v0.9.0 2026-03-09 17:43:11 +03:00
Danila Poyarkov abe108c473
Add event emitter to SceneGraph, refactor collab sync
- 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
2026-03-09 16:02:54 +03:00
Danila Poyarkov f2ef9c976d
XPath query for node selection (#82)
* Add XPath query for node selection

- packages/core/src/xpath.ts: XPath engine using fontoxpath with custom
  DOM facade over SceneGraph nodes
- RPC command: 'query' with selector, page, limit params
- CLI command: 'open-pencil query <file> <xpath>' with --json, --page, --limit
- AI/MCP tool: 'query_nodes' with XPath examples in description
- Queryable attributes: name, width, height, x, y, visible, opacity,
  cornerRadius, fontSize, fontFamily, fontWeight, layoutMode, itemSpacing,
  padding*, strokeWeight, rotation, locked, blendMode, text, lineHeight,
  letterSpacing

* Add tests and docs for XPath query

- 9 unit tests for queryByXPath/matchByXPath core functions
- 6 tool tests for query_nodes AI/MCP tool
- README: query examples in CLI section
- VitePress docs: patterns, queryable attributes, example output
- CHANGELOG: feature entry

* Lazy-load fontoxpath to avoid bundling in desktop app

- Dynamic import() instead of static import for fontoxpath
- queryByXPath/matchByXPath now async (they were sync before)
- RpcCommand.execute allows R | Promise<R> for async commands
- Fix 2 lint errors (unnecessary optional chain, always-truthy)
- Update all tests and CLI to await results
2026-03-09 15:32:27 +03:00
Danila Poyarkov c51f34689f
Multi-provider AI, sucrase JSX, Figma zoom shortcuts (#72)
* 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.
2026-03-08 22:05:18 +03:00
Danila Poyarkov 45df90c5c7
CSS Grid layout mode (#73)
* 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
2026-03-08 18:23:53 +03:00
Danila Poyarkov 9882375b4f Release v0.8.0 2026-03-08 00:20:47 +03:00
Danila Poyarkov 70c25cddf5 Fix render tool in automation: use ssrLoadModule for JSX transform
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.
2026-03-07 20:16:52 +03:00
Danila Poyarkov 661b83e749 Port analyze/diff tools from figma-use, split tools into domain files
Analyze: colors, typography, spacing, clusters
Diff: diff_create (tree diff), diff_show (preview changes)
Utility: get_components, get_current_page, arrange, node_to_component

Split schema.ts (2600 lines) into read, create, modify, structure,
variables, vector, analyze, registry — each under 600 lines.

Clean up inline types (use Color, Vector, SceneNode from existing defs).
Update AGENTS.md and CONTRIBUTING.md with code quality guidelines.
2026-03-05 19:16:23 +03:00
Danila Poyarkov be44b4d404 Release v0.7.0 2026-03-05 10:32:35 +03:00
Danila Poyarkov aa0a1b5def Fix workspace resolution: add bun export condition to core package
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.
2026-03-04 14:51:44 +03:00
Danila Poyarkov 7ad4116860 Release v0.6.0 2026-03-04 01:47:37 +03:00
Danila Poyarkov 9c9bfc38b2 Release v0.5.1 2026-03-03 17:30:09 +03:00
Danila Poyarkov 5eac653f4c Release v0.5.0 2026-03-03 14:51:56 +03:00
Danila Poyarkov 7d98201548 Release v0.4.2 2026-03-02 18:14:35 +03:00
Danila Poyarkov 7eb74c3969 Release v0.4.1 2026-03-02 15:51:34 +03:00
Danila Poyarkov cc77c68143 Fix core exports: node → dist, default → src 2026-03-02 15:03:40 +03:00
Danila Poyarkov 5856233832 Fix core exports: types → src for workspace typecheck 2026-03-02 15:02:43 +03:00
Danila Poyarkov 8fea6c086d Bump all packages to 0.4.0, fix Node.js ESM compatibility
- tsgo + fix-esm-import-path for .js extensions in dist/
- Core exports: bun condition → src, import → dist
- MCP depends on core ^0.4.0
2026-03-02 14:59:59 +03:00
Danila Poyarkov 518ddb0c5b Release v0.3.2 2026-03-02 11:36:18 +03:00
Danila Poyarkov 1b414bf680 Release v0.3.1 2026-03-02 11:14:15 +03:00
Danila Poyarkov d5ca92e01f Add MIT license 2026-03-02 08:31:54 +03:00
Danila Poyarkov f495f4072a Release v0.3.0 2026-03-01 22:46:06 +03:00
Danila Poyarkov 5e209eca1c Fix trackpad pinch-to-zoom on Safari macOS 2026-03-01 22:07:47 +03:00
Danila Poyarkov 13a95bf517 Release v0.2.1 2026-03-01 21:25:52 +03:00
Danila Poyarkov 6e4332a6ba Release v0.2.0 2026-03-01 18:27:12 +03:00
Danila Poyarkov 1e873d64b1 Add repository field and provenance to package configs 2026-03-01 16:23:30 +03:00
Danila Poyarkov eb6f8914a6 Point core exports to src/index.ts for Vite compatibility 2026-03-01 01:27:04 +03:00
Danila Poyarkov 22e9e7af87 Prepare packages for npm publishing
- Core: conditional exports (bun→src, import→dist), prepublishOnly builds
- CLI: publishConfig, files field
- bun publish resolves workspace:* → actual versions
2026-03-01 01:24:54 +03:00