Commit graph

319 commits

Author SHA1 Message Date
Anton A S 83c72b94a0 Add E2E test coverage for canvas, toolbar, panels, properties, auto-layout, variables, export 2026-03-07 22:11:25 +03:00
Danila Poyarkov 2b525b816b Fix eval response unwrapping and export_jsx page selection
- eval CLI was typing rpc result as {ok, result} but the bridge
  already unwraps to the inner value — use rpc<unknown> directly
- export_jsx defaults to current page children, not first page
- Fix import ordering in server.ts (doc comment above imports)
2026-03-07 20:51:53 +03:00
Danila Poyarkov b6c922e335 Fix all code review issues in RPC bridge
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
2026-03-07 20:42:37 +03:00
Danila Poyarkov c9e755a181 Fix RPC bridge response unwrapping and export
- 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
2026-03-07 20:27:57 +03:00
Danila Poyarkov 1f04926166 Compute layouts after mutating automation commands
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.
2026-03-07 20:18:59 +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 39af21ff06 Pre-process render JSX in bridge where esbuild is available
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.
2026-03-07 20:10:48 +03:00
Danila Poyarkov 2a9d47a761 Fix Vite build: import constants via relative path for esbuild compat
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.
2026-03-07 19:49:09 +03:00
Danila Poyarkov 2ec382fc9d Fix RPC bridge review issues: centralize colors, ports, remove non-null assertions
- 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
2026-03-07 15:11:31 +03:00
Danila Poyarkov 99365384df Merge remote-tracking branch 'origin/master' into fix-cli-rpc
# Conflicts:
#	packages/core/src/index.ts
2026-03-07 15:03:40 +03:00
Danila Poyarkov 97baef474d Merge branch 'reka-vueuse-refactor' 2026-03-07 14:46:40 +03:00
Danila Poyarkov 7cdb1d1eaa Replace Bun.serve with Hono + ws for Node.js compatibility
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.
2026-03-07 14:30:12 +03:00
Danila Poyarkov 3684caea87 Fix duplicate :class, rename race, useFileDialog leak, restore clip/wrap 2026-03-07 14:27:17 +03:00
Danila Poyarkov bd5cdc8eee Centralize all color utilities in color.ts
- 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
2026-03-07 10:02:11 +03:00
Danila Poyarkov 9784544f1f Clean up tools/schema, add geometry.ts, isMixed helper, fix imports, add 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
2026-03-06 22:33:10 +03:00
Danila Poyarkov e7e5f53b57 Add CLI-to-app RPC bridge for live document access
- 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
2026-03-06 22:29:49 +03:00
Danila Poyarkov 31326e8e5e Eliminate as Record<string, unknown> casts
- 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).
2026-03-06 21:43:32 +03:00
Danila Poyarkov 07e9182b4c Move collab routing logic into CollabPanel
CollabPanel now uses injected collab + useRouter() directly.
No more emit relay through EditorView.
2026-03-06 21:43:10 +03:00
Danila Poyarkov b4698d7a63 Use colorToCSS() instead of hand-rolled rgba conversions
Fix colorToCSS to include alpha channel. Replace all manual
Math.round(c.r * 255) patterns with colorToCSS() from core.
2026-03-06 21:43:03 +03:00
Danila Poyarkov 9d73297040 Delete re-export shims, import from @open-pencil/core directly
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.
2026-03-06 21:42:56 +03:00
Danila Poyarkov 6ec8fc518b Move demo to /demo route, add @unhead/vue for title management
- / 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
2026-03-06 20:54:48 +03:00
Danila Poyarkov 30cbba22aa Extract shared surface, button, and select styles 2026-03-06 19:36:36 +03:00
Danila Poyarkov bdb95a9d31 Refine breakpoint and rename patterns 2026-03-06 19:32:14 +03:00
Danila Poyarkov 35dc5bd2a5 Extract menu styling with tailwind-variants 2026-03-06 19:01:14 +03:00
Danila Poyarkov a334a7f63c Refactor UI to use Reka & VueUse primitives 2026-03-06 18:28:40 +03:00
Danila Poyarkov 5ae6234e40
Tailwind CSS v4 JSX export (#54)
* Add Tailwind CSS v4 JSX export format

- Add JSXFormat type ('openpencil' | 'tailwind') to sceneNodeToJSX/selectionToJSX
- Tailwind resolver (tailwind.ts): px→spacing (v4 multiplier), hex→color class,
  fontSize/fontWeight/borderRadius named lookups with arbitrary value fallback
- Export: FRAME→div, TEXT→p, SECTION→section, className with TW utility classes
- CLI: export --format jsx --style tailwind
- CodePanel: format toggle button (OpenPencil / Tailwind)
- Rename Jsx→JSX in all public APIs (sceneNodeToJSX, selectionToJSX, renderJSX, etc.)
- 29 new Tailwind export tests, all 43 export tests pass

* Extract escapeJSXText — replace nested ternary with entity map lookup

* Deduplicate export-jsx: extract shared padding, corner radius, and node context helpers

* Update changelog

* Tighten Tailwind JSX export fidelity
2026-03-06 18:04:11 +03:00
Danila Poyarkov bc52095138 Add double-click to rename layers, extract useInlineRename composable
- 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
2026-03-06 09:50:35 +03:00
Danila Poyarkov 6f31af44e9 Fix drawer animation: single spring transition, no jump on close
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.
2026-03-06 09:22:03 +03:00
Danila Poyarkov a2d7b89709 Adopt motion-v for mobile drawer and toolbar animations
- MobileDrawer: spring-animated height via motion.div :animate + @pan,
  replaces useSwipe + manual rAF. Tab state always set (never null),
  content stays rendered when drawer is closed.
- Toolbar: AnimatePresence with directional slide variants and
  layout-animated width via motion.div layout, replaces manual
  scrollWidth measuring + inline CSS transitions.
- Add SWIPE_VELOCITY_THRESHOLD constant.
- activeRibbonTab type narrowed from union | null to union.
2026-03-06 08:50:38 +03:00
Danila Poyarkov 724e632251 Fix mobile drawer jump on swipe down
Reset dragOffset after snap change and a rAF so the height
transition starts before the translate snaps back to 0.
2026-03-06 00:14:28 +03:00
Danila Poyarkov 49b891d3f9 Refactor mobile drawer: use VueUse composables, unify panels
- Replace hand-rolled touch handlers with useSwipe
- Replace manual offsetHeight measurement with useElementSize
- Replace window.innerHeight with useWindowSize
- Merge MobileRibbon into MobileDrawer (unified bottom sheet)
- Match desktop layout: PagesPanel + Layers in same panel
- Add viewport-fit=cover for safe area insets
- Glassy HUD buttons with backdrop-blur
- Fix toolbar overflow on narrow viewports
- Remove MobileRibbon.vue, SWIPE_MAX_DURATION constant
2026-03-05 22:13:24 +03:00
Anton Soldatov 168c25c189
feat: mobile layout, PWA support (#27) (#27)
- Mobile-responsive editor with bottom drawer, ribbon nav, HUD overlay
- Touch support: single-finger tools, two-finger pinch-zoom
- PWA: manifest, service worker, installability
- Extract LayerTree from LayersPanel, shared utils (colorToCSS, initials, toolIcons)
- Constants moved to src/constants.ts, clipboard state to editor store
- reka-ui Popover/DropdownMenu in MobileHud

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-05 19:51:51 +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 ed61445e2d
SVG export (#52)
* 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
2026-03-05 10:31:02 +03:00
Danila Poyarkov ad96b1c654 Add CJK font fallback for text rendering
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
2026-03-05 09:48:11 +03:00
Danila Poyarkov 29096fa56b Merge remote-tracking branch 'origin/master' into reka-color-picker
# Conflicts:
#	src/components/HsvColorArea.vue
2026-03-05 09:05:06 +03:00
Danila Poyarkov d060e32784 Merge origin/master (PR #41 data-test-ids) 2026-03-05 09:03:26 +03:00
Danila Poyarkov 38e7fdd4ac Fix hit-testing: scope hover to current page, click-through empty containers
- 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)
2026-03-05 08:56:35 +03:00
Danila Poyarkov b3e1bbdb09 Replace custom color picker with reka-ui Color components
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.
2026-03-04 20:52:30 +03:00
Anton Soldatov 3ac016b806
Add data-test-id attributes to all Vue components (#41) 2026-03-04 17:18:26 +03:00
Danila Poyarkov 74b09d4397 Merge branch 'renderer-profiler' into compatibility-fixes 2026-03-04 14:19:35 +03:00
Danila Poyarkov 713a0dc817 Fix instance overrides and layout scoping on .fig import
- 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
2026-03-04 14:18:37 +03:00
Danila Poyarkov 2a86ea5ce5 Add renderer profiler with HUD overlay, GPU timing, and phase instrumentation
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
2026-03-04 14:13:17 +03:00
Danila Poyarkov 942d3be059 Add auto-save toggle to File menu
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.
2026-03-04 10:43:20 +03:00
Danila Poyarkov 3c0c4e530c Guard against double destroy and post-unmount rendering
- 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)
2026-03-04 10:35:44 +03:00
Danila Poyarkov 9e3a6c0e7f Extract collectFontKeys into core and add font tests
- 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)
2026-03-04 10:16:26 +03:00
Danila Poyarkov ed85451579 Await font loading before hiding file-open loader
- 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
2026-03-04 10:11:12 +03:00
Danila Poyarkov a92315149a Always show visibility toggle on fill, stroke, and effect rows 2026-03-04 09:56:16 +03:00
Danila Poyarkov 1ca96094ab Fix stroke color row overlap
- 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
2026-03-04 09:54:09 +03:00
Danila Poyarkov e7426f3c58 Fix stroke section layout to match Figma
- 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
2026-03-04 09:51:39 +03:00