Commit graph

441 commits

Author SHA1 Message Date
Danila Poyarkov 492d9dfa75 Refactor LayerTree: Atlaskit DnD, split LayerRow, shared icons
Replace manual pointer-capture drag with @atlaskit/pragmatic-drag-and-drop:
- Tree-aware hitbox (reorder-above, reorder-below, make-child)
- Native drag preview via Atlaskit adapter
- No more manual DOM measurement for drop zones

Split into focused files:
- LayerTree.vue (164 lines) — tree data, expand, selection
- LayerRow.vue (175 lines) — per-row: icon, rename, lock/visibility, DnD setup
- utils/layer-icons.ts (43 lines) — icon maps shared across components
- use-layer-drag.ts (127 lines) — Atlaskit DnD composable in SDK

Add toggleNodeVisibility/toggleNodeLock per-node methods to editor.
2026-03-17 08:46:23 +03:00
Danila Poyarkov f084212ce8 Inline context menu into EditorCanvas, delete CanvasContextMenu.vue
The wrapper component existed only to do hit-test + select on right-click,
which is now editor.selectAtPoint(). The 5-line Reka UI ContextMenu
composition belongs directly in EditorCanvas where the canvas ref lives.
2026-03-17 08:31:12 +03:00
Danila Poyarkov b2ed748608 Move hit-test logic out of CanvasContextMenu into editor
Add hitTestAtPoint() and selectAtPoint() to core editor/selection.ts.
CanvasContextMenu is now a pure Reka UI wrapper — receives onContextMenu
callback from EditorCanvas which owns the canvas ref and coordinates.
2026-03-17 08:28:39 +03:00
Danila Poyarkov 44f6f55b70 Split FillPicker into focused components
FillPicker.vue (504 → 121 lines): dumb tab switcher, no gradient
internals. Delegates entirely to sub-components.

New GradientEditor.vue (227 lines): owns gradient stops, stop bar
with pointer drag, subtype select, and HsvColorArea for active stop.
Uses AppSelect instead of hand-rolled Reka UI Select.

New ImageFillPicker.vue (79 lines): image preview, file picker,
scale mode select via AppSelect.

Each component owns its full concern — no data leaking between them.
Replaced inline SVGs with Lucide icons, use twMerge for tab classes.
2026-03-17 08:22:54 +03:00
Danila Poyarkov 51bbf67f1f Remove re-export shims, import directly from @open-pencil/vue
Delete 9 shim files from src/composables/ that just re-exported
SDK composables. All 18 consumers now import from '@open-pencil/vue'.

Merge dual useNodeProps()/useMultiProps() calls into single
useNodeProps() in 5 property section components.

src/composables/ now has only 4 app-specific files:
use-chat, use-collab, use-keyboard, use-menu.
2026-03-17 08:10:05 +03:00
Danila Poyarkov 6d19a13526 Add computeBounds/requireNode helpers, scope CodePanel style, dedupe snap/zip
- geometry.ts: add computeBounds() utility for bounding box accumulation
- tools/schema.ts: add requireNode() helper + NodeNotFoundError
- CodePanel.vue: scope <style> to prevent global CSS leak
- snap.ts: replace 44-line getEdges() with rotatedBBox() from geometry
- fig-compress.ts: extract shared zip assembly from fig-export + worker
- Remove unused computeBounds import from vector.ts
2026-03-16 23:01:51 +03:00
Danila Poyarkov 30f8a3fda6 Wire editor actions, move kiwi-serialize, clean dead code
- VariablesDialog: replace 7 manual undo.push + graph mutation + requestRender
  patterns with editor.renameCollection/addVariable/removeVariable etc.
- PositionSection: replace inline alignment/flip/rotate geometry with
  editor.alignNodes/flipNodes/rotateNodes
- Move kiwi-serialize.ts into kiwi/ where it belongs
- Remove dead createPropertyChange export from undo.ts
- Un-export internal queryFonts/FontInfo from fonts.ts
- Deduplicate font weight maps: single FONT_WEIGHT_NAMES in fonts.ts,
  figma-api-proxy.ts imports from there
2026-03-16 22:30:56 +03:00
Danila Poyarkov 4b49d69601 Fix all 135 vue-tsc errors, promote to check gate
vue-tsc now runs as part of 'bun run check' — zero errors.

