Commit graph

392 commits

Author SHA1 Message Date
Danila Poyarkov c1d7fc80b0 Fix property panel visibility toggles, independent corners/stroke sides, extract shared ColorStyleRow
- Fix fill/stroke/effect/appearance visibility eye toggle (stale clone reads)
- Fix independent corner radii toggle not showing 4 inputs
- Fix stroke sides toggle: replace dropdown with direct toggle like corners
- Copy uniform weight to all sides when expanding stroke sides
- Batch multi-selection visibility toggles into single undo entry
- Extract ColorStyleRow shared by FillSection and StrokeSection
- Replace useFillVariableBinding/useStrokeVariableBinding with useColorVariableBinding(kind)
- AppearanceSection uses useAppearance() directly instead of slot wrapper
- Add sceneVersion dependency to useNodeProps and PropertyListRoot computeds
- Add E2E tests for visibility toggles and corner/stroke side toggles
2026-03-27 17:06:33 +03:00
Danila Poyarkov ae13bb1cbd Sync locale files with latest i18n keys 2026-03-25 21:54:12 +03:00
Danila Poyarkov e03038f4bf Translate collaboration share popover 2026-03-25 21:48:38 +03:00
Danila Poyarkov 4b9cd54983 Translate more editor UI surfaces 2026-03-25 18:47:23 +03:00
Danila Poyarkov 9f296b7357 Translate chat setup and code panel strings 2026-03-25 18:40:17 +03:00
Danila Poyarkov 2f646ffcaf Translate more visible UI strings 2026-03-25 18:26:03 +03:00
Danila Poyarkov 77032ab092 Translate visible panel chrome for Russian locale 2026-03-25 18:12:09 +03:00
Danila Poyarkov cf16857a76 Add locale picker to app menu 2026-03-25 18:05:58 +03:00
Danila Poyarkov e52fcf1b8f Fix Vue package declaration build 2026-03-25 17:43:16 +03:00
Danila Poyarkov 6bfc5c2189 Rename toggleUI i18n key 2026-03-25 17:40:26 +03:00
Danila Poyarkov ea4676d991 Finish i18n pass for menus and tool labels
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.
2026-03-25 17:39:47 +03:00
Danila Poyarkov eb2891233a Wire i18n into commands, menus, and add locale files
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.
2026-03-25 17:05:11 +03:00
Danila Poyarkov 2b3219e106 Add i18n to Vue SDK with nanostores
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.
2026-03-25 16:17:03 +03:00
Danila Poyarkov 1f851e29cd Normalize textAutoResize to NONE for auto-layout children on export
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
2026-03-25 06:41:45 +03:00
Danila Poyarkov 1e35c0d3a8 Preserve fig-kiwi version on roundtrip
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
2026-03-25 06:02:24 +03:00
Danila Poyarkov 29876f0c2b Await font loading before first canvas render
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.
2026-03-25 00:32:06 +03:00
TKman c52f88e34c feat: add spacebar hold to temporarily activate Hand (pan) tool
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
2026-03-24 23:47:56 +03:00
Shai Rubinstein 2e723caba0 fix: kiwi serialization bugs causing broken auto-layout in Figma import
- 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
2026-03-24 23:47:56 +03:00
Shai Rubinstein 5139836816 fix: default TEXT nodes to solid black fill
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
2026-03-24 23:47:56 +03:00
Shai Rubinstein 2efd63ea09 fix: normalize font family names on .fig export
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
2026-03-24 23:47:56 +03:00
myhency 8599750f1d feat(fonts): load multiple CJK Google Fonts for full Han/Japanese/Korean coverage
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
2026-03-24 23:47:55 +03:00
Danila Poyarkov a743da54b6 Document Vue SDK public APIs 2026-03-24 23:25:06 +03:00
Danila Poyarkov 99e646aaa4 Refine docs structure for SDK and automation 2026-03-24 23:24:08 +03:00
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