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.
Figma expects text inside auto-layout frames to have no textAutoResize
field (NONE). Internally OpenPencil uses WIDTH_AND_HEIGHT so the layout
engine can measure text, but this caused overflow when exported .fig
files were opened in Figma.
Normalize at export time, matching how we already normalize font
families for the binary format.
Fixes#136
Parse the version field from the fig-kiwi header during .fig import
and store it on SceneGraph.figKiwiVersion. Use the original version
when re-exporting. New files default to 101 (current Figma standard).
Previously hardcoded to 106, which Figma rejected on import.
Fixes#153
The initial renderNow() fired before loadFonts() completed, causing
text measurement to return null on slower connections (especially
Safari). Layout computed without text dimensions got corrupted.
Move loadFonts() into the async init() flow and only render after
fonts are ready.
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
- Zero transforms for auto-layout children (Figma computes positions)
- Write bordersTakeSpace from strokesIncludedInLayout
- Always write explicit lineHeight, defaulting to ceil(fontSize * 1.2)
- Normalize font family in derivedTextData fontMetaData and fontName
- Extend normalizeFontFamily to strip optical size suffixes (9pt, 18px)
- Extract computeExportTransform() helper for lint compliance
- Add 20 unit tests covering all fixes
Fixes#134
TEXT nodes created via SceneGraph.createNode() defaulted to empty
fills, causing text to be invisible when .fig files are opened in
Figma. Now defaults to a solid black fill matching Figma's behavior.
Closes#133
Strip optical size suffixes (e.g. "DM Sans 9pt" → "DM Sans") and
"Variable" suffixes when writing fontName.family to .fig files.
This ensures Figma recognizes the font instead of showing a
"Missing font" dialog.
Closes#131
Previously, the CJK fallback loaded only a single Google Font
(Noto Sans SC) when local system fonts were unavailable. This meant
Korean and Japanese text would not render correctly in browsers that
block the Local Font Access API.
Changes:
- Replace CJK_GOOGLE_FONT (single string) with CJK_GOOGLE_FONTS array
containing Noto Sans SC, Noto Sans JP, and Noto Sans KR
- Load all three fonts in parallel via Promise.allSettled so each
script has proper glyph coverage
- Update text renderer to pass all loaded CJK families as fallbacks
- Maintain backward compatibility: getCJKFallbackFamily() still works,
new getCJKFallbackFamilies() returns the full list
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.
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.
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.
- 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.
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