Fixes by category:
- TS18047 (73): null guards in LayoutSection, TypographySection
- TS18048 (17): collab injection guards in CollabPanel, MobileHud
- TS2339 (12): Uint8Array.fromBase64/toBase64 declarations (ES2024)
- TS2741 (10): AppMenu discriminated union for separators
- TS6133 (7): remove unused declarations
- TS2322 (6): FontPicker AcceptableValue, Toolbar motion-v variants
- TS2345 (3): FillSection updateArrayItem signature, ScrubInput guard
- TS2314 (2): ChatMessage UIMessagePart generic args
- Misc (5): PropertiesPanel dead comparison, VariablesDialog onSubmit,
  NodeContextMenuContent document access, LayerTree arg count
2026-03-16 22:21:21 +03:00
Danila Poyarkov 4a2c5ac0ce Fix type errors found by tsgo and vue-tsc
- Add interaction constants to core barrel export (were in constants.ts
  but not reachable via '@open-pencil/core')
- Fix variable shadowing in use-text-edit.ts: rename inner 'editor'
  (textEditor) to 'textEd' to avoid shadowing the Editor parameter
- Fix unused params: prefix with _ in scene.ts, describe-issues.ts, xpath.ts
- Remove unused getCoords param from setupPanZoom
- Move cursorCanvasX/Y assignment to app callback (not on EditorState)
- Simplify EditorStore return: spread editor instead of listing 80+ methods
  individually — new Editor methods auto-forward, no more manual sync
- Add tsgo to check script, add check:vue script for vue-tsc
- Add vue-tsc as dev dependency
- Reduces vue-tsc errors from 172 (pre-existing) to 135 (37 fixed)
2026-03-16 21:54:08 +03:00
Danila Poyarkov f44f06f721 Refactor: extract business logic from components, clean composables
Tier 1 — Core API fixes:
- updateNodeWithUndo now calls requestRender internally, eliminating
  15+ redundant requestRender() calls across 6 property panel components
- New editor/variables.ts: 7 undo-able variable CRUD operations
  (renameCollection, addCollection, removeCollection, addVariable,
  removeVariable, renameVariable, updateVariableValue)
- New editor/alignment.ts: alignNodes, flipNodes, rotateNodes —
  moves bounding box geometry out of PositionSection.vue

Tier 2 — Composable cleanup:
- Merged use-node-props + use-multi-props into single useNodeProps()
  with MIXED sentinel, eliminating duplicate computed refs
- Added toolCursor() utility — replaces if/else chain in EditorCanvas
- Renamed use-toast.ts → toast.ts (singleton module, not a composable)

Tier 3 — use-canvas-input.ts split (1497 → 817 lines):
- input/types.ts (88) — DragState variants, HandlePosition, TOOL_TO_NODE
- input/geometry.ts (170) — hit testing, handle positions, rotation cursor
- input/pan-zoom.ts (266) — wheel, touch pinch, Safari gestures
- input/resize.ts (121) — constrainToAspectRatio, applyResize
- input/auto-layout.ts (103) — insert indicator computation

All 955 tests pass, 0 lint errors, 1.48% duplication.
2026-03-16 20:35:17 +03:00
Danila Poyarkov 608be0ea27 Move composables to @open-pencil/vue SDK, unify Editor type
Phase 2 of SDK refactor:

Composables moved to packages/vue/src/composables/:
- use-canvas-input (1497 lines) — mouse/touch/gesture interaction
- use-text-edit (340 lines) — inline text editing
- use-canvas-drop (68 lines) — image drag-and-drop
- use-node-props (55 lines) — single/multi node property editing
- use-multi-props (106 lines) — multi-selection property merging
- use-inline-rename (58 lines) — inline rename input
- use-font-status (24 lines) — font loading status
- use-toast (39 lines) — toast notifications

All retyped from EditorStore to Editor (core type). App composables
are now 1-2 line re-export shims from @open-pencil/vue.

Also moved to core Editor:
- placeImageFiles / placeImageNode / decodeImageDimensions
- Interaction constants (PEN_CLOSE_THRESHOLD, ROTATION_SNAP_DEGREES, etc.)

Removed dead exports: HANDLE_SIZE, DRAG_DEAD_ZONE, ACP_DESIGN_CONTEXT.
2026-03-16 17:41:07 +03:00
Danila Poyarkov 8cdb4db7ee Add @open-pencil/vue headless SDK package
Renderless components (Reka UI-style) exposing editor logic via slots:
- OpenPencilProvider — injects editor via Vue provide/inject
- OpenPencilCanvas — CanvasKit/Skia surface with render loop
- PageList, LayerTree, ToolSelector, NodeProperties — slot-based

