Localize remaining menu state labels like Hide/Show and Lock/Unlock,
wire toolbar tool/action labels through the i18n stores, and localize
the app menu UI toggle tooltip.
Replace template-string JSON imports with an explicit locale loader map
so published package builds have stable locale asset references.
Commands and menu models now read labels from nanostores i18n
messages instead of hardcoded English strings. App menu rebuilt
to use i18n directly instead of matching on localized labels.
Added locale files: de, es, fr, it, pl (ru was added earlier).
All 6 translations plus English base cover menus, commands, tools,
panels, pages, and dialogs.
nanostores + @nanostores/i18n + @nanostores/vue (~6 KB total).
English base translations defined inline in messages.ts, other
locales loaded on demand from JSON. Grouped by domain: menu,
commands, tools, panels, pages, dialogs.
useI18n() composable returns reactive translation objects plus
locale controls. Russian locale included as first translation.
Browser language auto-detected, user preference persisted in
localStorage.
Hold Spacebar to temporarily switch to the Hand tool for panning,
release to return to the previously active tool. This matches the
standard Figma UX pattern.
- Uses separate keydown/keyup event listeners for reliable handling
- Guards against text editing mode and key repeat
- References state.activeTool for correct tool state
Fixes#147
Each RPC tool call created a fresh FigmaAPI instance, so switch_page
set currentPageId on a throwaway object. Sync the page change back
to the editor store after tool execution.
Fixes#112
Backspace/Delete and tool shortcuts (V, R, F, T, etc.) fired during
canvas text editing because the guard only checked for DOM input/textarea
focus, not the canvas TextEditor state. Add editingTextId check to both
the onEventFired handler and the plain() shortcut helper.
The loader div from index.html was never dismissed after the SDK
migration because use-canvas.ts moved to the SDK (which correctly
doesn't touch app-specific DOM). Added onReady callback in
EditorCanvas.vue to fade out and remove the loader.
Browser validation found 2 runtime issues:
1. TooltipProvider only wrapped EditorView, but <Tip> components in
AppToast rendered outside it → moved TooltipProvider to App.vue
to wrap everything including toasts.
2. SDK components call useEditor() but the app never provided the
editor via the SDK's injection system → added provideEditor(store)
in EditorView.vue so SDK primitives can access the editor.
Validated: zero console errors, canvas renders, demo shapes visible.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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
- 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)
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/.