Commit graph

369 commits

Author SHA1 Message Date
Danila Poyarkov 07fade6abb Use explicit Vue SDK barrel exports 2026-03-24 18:37:08 +03:00
Danila Poyarkov 80a2855bb3 Fix review issues in undo and Vue SDK 2026-03-24 18:15:40 +03:00
Danila Poyarkov bff8895775 Fix CLI tsconfig rootDir mismatch 2026-03-24 17:16:44 +03:00
Danila Poyarkov 936ba6637b Adopt package aliases across Vue SDK 2026-03-24 17:10:43 +03:00
Danila Poyarkov 1da5483fec Format remaining SDK refactor files 2026-03-24 17:02:01 +03:00
Danila Poyarkov 07af72ab24 Fix imported fig rendering and file open regressions 2026-03-24 16:52:45 +03:00
Danila Poyarkov 0a770248a2 Reorganize Vue SDK modules by domain 2026-03-24 09:50:31 +03:00
Danila Poyarkov 74bb119579 Extract Vue SDK editor controls and pickers 2026-03-24 09:50:28 +03:00
Danila Poyarkov 329dd301bd Align app UI primitives with shared ui overrides 2026-03-24 09:50:20 +03:00
Danila Poyarkov 0c30d044b2 Extract Vue SDK commands and menu model 2026-03-24 09:50:17 +03:00
Danila Poyarkov b9b960f6ef Fix broken SVG import path after shared/ restructure 2026-03-17 20:56:35 +03:00
Danila Poyarkov fb2bfb5827 SDK rewrite: delete 15 useless wrappers, replace with composables
Deleted 15 fake headless component folders (slot-prop wrappers with
no provide/inject, no DOM, no ARIA). Replaced with 9 composables:
- usePosition, useLayout, useAppearance, useTypography
- useExport, useVariables, useFillPicker, useGradientStops, usePageList

Kept 6 real headless component families with provide/inject + DOM:
- Canvas (WebGL surface lifecycle)
- ScrubInput (pointer capture + input + display)
- Toolbar (wraps Reka UI Toolbar)
- LayerTree (wraps Reka UI Tree + Atlaskit DnD)
- PropertyList (array CRUD via inject)
- GradientEditor (pointer capture for stop drag)

App components updated: use composables directly instead of wrapping
in Root components. -1047 net lines, cleaner separation.
2026-03-17 20:22:41 +03:00
Danila Poyarkov 30ae92483e Restructure SDK: shared/ replaces composables/+input/+utils/+context
Reka UI source structure: component folders + shared/ for utilities.
- composables/ → shared/ (camelCase names: useCanvas, useNodeProps, etc)
- input/ → shared/input/ (internal to useCanvasInput)
- utils/ → shared/ (toolCursor)
- context.ts → shared/editorContext.ts
- assets/ → shared/assets/
- components/ deleted (legacy wrappers)
- Added shared/createContext.ts (Reka UI pattern)
2026-03-17 20:04:14 +03:00
Danila Poyarkov 920af0a14e Fix scrub lag: simplify useSceneComputed to synchronous sceneVersion
Drop the emitter-to-shallowRef bridge (rAF batched, 60 lines).
state.sceneVersion on shallowReactive IS already Vue-reactive.
useSceneComputed is now 15 lines. Zero latency during scrub.
2026-03-17 19:45:22 +03:00
Danila Poyarkov 92c49f80f4 Self-review fixes: CanvasContext types, toast out of SDK, last graph access
- CanvasContext hit test return types: unknown → SceneNode | null
- Move toast.ts from SDK to app (src/utils/toast.ts) — singleton
  with unguarded window access doesn't belong in headless SDK
- ExportSection: store.graph.getNode → store.getNode
- ImageFillPicker: store.graph.images.get → store.getImage (new method)
- Add getImage(hash) to core editor create.ts

Zero store.graph access from components. Zero toast in SDK.
2026-03-17 17:13:33 +03:00
Danila Poyarkov c01e5ec141 Fix review round 2 remaining: duplicate children, JSON.stringify perf
- duplicateSelected: recursive subtree cloning via graph.cloneTree()
  Filters to top-level selection to avoid double-cloning. Children
  get new IDs wired to new parent. Single undo entry.
- PropertyListRoot isMixed: compare array lengths first (fast path),
  only JSON.stringify when lengths match (rare).
- glContext in use-canvas.ts: ownership verified correct, no change.
2026-03-17 16:58:25 +03:00
Danila Poyarkov 4d5b3482d4 Fix review round 2: emitter leaks, snapshot restore, undo ID safety
Critical:
- restorePageFromSnapshot: emit node:reordered after bulk restore
  so component sync and useSceneComputed bridge detect changes