Composables:
- useEditor() — access editor from any descendant
- useCanvas() — low-level surface lifecycle (moved from app)

The app's use-canvas.ts is now a 2-line re-export from the SDK.
Includes example app in packages/vue/example/.
2026-03-16 17:24:45 +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 f14f0a4259 Fix review issues: undoable lock/visibility toggles, use degToRad 2026-03-16 14:29:26 +03:00
Anton A S edd9a8e398 Entered container border, rotation support, INSTANCE/COMPONENT enterable, tests 2026-03-16 14:26:52 +03:00
Anton A S 6d3e2d20cd Scope checklist: marquee in scope, layer panel sync, delete locked guard, undo/redo validation, tests 2026-03-16 14:26:52 +03:00
Anton A S 1b743bec94 Add selection scope: enter containers with dblclick, exit with Escape or click-outside 2026-03-16 14:26:52 +03:00
Anton A S 19d6e01960 Fix double-click enter: offset coords to group-local space 2026-03-16 14:26:52 +03:00
Anton A S 01235c0d84 Fix group selection: single click selects group, double click enters, locked groups block enter 2026-03-16 14:26:52 +03:00
Anton A S 116acb5cfa Make lock/visibility icons more visible 2026-03-16 14:26:52 +03:00
Anton A S 6596702389 Fix lock: block move/resize/rotate for locked nodes, skip locked in marquee, improve toggle icon visibility 2026-03-16 14:26:52 +03:00
Anton A S bc85d73cc8 Add lock and visibility toggles to layers panel 2026-03-16 14:26:52 +03:00
Danila Poyarkov 8d7dd0a36b Add random.ts helpers, replace all raw crypto.getRandomValues calls
New module packages/core/src/random.ts with randomHex, randomInt,
randomIndex — centralizes all crypto-based randomness.

Replaces:
- Date.now() IDs in VariablesDialog (not unique, not random)
- Hand-rolled hex token in automation/server.ts
- Inline crypto.getRandomValues in clipboard.ts, use-collab.ts

