Commit graph

344 commits

Author SHA1 Message Date
Danila Poyarkov 3b32b4d80c
Perf: 5.4× faster file open, worker-based parsing, instance index (#96)
* Perf: cache label collection, offload .fig compression to worker

Label cache: collect sections/components once per scene change,
filter by viewport on each frame. Eliminates full tree walk during
pan/zoom (~17ms/frame → <1ms on large files).

Export worker: move fflate compression off the main thread to prevent
frame drops during save (451ms+ → non-blocking).

* Perf: worker-based .fig parsing, instance index, non-blocking font loading

- Offload .fig parsing (unzip + Kiwi decode) to a Web Worker
- Add instance index (componentId → Set<nodeId>) for O(1) getInstances()
- Defer graph event subscription during file open to skip redundant syncs
- Make font loading non-blocking — render immediately, load fonts in background
- Copy image buffers before worker transfer to prevent detached ArrayBuffer crash
- Show toast on font load failure and file open errors
- Cache failed Google Fonts families to avoid repeated network requests
- Fix missing ref import in FillPicker
- Yield to UI between parse and layout for responsive loading spinner
2026-03-12 14:49:28 +03:00
Danila Poyarkov 6072ce57c1 Fix lint warnings: non-null assertions, useless spreads, unused code, type safety
- Replace non-null assertions with guards/optional chaining
- Replace useless spreads with Array.from() where mutation-safe
- Remove unused imports, variables, and functions
- Fix no-base-to-string with proper type narrowing
- Fix sort without comparator, new Array() pattern
- Prefix unused parameter with underscore

Warnings: 76 → 42 (remaining are max-lines, justified any casts, vendored code)
2026-03-11 13:33:54 +03:00
Danila Poyarkov 509520f273 Add MiniMax AI provider support 2026-03-11 13:26:32 +03:00
Dipankar Shaw c4db9ec165
Add Z.ai support to AI providers and chat functionality (#90) 2026-03-11 13:25:15 +03:00
Anton Soldatov 37da83aec3
Image drag-and-drop, clipboard paste, and paste-at-cursor (#92)
* Image drag-and-drop and clipboard paste onto canvas

* Paste images at cursor position, track canvas cursor in state

* Paste nodes at cursor position (Figma and internal clipboard)

* Fix review: center images at cursor, fix hasImageFiles, clean up hash/undo

* Paste at viewport center when cursor is outside canvas

* Fix Figma paste positioning: center nodes at cursor after import

* Image support: clipboard, export, drag-and-drop, paste, renderer, tools, UI

- Clipboard: embed image bytes (base64) in OpenPencil clipboard payload so
  copy/paste between documents preserves image fills
- Fig export: write images/ folder to .fig zip (both fflate and Tauri paths)
- Tauri: extend build_fig_file to accept image entries
- Renderer: implement CROP (with imageTransform) and TILE (TileMode.Repeat)
  scale modes, fix FIT to center the fitted image
- FigmaAPI: add createImage(bytes) with sync FNV-1a hash
- Tools: add set_image_fill tool for AI/MCP
- FillPicker: replace placeholder with file picker, preview, scale mode selector
- Drag-and-drop: new use-image-drop composable creates image nodes from dropped files
- Paste: keyboard paste handler detects image clipboard items
- Shared utils: extract hashImageBytes and getImageDimensions to src/utils/image.ts

* Add image tests and Yjs image sync for collaboration

Tests (18 new):
- FigmaAPI.createImage: deterministic hash, storage, format
- set_image_fill tool: all scale modes, error handling, storage
- Clipboard roundtrip: image bytes preserved, multiple images, children
- Fig export/import: zip contains images/, full round-trip

Collab:
- Add yimages Y.Map to sync graph.images via Yjs
- Observer applies remote image adds/deletes to local graph
- syncNodeToYjs pushes referenced image data alongside node props
- syncAllNodesToYjs bulk-syncs all images on room share

* Fix FillPicker: remove deleted utils/image import, use SHA-1 inline

* Extract storeImage() on editor store, use in FillPicker and placeImageNode

* Unify image hash: use sync FNV-1a everywhere, export computeImageHash

Editor store's hashBytes (async SHA-1) produced different hashes than
FigmaAPI.createImage (sync FNV-1a) for the same bytes. This meant
drag-and-drop images couldn't be deduplicated against AI tool images.

Replace hashBytes with computeImageHash from core. storeImage() is
now sync.

* Skip drawing IMAGE fills when image data is missing

When pasting from Figma, image fills reference a CDN hash but no pixel
data is included in the clipboard. Previously this rendered as a solid
black rectangle because applyImageFill bailed without setting a shader,
leaving stale paint state.

applyFill now returns false when the fill can't be applied, and callers
skip the draw call. The node still exists with the correct imageHash —
if the image data is later provided (e.g. via file re-open), it will
render correctly.

* Warn when Figma paste has missing image data

Show amber warning toast when pasted nodes reference image fills
without available bytes (Figma clipboard limitation).

Add 'warning' toast variant with tailwind-variants, extract toast
styles to src/components/ui/toast.ts.

* Use useFileDialog and useObjectUrl in FillPicker

Replace manual file input ref, click(), createObjectURL/revokeObjectURL
with vueuse composables. Remove hidden <input type=file> from template.

---------

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-11 07:52:04 +03:00
Anton Soldatov e939025f7d
Fix auto-layout overflow, grid layout, AI chat UX (#86)
* Fix auto-layout overflow: MeasureFunc, min/max, absolute positioning, text wrap

Layout engine (layout.ts):
- Use Yoga MeasureFunc for text nodes instead of static pre-measurement,
  so text wraps correctly when width is determined by flex layout
- Add min/max width/height constraint support (minWidth, maxWidth, etc.)
- Translate counterAxisAlignContent to Yoga setAlignContent for wrap layouts
- Extract configureFlexContainer to share between root and nested frames

JSX renderer (render/renderer.ts):
- Auto-set layoutPositioning ABSOLUTE for elements with x/y inside auto-layout
- Fix text auto-resize: set HEIGHT when text fills parent in auto-layout

Text measurer (renderer/renderer.ts):
- Accept optional maxWidth parameter for constraint-based measurement

* AI prompt: prefer describe over export_image for post-render verification

* Strip TS casts from AI-generated JSX before sucrase parse

AI models sometimes emit `as any`, `as const` etc. in JSX props.
Sucrase with jsx-only transform can't handle these, causing parse errors.

- Strip TypeScript cast expressions before passing to sucrase
- Add forbidden patterns to AI prompt: as any, template literals for
  sizes, Math.random()

* Cache text MeasureFunc results to avoid repeated buildParagraph calls

Yoga calls MeasureFunc multiple times per node during calculateLayout.
Each call was creating a full CanvasKit Paragraph — expensive for layouts
with many text nodes. Cache by rounded constraint width.

* AI chat: fix provider settings popover, add max output tokens, fix paste in chat input

- Fix ProviderSettings popover not visible (remove tooltip/popover trigger
  conflict, add collision-padding, isolate z-[51])
- Add configurable max output tokens (default 16384) in provider settings
- Pass maxOutputTokens to ToolLoopAgent to prevent truncated tool calls
- Add system prompt size limits: split render calls at ~40 elements
- Fix paste/copy/cut in chat input (stop event propagation to canvas handler)

* Fix 6 layout engine bugs: hidden size, FILL basis, SPACE_EVENLY, alignSelf range, grid stretch, absolute children

Layout engine (layout.ts):
- Guard applyYogaLayout to preserve hidden children dimensions
- Add setFlexBasis(0) for FILL sizing so children share space from zero
- Add SPACE_EVENLY to mapJustify via Justify.SpaceEvenly
- Replace hardcoded STRETCH checks with mapAlignSelf for full range
- Use setWidthStretch/setHeightStretch for grid children instead of flexGrow
- Insert absolute children in Yoga tree via configureAbsoluteChild with
  PositionType.Absolute, keeping manual x/y (no write-back)

Types (scene-graph.ts):
- Add SPACE_EVENLY to LayoutAlign union
- New LayoutAlignSelf type: AUTO | MIN | CENTER | MAX | STRETCH | BASELINE

Import (kiwi-convert.ts):
- Map SPACE_EVENLY correctly instead of collapsing to SPACE_BETWEEN
- New mapAlignSelf function for full StackCounterAlign range

Tools (modify.ts):
- Expand set_layout align enum with SPACE_EVENLY
- Expand set_layout_child align_self enum with MIN/CENTER/MAX/BASELINE

Tests: 86 pass (+13 new covering all 6 fixes)

* Grid layout support in JSX renderer, auto-height grids, flex children stretch in cells

Renderer:
- Parse grid/columns/rows/columnGap/rowGap/gap props
- Parse grid child positioning: colStart/col/rowStart/row/colSpan/rowSpan
- Grid prop takes precedence over padding-triggered auto-layout
- SPACE_EVENLY added to ALIGN_MAP
- Numeric columns shorthand (columns={3} → 3×1fr)
- Auto-height: grid without rows sets height=0 for Yoga auto-sizing
- fill sizing in grid children → layoutAlignSelf: STRETCH
- Component builders accept variadic children: Frame(props, ...children)

Layout:
- Grid auto-height: skip setHeight when no gridTemplateRows
- Write back computed height for auto-height grids
- Grid children with layoutMode stretch width to fill cell
- recomputeGridChild: re-run flex layout after grid assigns cell size
  (temporarily sets sizing to FIXED so HUG doesn't override grid width)

Export:
- Skip height export for auto-height grids (no rows template)

Tests: 18 new grid render tests covering all paths

* Auto-enable flex for justify/items props, discourage export_image in AI prompt

* Address review: use sucrase typescript transform, don't reset chat on maxOutputTokens change

* Fix lint errors: extract applyFrameSize, wrap nested ternary, remove unnecessary cast

* Remove SPACE_EVENLY — not a valid Figma layout value

* CI: debug LFS pull from R2

* Apply maxOutputTokens dynamically via prepareCall instead of at agent creation

* Fix heavy .fig parse: restore GUID/defID guards, fix optional types

Commit f3eac5a removed null guards from fig-import and
instance-overrides as 'unnecessary conditions', but Kiwi-decoded data
can have NodeChanges without GUIDs and ComponentPropAssignments/Refs
without defIDs. Made the types optional to match reality.

Also fix CI LFS: add git lfs install --force before pull (R2 endpoint
needs the filter registered first).

* Fix Google AI model IDs: gemini-3.1-pro → gemini-3.1-pro-preview

Closes #91

---------

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-10 15:43:04 +03:00
Danila Poyarkov 3e102ea55a Event-driven requestRender and component sync via SceneGraph emitter
Move requestRender() and component instance sync from manual call sites
to graph event subscriptions. Removes 79 manual requestRender() calls
(94→22) and all 9 syncIfInsideComponent calls.

Batching: component sync uses queueMicrotask — mutated node IDs
accumulate during a synchronous block, then deduplicate to ancestor
component IDs and call syncInstances once per component. requestRender()
is already rAF-batched (just increments counters checked by animation
frame loop), so multiple event-triggered calls in one sync block
collapse to a single repaint.

Event subscriptions:
- node:updated → invalidate render cache, schedule component sync, requestRender
- node:created/deleted/reparented/reordered → schedule component sync, requestRender
- Re-subscribes when graph instance is replaced (file open/reload)

Also fixes collab sync gap: subscribe to node:reordered in use-collab.ts
(same-parent reorder was never synced to Yjs peers).

Remaining requestRender() calls are for pure UI state changes (pen tool,
viewport pan/zoom, text editing, selection) and methods that bypass
events (detachInstance, bringToFront/sendToBack, restorePageFromSnapshot).
2026-03-09 16:13:03 +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 e3ab8dab38 AI chat rendering, undo, visual verification, and provider compatibility
Rendering & layout:
- Fix COUNTER_ALIGN_MAP stretch mapping
- Direction-aware fill sizing based on parent flex direction
- Text without explicit width defaults to WIDTH_AND_HEIGHT auto-resize
- Padding auto-enables vertical auto-layout
- clipsContent propagated as Overflow.Hidden to Yoga
- Text height measurement for HEIGHT auto-resize mode
- Export x/y for absolute children, text w/h per textAutoResize
- Export STRETCH as fill on cross axis

Undo system:
- Undo for auto-layout and layer tree reorder
- Atomic undo for drag + reparent
- Page snapshot undo for AI tool operations
- mouseleave no longer terminates active drag

AI tools & vision:
- Add get_jsx, diff_jsx, and describe tools
- export_image returns image via toModelOutput
- Chunked uint8ArrayToBase64 (stack overflow fix)
- computeAllLayouts after AI tool execution
- onBeforeExecute/onAfterExecute receive ToolDef

Provider & UI:
- Add anthropic-compatible provider
- API type toggle for OpenAI-compatible
- Debug toolbar (dev only)
- Preserve chat on UI toggle
- Enhanced system prompt with JSX reference

Co-authored-by: Anton A S <eddclyde@yandex.ru>
2026-03-09 15:49:29 +03:00
Danila Poyarkov 9890b7c1a1 Enable Tailwind CSS class sorting in oxfmt
Sort classes using the same algorithm as prettier-plugin-tailwindcss,
configured for Tailwind v4 (reads src/app.css for theme).

Also sorts class strings inside twMerge() and tv() calls.

Eliminates 2 nested ternaries that conflicted with the formatter
(oxfmt strips parentheses that the lint rule requires).
2026-03-09 15:46:20 +03:00
Danila Poyarkov 7f5a388e2b Code review: svg-export/ folder, restore comments, cleanup
- Move svg-export-{defs,paths}.ts into svg-export/ folder
  (defs.ts, paths.ts, index.ts) instead of prefix-based split
- Remove duplicate getChildren lambda in fig-import.ts importPages
- Remove dead dashPattern local in kiwi-convert.ts nodeChangeToProps
- Restore useful explanatory comments in instance-overrides.ts
  (DSD propagation, direct vs cloned assignments, seed skipping)
- Rename applyEffectOverrides → applyShapeAndEffectOverrides
  (handles polygon props + shadow + blur, not just effects)
- Format src/ with oxfmt
2026-03-09 15:22:14 +03:00
Danila Poyarkov 5549a2f46f Auto-layout icons in layer tree, frame title draggable on canvas
Layer tree shows layout-specific icons for auto-layout frames:
vertical → rows-3, horizontal → columns-3, grid → grid-3x3.
Only applies to FRAME nodes — components/instances keep their
purple diamond icon.

Clicking a selected top-level frame's name label on the canvas
now starts a drag instead of deselecting (hitTestFrameTitle).

Fix toolbar disappearing — TOOLS was incorrectly moved into
import type by consistent-type-imports, erasing it from the
runtime bundle.
2026-03-09 12:36:01 +03:00
Danila Poyarkov 1631d48c40 Reduce cyclomatic complexity below 20 in all functions
Extract dispatch branches into focused helpers:

use-canvas-input.ts (5 functions, was 21–61):
- onMouseDown → handleSelectDown, startPanDrag, tryStartRotation,
  tryStartResize, handleTextEditClick, duplicateAndDrag
- onMouseMove → dragNodes, updatePan, updateRotation,
  extendTextSelection, updateDrawShape, updateMarquee
- onMouseUp → commitDrag, commitDrawing, reparentOutsideNodes
- applyResize → constrainToAspectRatio
- computeAutoLayoutIndicatorForFrame → computeIndicatorPosition

use-keyboard.ts (was 39):
- onEventFired → shouldPreventDefault + Set-based lookups

server.ts (was 34):
- handleRequest → Partial<Record> command dispatch map

LayerTree.vue (was 22):
- updateDropTarget → classifyDropZone, resolveInsertPosition

use-text-edit.ts (was 27):
- onKeyDown → handleDeletion, handleHorizontalArrow, metaKeyActions map
2026-03-09 11:24:56 +03:00
Danila Poyarkov cd614ef8f7 Add custom JS lint rules, replace inline types with named imports
Three oxlint JS plugin rules in lint/plugin.js:

- no-inline-named-types: configurable shape→name map catches inline
  { x: number; y: number } etc. and suggests Vector, Color, GUID,
  Rect, Matrix from @open-pencil/core
- no-structuredclone-scene-arrays: flags structuredClone on
  fills/strokes/effects — use typed copy helpers instead
- no-math-random: bans Math.random() in favor of crypto

Fixed 40 violations across 13 files: replaced inline type literals
with named imports, switched figma-api.ts structuredClone calls to
copyFills/copyStrokes/copyEffects.
2026-03-09 09:41:28 +03:00
Danila Poyarkov 17faf4df93 Add strict lint rules to catch AI-generated slop at compile time
New rules:
- typescript/no-unnecessary-condition: catches redundant ?., ??, and
  always-true/false branches (22 violations fixed)
- typescript/consistent-return: mixed return/no-return in functions
- typescript/no-unnecessary-type-parameters: unused generic params
- typescript/prefer-for-of: use for-of instead of indexed loops
- unicorn/no-nested-ternary: forbid nested ternaries
- unicorn/consistent-existence-index-check: !== -1 over >= 0
- no-empty-function: no empty function bodies

Fixed all 32 violations: removed unnecessary optional chaining on
non-nullish values, tightened Record types to Partial<Record> where
index access can return undefined, replaced nested ternary with
lookup table, added exhaustive default cases.
2026-03-09 09:16:07 +03:00
Danila Poyarkov f96fffb83f Harden oxlint: consistent-type-imports, no-duplicates, no-misused-promises, prefer-optional-chain
Add 6 new lint rules: typescript/consistent-type-imports,
typescript/no-unnecessary-type-assertion, typescript/prefer-optional-chain,
typescript/no-misused-promises, import/no-duplicates, import/no-mutable-exports.
Ban @/types shim via no-restricted-imports.

Fix all violations: replace inline import() annotations with proper
type imports, merge duplicate module imports, add void to fire-and-forget
promises, convert && chains to optional chaining.
2026-03-09 09:05:04 +03:00
Danila Poyarkov 33389a95ad Remove re-export shims and import types directly from core
Delete src/types.ts (pure re-export of Color, Rect, etc.) — 9 files
now import directly from @open-pencil/core. Remove dead AI_PROVIDERS
re-export from use-chat.ts and dead font function re-exports from
engine/fonts.ts.
2026-03-09 08:56:47 +03:00
Danila Poyarkov 9c110bff92 Add Vue linting rules and fix all violations
Enable 12 vue/ rules in oxlint: lifecycle, props, emits, imports.
Convert 9 components from `const props = defineProps()` to
destructured form (Vue 3.5+ reactive destructuring).
2026-03-08 23:08:27 +03:00
Danila Poyarkov 7a334268d0 Fix all type-aware lint warnings
Add void prefix to 23 fire-and-forget promises across 10 files.
Fix no-base-to-string in bridge.ts by using Buffer.from().toString().
Promote no-floating-promises from warn to error.
2026-03-08 23:04:44 +03:00
Danila Poyarkov 5cd7cc12b2 Fix double-click rename in layer tree and pages panel
Prevent reka-ui TreeItem from toggling expand/collapse on click —
expand is handled by the chevron button with @click.stop.

Match page rename input styling to layer tree: show file icon next
to the input, use the same compact border/padding classes.
2026-03-08 22:58:38 +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 d84f165a27 Update README roadmap, features, tool counts, and project structure 2026-03-08 00:04:22 +03:00
Danila Poyarkov 01c8753f6e Merge PR #70: E2E test coverage + mutation unit tests
48 new E2E tests across 9 spec files, 26 mutation unit tests,
store/canvas test helpers, data-test-id attributes.

Fixes: explicit error in canvasBounds(), remove any casts,
trim trailing blank line, condense changelog entries.
2026-03-07 23:47:00 +03:00
Danila Poyarkov 6eb206a451 Merge docs PR, fix all lint/typecheck errors, update changelog
- Merge add-vitepress-docs: docs site with 6 locales, SEO, user guide
- Remove VitePress cache files that slipped into git
- Fix all 11 oxlint warnings: replace non-null assertions in
  use-collab.ts with local const captures inside closures
- Update changelog with docs and lint fix entries
2026-03-07 23:19:19 +03:00
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