- subscribeToGraph: capture and cleanup unbind functions on
  replaceGraph() — prevents listener leak on old graph emitter

Important:
- useNodeProps: node/nodes use useSceneComputed instead of plain
  computed — re-evaluate on any scene mutation, not just selection
- Undo forward closures: explicit id: containerId/frameId/groupId
  in createNode spreads — no more implicit ID preservation contract

Moderate:
- Extract usePropScrub composable — deduplicate previousValues +
  updateProp + commitProp from AppearanceControls + PositionControls
2026-03-17 16:54:18 +03:00
Danila Poyarkov ced083439e Complete SDK plan: all 21 component families built
New SDK components (7):
- ColorPickerRoot — color state, swatch CSS, update handler
- ColorInputRoot — hex string, updateFromHex handler
- FontPickerRoot — font families, search/filter, selection
- ImageFillControlsRoot — image preview, file picker, scale mode
- GradientEditorBar — pointer capture drag for stop bar
- GradientEditorStop — per-stop data + update handlers
- PageControlsRoot — page background color
- VariablesIndicatorRoot — collection/variable counts
- VariablesEditorRoot — collections, variables, CRUD, search/filter

App components rewritten to compose from SDK:
- ColorPicker, ColorInput, FontPicker, ImageFillPicker
- GradientEditor (script: ~100 → imports only)
- PageSection, VariablesSection

Full SDK inventory: 21 component families, 35 Vue files, 48 TS files.
Every item from SDK-PLAN.md is now implemented.
2026-03-17 16:21:26 +03:00
Danila Poyarkov 2a69ef956f SDK: AppearanceControls, TypographyControls, EffectsSection via PropertyListRoot
New SDK components:
- AppearanceControlsRoot — corner radius, visibility, opacity, blend mode
- TypographyControlsRoot — font family/weight/size, alignment, formatting
  toggles, missing font detection, font loading

EffectsSection rewritten to compose from PropertyListRoot \u2014 effect CRUD
via SDK, scrub tracking stays in app component.

All property sections now use SDK headless primitives:
- PositionSection → PositionControlsRoot
- LayoutSection → LayoutControlsRoot
- AppearanceSection → AppearanceControlsRoot
- TypographySection → TypographyControlsRoot
- FillSection → PropertyListRoot
- StrokeSection → PropertyListRoot
- EffectsSection → PropertyListRoot
- ExportSection → ExportControlsRoot
2026-03-17 15:59:43 +03:00
Danila Poyarkov 35e78e276a Fix all deferred code quality items
1. childIds direct mutation bypasses emitter (CRITICAL):
   Added SceneGraph.insertChildAt() — removes from old parent, inserts
   at index, emits node:reordered. Replaced all direct childIds
   filter+splice in structure.ts (wrapSelectionInContainer, ungroup,
   bringToFront, sendToBack) with insertChildAt().

2. Canvas input orchestrator 815→461 lines:
   Extracted input/move.ts (215 lines) — move drag, snap, reparent
   Extracted input/select.ts (172 lines) — hit resolve, select, hover
   Extracted input/draw.ts (32 lines) — shape drawing

3. PropertyListContext generic typing:
   PropertyListContext<T> — items, add, update, patch all typed.
   usePropertyList<Fill>() gives Fill-typed context.
   Added missing patch to context and PropertyListItem.
2026-03-17 15:51:42 +03:00
Danila Poyarkov 2757e9e6ec Fix PR review: critical bugs, package config, remaining title attrs
Critical fixes:
- useSceneComputed: watchEffect on state.sceneVersion as fallback
  when replaceGraph() swaps the emitter (bridge stayed on old graph)
- useLayerDrag: capture monitorForElements() cleanup, onScopeDispose
  (was leaking global listeners on tab switch)

Package fixes:
- packages/vue: sideEffects: false for tree-shaking
- Build: --noEmit → --emitDeclarationOnly (was skipping .d.ts)
- canvaskit-wasm peerDep: optional via peerDependenciesMeta
- use-canvas.ts: onUnmounted → onScopeDispose

Remaining title= attrs migrated to <Tip>:
- AppMenu toggle UI, TypographySection missing font, ProviderSettings
2026-03-17 15:30:28 +03:00
Danila Poyarkov 92857aed45 Add LayoutControlsRoot SDK primitive, LayoutSection 556→119 lines
New SDK component LayoutControls/LayoutControlsRoot.vue (189 lines):
- Layout sizing logic (width/height sizing, flex vs auto-layout vs grid)
- Alignment grid computation (3x3 for flex)
- Grid track CRUD (add/remove/update columns and rows)
- Padding management (uniform + per-side toggle)
- Sizing options derived from layout context