Also adds --color-component Tailwind theme color (#9747ff) and
replaces all 6 hardcoded occurrences across DesignPanel, LayerTree,
NodeContextMenuContent, and menu.ts.
2026-03-16 14:13:36 +03:00
Danila Poyarkov 921d2d35c5 Export IS_BROWSER from core, replace all raw typeof window checks
- Add IS_BROWSER constant to core/constants.ts, rewrite IS_TAURI to use it
- Replace 9 raw 'typeof window' checks across canvaskit, fig-export,
  figma-api, fig-file, fonts, SafariBanner, use-chat
- Promote no-typeof-window-check lint rule from warn to error
- Extract array mutation helpers (updateArrayItem, removeArrayItem,
  toggleArrayVisibility) into useMultiProps — DRY up FillSection and
  StrokeSection (removes 20+ duplicated for-loops)
2026-03-16 14:05:24 +03:00
Danila Poyarkov c06ed32990 Harden linting: add eqeqeq, prefer-const, radix, no-console, no-typeof-window-check
New built-in rules:
- eqeqeq (null-safe) — prevent loose equality coercions
- prefer-const — catch unused let declarations
- radix — require parseInt radix parameter
- no-console — warn on console.log in app code (CLI/MCP exempted)

New custom rule:
- no-typeof-window-check — flag raw typeof window checks, prefer
  IS_BROWSER/IS_TAURI constants (warn-level, constants.ts + fonts.ts
  exempted)

Fix 4 violations caught by new rules.
2026-03-16 13:54:07 +03:00
Danila Poyarkov b3d3c0a00b Use collab injection in MobileHud instead of prop drilling
MobileHud now uses useCollabInjected() directly — same pattern as
CollabPanel — and handles share/join/disconnect internally.

Removes broken event handlers that were never defined in EditorView.
2026-03-16 13:27:17 +03:00
Danila Poyarkov 6a40e4e3e3 Disable automation WebSocket in production web app
Only connect to the local automation bridge in Tauri desktop or
local dev, matching the existing spawnMCPIfNeeded guard.

Fixes #126
2026-03-16 13:27:17 +03:00
Danila Poyarkov 12d3b60e69 Clean up ACP branch: gate debug log, fix capitalizations, harden types
- Gate acpDebugLog behind import.meta.env.DEV (no logging in production)
- Remove saveAcpDebugLog (Desktop hardcode) — clipboard copy is sufficient
- Hide ACP log button unless in DEV mode with entries
- Fix textFromContent: proper type guards instead of unsafe casts
- Fix trailing empty line in server.ts handleBrowserMessage
- Rename Acp → ACP, Mcp → MCP in all identifiers:
  AcpPermissionDialog → ACPPermissionDialog (file + component)
  AcpDebugEntry → ACPDebugEntry
  spawnMcpIfNeeded → spawnMCPIfNeeded
  checkMcpHealth → checkMCPHealth
  McpContent/McpResult/McpTransport → MCPContent/MCPResult/MCPTransport
  createMcpSession → createMCPSession
2026-03-15 18:24:37 +03:00
Anton A S 63f3ef3755 Show agent name in model selector for ACP, fix model dropdown, update docs 2026-03-15 18:13:29 +03:00
Anton A S 55ba13b43b Use full system prompt for ACP agents instead of short context 2026-03-15 18:06:15 +03:00
Anton A S 6fcb29686d Save ACP debug log to Desktop, keep last 5 minutes 2026-03-15 17:47:32 +03:00
Anton A S 83f7c53fee Add ACP debug log with copy button in chat toolbar 2026-03-15 17:45:14 +03:00
Anton A S 206cb3742f Debug: log ACP session updates 2026-03-15 17:42:50 +03:00
Anton A S aaae074223 Fix ACP settings: show stock keys for all providers, always show Clear button 2026-03-15 17:42:01 +03:00
Anton A S a76d20b27b Fix ACP agent commands: claude-agent-acp, remove stale ts-expect-error 2026-03-15 17:06:57 +03:00
Anton A S 583b03eb3b Harden ACP transport, MCP server, and add permission dialog
- Extract mapUpdate to testable module, dynamic import for @tauri-apps/plugin-shell
- Add warnings for unhandled ACP content types and empty tool titles
- MCP server: session limit (max 10), fix null WS comparison, guard JSX preprocessing
- MCP server: read version from package.json instead of hardcoded 0.0.0
- MCP tests: use port 0 (OS-assigned) to prevent collision
- Connection error handling with user-friendly messages, stale session recovery
- Agent crash detection via close handler, destroying flag, buildCrashChunks
- Port collision: detect EADDRINUSE in vite-plugin stderr and log clear error
- Production Tauri: spawn openpencil-mcp via shell plugin, orphan reuse via health check
- Permission confirmation dialog (reka-ui AlertDialog) with queue, 60s auto-reject timeout
- Health check in ProviderSelect hides ACP agents when MCP server unavailable
- Move DESIGN_CONTEXT to app constants, add ACP_PERMISSION_TIMEOUT_MS
- 36 tests across 3 files (acp-transport, acp-permission, mcp-server)
- Update CHANGELOG, README, CONTRIBUTING, AGENTS.md
2026-03-15 16:49:43 +03:00
Danila Poyarkov dfb050538c Spawn MCP server as subprocess instead of SSR module
Replace ssrLoadModule hack with simple child_process.spawn.
Same approach as production (Tauri sidecar) — just run the MCP
server entry point directly. No re-evaluation on HMR, no
WebSocket reconnect storms, no globalThis guards.
2026-03-15 16:42:49 +03:00
Danila Poyarkov 2aa6d7f607 Fix bridge loading: use ssrLoadModule for workspace TS resolution 2026-03-15 16:42:48 +03:00
Danila Poyarkov 1092e6b9f5 Unify MCP server: always proxy to live editor via WebSocket
Remove headless SceneGraph mode — the MCP server now always proxies
tool calls to the browser via WebSocket. One server, one architecture:

  HTTP :7600 — /health, /rpc (CLI), /mcp (MCP Streamable HTTP)
  WS   :7601 — browser connects, executes tool calls against live editor

- Delete http.ts (merged into server.ts)
- Delete headless SceneGraph, open_file, save_file, new_document
- Delete canvaskit-wasm dependency (no headless rendering)
- Add ws dependency for WebSocket server
- Rewrite tests with mock browser over real HTTP+WebSocket
- Simplify bridge.ts to 6 lines (just imports and starts the server)
2026-03-15 16:42:46 +03:00
Danila Poyarkov 5641554ab7 Clean up automation bridge: direct imports, upgrade zod to v4
- Replace ssrLoadModule hacks with direct imports from @open-pencil/core
- Replace isBunRuntime() hack with @hono/node-server (works everywhere)
- Remove ViteServer type parameter from startAutomationBridge
- Upgrade @open-pencil/mcp zod dependency from v3 to v4
- Add @open-pencil/mcp as root devDependency for bridge imports
2026-03-15 16:40:41 +03:00
Danila Poyarkov fea1f4f95d Fix MCP bridge SSR module resolution
ssrLoadModule('@open-pencil/mcp') fails because Vite can't resolve
workspace TypeScript packages. Use relative path to source instead.
2026-03-15 16:40:39 +03:00
Danila Poyarkov 3d098b8a13 Remove shiki shim — handled by forked vue-stream-markdown
The fork inlines shiki stubs at build time, so consumers no longer
need to alias shiki to a shim.
2026-03-15 16:40:38 +03:00
Danila Poyarkov 4f0c11e275 Export shiki stubs from shim to prevent undefined destructuring
The shim must export everything vue-stream-markdown destructures
from import('shiki'): bundledThemesInfo, bundledLanguagesInfo,
createHighlighter. A throwing shim causes the import to succeed
but all named exports to be undefined.
2026-03-15 16:40:37 +03:00
Danila Poyarkov 758ddde8d8 Fix ACP stop button: close stream immediately on abort
The cancel request was sent to the agent but the stream stayed open
until prompt() resolved, leaving the UI in streaming state. Now the
stream closes immediately with a finish chunk, guarded against
double-close from the concurrent prompt resolution.
2026-03-15 16:40:36 +03:00
Danila Poyarkov ace8c26f2e Use forked vue-stream-markdown with shiki optional chaining fix
Replace npm vue-stream-markdown with open-pencil fork that guards
bundledThemesInfo/bundledLanguagesInfo with ?. to prevent TypeError
when shiki is not installed. Revert shiki shim and cdn-options hacks.
2026-03-15 16:40:35 +03:00
Danila Poyarkov 660bb3aab6 Fix shiki shim: export stubs instead of throwing
The throwing shim caused hasShikiModule() to return false, but
import('shiki') still resolved in some code paths where the module
was imported directly without the has-check, causing undefined
destructuring and the 'e.find' TypeError.
2026-03-15 16:40:33 +03:00
Danila Poyarkov 19505665f5 Add ACP transport unit tests for mapUpdate chunk mapping
Tests cover: text deltas (empty skip, start/continue), reasoning
chunks, tool call lifecycle (input/output/error), title fallback,
and unhandled update types.
2026-03-15 16:40:31 +03:00
Danila Poyarkov b71a3523d2 Inject design context into first ACP prompt
Without context, the agent uses its own file/terminal tools instead
of the MCP design tools. Prepend instructions on the first message
telling it to use open-pencil MCP tools to draw on the canvas.
2026-03-15 16:40:28 +03:00
Danila Poyarkov ff78d8d57b Bridge MCP server to live editor via automation WebSocket
ACP agents now get an MCP server at http://127.0.0.1:7600/mcp that
proxies tool calls through the automation WebSocket to the browser,
where they execute against the live editor store. No separate
subprocess, no stale npm package — tools operate on the open canvas.

- Add /mcp endpoint to automation bridge (Streamable HTTP transport)
- Each MCP tool call → sendToBrowser({command:'tool'}) → browser executes
- Strip mcp__open-pencil__ prefix from tool names in chat UI
- Export paramToZod from MCP package for bridge reuse
2026-03-15 16:40:27 +03:00
Danila Poyarkov e79fb5ac5d Connect ACP agents to OpenPencil MCP tools
Pass openpencil-mcp as a stdio MCP server to the ACP session so
agents get all design tools (create shapes, render JSX, export
images, etc.) instead of just plain conversation.

- Add makeFigma option to createServer for external FigmaAPI injection
- Skip file lifecycle tools (open/save/new) when using external FigmaAPI
- Pass MCP server config in ACP newSession mcpServers
- Add bun to Tauri shell scope for MCP server subprocess
2026-03-15 16:40:26 +03:00