App LayoutSection.vue is now template-only (119 lines) — composes
LayoutControlsRoot with Tailwind styling + Lucide icons.
2026-03-17 12:10:27 +03:00
Danila Poyarkov 25b7d8afa4 useSceneComputed: bridge nanoevents emitter into Vue reactivity
Replace the sceneVersion counter hack with proper Vue pattern:
- shallowRef + triggerRef bridged from SceneGraph's nanoevents emitter
- Ref-counted listeners — subscribe on first useSceneComputed, unsubscribe
  when last scope is disposed. WeakMap per editor instance.
- Emitter fires on node:updated/created/deleted/reparented/reordered

sceneVersion counter stays in EditorState for:
- Renderer cache invalidation (label-cache, scene picture recording)
- watch() dependencies in LayerTreeRoot, ExportSection, autosave
These are legitimate uses of the counter as a cache key.
2026-03-17 11:55:13 +03:00
Danila Poyarkov a8e8e63a55 Replace void sceneVersion hacks with useSceneComputed composable
Add useSceneComputed() to SDK — wraps a function in a computed()
that depends on sceneVersion internally. Eliminates the anti-pattern
of 'void editor.state.sceneVersion' at the top of computed blocks.

Updated 8 files: EditorMenuRoot, ExportControlsRoot, PageListRoot,
useSelectionState, PagesPanel, CodePanel, VariablesDialog,
VariablesSection.

Zero void-sceneVersion hacks remaining in components.
2026-03-17 11:48:03 +03:00
Danila Poyarkov 513b16983f Clean remaining component leaks: requestRender, graph access, variable queries
Editor API additions:
- bindVariable/unbindVariable in nodes.ts (with undo + requestRender)
- setPageColor in pages.ts (state + requestRender)
- getVariablesByType, getVariable, getCollection, resolveColorVariable,
  getVariablesForCollection, getCollections, getCollectionCount,
  getVariableCount in variables.ts (read-through to graph)
- getNode, getChildren, getPages in create.ts (read-through to graph)

Removed:
- 5 redundant requestRender() after updateNodeWithUndo
- All store.graph.* access from components (was in 7 files, now 0)

Remaining requestRender (1 file): EffectsSection uses updateNode
(without undo) which correctly needs manual render trigger.
2026-03-17 11:38:32 +03:00
Danila Poyarkov fd44d7b853 Deduplicate: 47 → 28 clones, 1.37% → 0.61% duplication
Extracted shared helpers to eliminate copy-pasted code:

Core:
- computeAbsoluteBounds() in geometry.ts — replaces 4 bounding box loops
- makeDocumentNodeChange/makeCanvasNodeChange in kiwi-serialize.ts
- parseFigBuffer/parseFigKiwiContainer in kiwi/fig-parse-core.ts
  (fig-parse-worker.ts: 103 → 11 lines)
- createDefaultEditorState() — shared between core + app store
- calcClusterConfidence() in tools/analyze.ts
- retryWithNormalizedFamily() in fonts.ts
- getVectorNode() in tools/vector.ts
- renderFills() helper in renderer/scene.ts
- withNodeBounds() helper in renderer/overlays.ts
- drawNodeHighlightRect() in renderer/highlight-rect.ts
- isInViewport() in renderer/label-cache.ts

App:
- decodeTauriStderr() in utils/tauri.ts
- printNodeResults() in cli format.ts

Remaining 28 clones are: text-editor cursor movement (inherent),
xpath DOM traversal (inherent), small 5-line patterns too
context-specific to extract.
2026-03-17 11:10:11 +03:00
Danila Poyarkov 579492649d Phase 6: FillPicker, GradientEditor, ExportControls headless primitives
New SDK components:
- FillPickerRoot — fill type category (solid/gradient/image), swatch
  background, category switching. Events via emit, not callback props.
- GradientEditorRoot — stop state, subtype, add/remove/drag/select stops,
  active color, bar background. All stop mutations + color updates as
  action slot props.
- ExportControlsRoot — export settings list, scale/format options,
  selected node name.

Fixed callback props passed as :on-xxx across all SDK components:
- ScrubInputRoot: :on-keydown → :keydown
- FillPickerRoot: removed :on-update (consumers use @update on the
  component itself)
- GradientEditorRoot: :on-active-color-update → :update-active-color,
  :on-stop-drag → :drag-stop
2026-03-17 10:16:34 +03:00
Danila Poyarkov 72876f8a3e Phase 4-5: LayerTree and EditorMenu headless primitives
LayerTree SDK components:
- LayerTreeRoot — tree data from editor graph, expand/collapse with
  auto-expand on selection, scroll-into-view, scope sync
- LayerTreeItem — per-node slot with isSelected, padLeft, select,
  toggleExpand, toggleVisibility, toggleLock, rename

App LayerTree.vue now composes LayerTreeRoot + LayerTreeItem + TreeRoot/
TreeItem from Reka UI. Deleted LayerRow.vue — its logic is in the SDK.

EditorMenu SDK component:
- EditorMenuRoot — selection-aware state (hasSelection, isInstance,
  isComponent, isGroup, canCreateComponentSet, otherPages, visibility,
  lock) + copyAsText/SVG/JSX via slots

App CanvasMenu.vue now composes EditorMenuRoot — pure template with
Reka UI ContextMenu + Tailwind styling, zero business logic.

Removed useSelectionState composable — EditorMenuRoot replaces it.
2026-03-17 10:06:24 +03:00
Danila Poyarkov 638db38589 Phase 3: Property primitives — PropertyList, PositionControls
New SDK components:
- PropertyListRoot / PropertyListItem — headless array property editor
  for fills, strokes, effects. Provides add/remove/update/patch/
  toggleVisibility via slots. Handles multi-selection, mixed state.
- PositionControlsRoot — headless position/alignment/flip/rotate.
  Provides x/y/w/h/rotation + updateProp/commitProp/align/flip/rotate.

App FillSection.vue rewritten to compose from PropertyListRoot —
no more useNodeProps() destructuring, CRUD logic in SDK.

SkiaRenderer.renderFromEditorState() — single-call render that
takes EditorState directly (was 30 lines of manual property sync).
2026-03-17 09:43:26 +03:00
Danila Poyarkov 46e93a1b70 Phase 2: Canvas primitives, renderer renderFromEditorState
New headless components:
- CanvasRoot — provides canvas context, initializes surface + render loop
- CanvasSurface — bare <canvas> element, receives ref from CanvasRoot

SkiaRenderer.renderFromEditorState() — single method replaces 30-line
state sync in use-canvas.ts. Takes EditorState + graph + textEditor
directly, sets all renderer properties and calls render().

Example app updated to use CanvasRoot/CanvasSurface, ToolbarRoot/
ToolbarItem, PageListRoot instead of old monolithic components.
2026-03-17 09:29:38 +03:00
Danila Poyarkov d4c102c6f4 Phase 1: establish SDK component pattern with 3 exemplars
New Reka UI-style headless components following the pattern:
  ComponentName/context.ts — InjectionKey, provide/inject
  ComponentName/Root.vue — state management, slot props
  ComponentName/Item.vue — per-item logic, slot props
  ComponentName/index.ts — barrel export

ScrubInput (new headless):
  ScrubInputRoot — scrub state, pointer capture, edit/commit logic
  ScrubInputField — input element for direct editing
  ScrubInputDisplay — read-only display with slot for value/suffix
  App ScrubInput.vue composes these with Tailwind styling

Toolbar (new headless):
  ToolbarRoot — active tool, tool list, flyout state
  ToolbarItem — per-tool active state and select handler

PageList (refactored):
  PageListRoot — pages, currentPageId, CRUD actions via slots

Old components kept as backward-compatible aliases.
2026-03-17 09:12:27 +03:00
Danila Poyarkov aa805edf4d Rename NodeContextMenuContent → CanvasMenu, extract logic
- Rename to CanvasMenu.vue (was NodeContextMenuContent — terrible name)
- Move copy-as-text/SVG/JSX to core editor clipboard module
- Add useSelectionState() composable to SDK — selection-derived computeds
  (hasSelection, selectedNode, isInstance, isComponent, etc.)
- Eliminate 7x 'void store.state.sceneVersion' reactivity hacks
- Component is now mostly declarative template with menu items
2026-03-17 08:52:32 +03:00
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 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 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
Anton A S edd9a8e398 Entered container border, rotation support, INSTANCE/COMPONENT enterable, tests 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
Danila Poyarkov 9391ac0112 Fix save crash when COLOR variable is missing alpha
Reuse safeColor() (which defaults a ?? 1) in the variable export
path. The Kiwi schema requires all Color struct fields — variables
created without explicit alpha caused 'Missing required field a'.

Fixes #128
2026-03-16 14:17:28 +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 283b836d50 Add unit tests for font helpers: isVariableFont, normalizeFontFamily, styleToVariant, fetchBundledFont
21 new tests covering:
- normalizeFontFamily: Variable suffix stripping, edge cases
- styleToVariant: all weight→Google variant mappings
- isVariableFont: fvar table detection with synthetic TTF buffers
- fetchBundledFont: headless asset resolution, TTF magic validation
2026-03-16 13:40:16 +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