Commit graph

475 commits

Author SHA1 Message Date
Danila Poyarkov 405a3f2ecf fix(vue): show Create Instance instead of Create Component for components
When right-clicking a component, the context menu now shows
'Create Instance' instead of 'Create Component'. Previously both
were shown simultaneously.
2026-04-22 14:21:18 +03:00
Danila Poyarkov 3a2a3865f4 fix(vue): enable Create Instance menu item for components
The disabled flag was inverted — the manually constructed menu item
used `!commandMenuItem(...).disabled` which made the item disabled
when the command was enabled, and vice versa. Replaced with a direct
commandMenuItem() call which handles label, disabled, and action
correctly.

Fixes #223
2026-04-22 14:16:14 +03:00
Danila Poyarkov e23b3f0ce1 feat(layout): use opentype.js for accurate headless text measurement
estimateTextSize() now tries opentype.js per-glyph advance widths
before falling back to the rough 0.6 × fontSize × charCount heuristic.
Since font data is already cached via loadFont(), this gives accurate
variable-width measurement for any loaded font — no CanvasKit needed.

Parsed opentype.js Font objects are cached per family+style to avoid
reparsing on every layout pass.
2026-04-22 14:11:36 +03:00
Danila Poyarkov 226542cfcf fix(layout): use stored .fig dimensions for headless text measurement
The estimateTextSize() fallback (fontSize × 0.6 × charCount) produced
wildly inaccurate results, causing 26K node mismatches on material3.fig.
Nodes imported from .fig files already carry Figma's computed dimensions —
use those instead. Falls back to the estimate only for newly-created
nodes with the 100×100 default size.

Reduces headless layout mismatches by ~56% (26,299 → ~11,669), with
TEXT node mismatches dropping 92%.

Fixes #212

Co-authored-by: Alexander Bonin <3462438+alex4o@users.noreply.github.com>
2026-04-22 14:00:07 +03:00
Danila Poyarkov c2a43234a3 fix(cli): remove inline examples from query description
The multi-line description caused citty to pad every other command's
row to the same height, creating huge vertical gaps in --help output.

Fixes #219
2026-04-22 13:53:03 +03:00
Danila Poyarkov 44484050e2 fix(canvas): make canvas focusable so Backspace works after click
The canvas had no tabindex, so clicking on it didn't move focus away
from previously focused input elements. When document.activeElement
was still an input/textarea (e.g. from a panel interaction), the
keyboard shortcut guard treated Backspace as blocked by an active
input — making it impossible to delete selected nodes via the canvas.

- Add tabindex="-1" + outline-none to the canvas element
- Explicitly focus canvas on mousedown (except during text editing)

Fixes #220
2026-04-22 13:45:23 +03:00
Chris Gliddon 0f875eee46
feat(canvas): default canvas bg to dark on prefers-color-scheme: dark (#205)
A user on a dark OS who launches OpenPencil currently gets a pure-white
canvas flash into their dark desktop every time — visually jarring and
eye-straining in low light. Upstream has no theme system at all, so
this change defers to the OS `prefers-color-scheme` for newly-created
pages only.

Changes:

- Add `CANVAS_BG_COLOR_DARK` (#2c2c2c, matching Figma's dark-mode
  canvas) next to the existing light `CANVAS_BG_COLOR`.
- Add `getDefaultCanvasBgColor()` helper that reads
  `matchMedia('(prefers-color-scheme: dark)')` once per call.
- Use the helper in `editor/create.ts` (initial editor state) and
  `editor/pages.ts` (page-switch reset). Both are new-page
  initializers.

Deliberately untouched:

- `io/formats/fig/export.ts` still writes the static light
  `CANVAS_BG_COLOR` as the page `backgroundColor`. If a dark-theme
  user's exports forced darkness on recipients, files wouldn't be
  portable. The serialization path stays neutral.
- `canvas/renderer.ts` default — only a fallback when state is
  missing `pageColor`; effectively dead once `create.ts` runs.
- Per-page ColorInput in `PageSection.vue` remains the manual
  override for any user preference.

Companion colors (rulers, selection, snap) were already dark-
friendly values and need no adjustment — rulers become slightly more
legible on dark canvas than they were on light.

Tested on Arch + KDE Plasma (dark) + WebKit2GTK 2.52 + Mesa 26:
new pages render ~#2c2c2c, switching to light OS theme flips back
to #f5f5f5 on next launch, saved-and-reopened files keep the light
export value (confirming the serialization boundary).
2026-04-20 11:44:50 +03:00
Anton Soldatov 029c3637a1
Fix tab order and keyboard handling in inspector panel (#191)
* fix(a11y): tab order in inspector panel follows visual layout

- ScrubInput gets tabindex=0 so Tab reaches it; on focus it enters
  edit mode and transfers focus to the inner <input>
- All inspector buttons get explicit tabindex=0 to follow DOM order
- Tab flow: align buttons → X → Y → W → H → rotation → flip/rotate →
  opacity → corner radius → ...

Closes #180

* fix(inspector): apply ScrubInput value live while typing

* fix(keyboard): block shortcuts while ScrubInput is focused

- Detect scrub input focus via closest() in isEditing() guard
- Add isScrubInputFocused() check in plain() so tool shortcuts,
  Backspace, Enter etc. don't fire while typing in inspector fields

* fix(inspector): use reactive state for ScrubInput focus, remove redundant tabindex on buttons

- Surface editing state from ScrubInputRoot via editing-change emit
- Track scrubInputFocused in editor store instead of querying DOM
- Remove no-op tabindex="0" from all <button> elements
- Keep tabindex on ScrubInput wrapper <div> (actual tab order fix)

---------

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-04-16 10:14:16 +03:00
Lay Flags 7ae97935e2
fix(layout): resolve design token variables before passing to yoga-layout (#202)
* fix(layout): resolve design token variables before passing to yoga-layout

applyPadding() and applyAutoLayout() passed variable references like
$--spacing-lg directly to yoga-layout's setPadding()/setGap() which
expects numeric values, causing a crash.

Now resolves variable references via ctx.resolveNumber() before
assignment, following the same pattern already used by parseSize()
and applyCornerRadius() in the same file.

Fixes open-pencil/open-pencil#201

* test(layout): add regression tests for variable padding/gap resolution (#201)
2026-04-16 10:11:28 +03:00
Chris Gliddon 2cb32cfb00
feat(mcp): add list_available_fonts tool for font discovery (#210)
* feat(mcp): add list_available_fonts tool for font discovery

The existing list_fonts tool reports only fonts currently used in the
current page (it walks every TEXT node and groups by fontFamily).
Agents authoring new text have no way to discover what fonts the host
can render — they have to guess and hope set_font succeeds.

This adds list_available_fonts, which returns the set of font
families the host advertises. On desktop this is the system fonts
font-kit enumerates plus any bundled fonts the host registers; in
non-desktop runtimes that don't expose enumeration, it returns an
empty list with an explanatory note rather than throwing.

The optional `family` filter mirrors list_fonts for a substring
search, so agents can do `list_available_fonts({family: "mono"})` to
narrow before set_font.

Wired through:
- packages/core/src/figma-api/index.ts: adds optional
  listAvailableFontFamilies hook on FigmaAPI (same pattern as
  exportImage). Optional so non-desktop hosts aren't forced to
  implement it.
- packages/core/src/tools/read.ts: defines the tool.
- packages/core/src/tools/registry.ts: registers it in CORE_TOOLS so
  the MCP server advertises it.
- src/automation/figma-factory.ts: provides the desktop
  implementation by delegating to engine/fonts.ts#listFamilies which
  already wraps the Tauri list_system_fonts sidecar.

* refactor(mcp): reshape to listAvailableFontsAsync matching Figma API

Per review feedback on #210: FigmaAPI mirrors the Figma Plugin API, and
Figma already exposes figma.listAvailableFontsAsync(): Promise<Font[]>
where Font = { fontName: { family, style } }. Replacing the custom
listAvailableFontFamilies hook with the Figma-shaped method means eval'd
plugin code and any future plugin-compat layer get font enumeration for
free. Follows the same pattern as loadFontAsync on FigmaAPI.

- packages/core/src/figma-api/proxy.ts: export FigmaFont type
  ({ fontName: FigmaFontName }) alongside FigmaFontName.
- packages/core/src/figma-api/index.ts: drop optional
  listAvailableFontFamilies; add listAvailableFontsAsync() as a real
  method (non-optional) with a safe [] default, mirroring loadFontAsync.
- packages/core/src/tools/read.ts: list_available_fonts execute() now
  consumes Font[] and dedupes families via a Set. Drops the
  host-capability fallback note — the new method always returns an
  array, so a non-desktop host just yields {count: 0, fonts: []}.
- src/engine/fonts.ts: add listFonts() returning TauriFontFamily[]
  ({family, styles[]}) so the factory can emit one Font per (family,
  style) pair.
- src/automation/figma-factory.ts: override listAvailableFontsAsync,
  flattening {family, styles[]}[] to Font[].
- oxlint.json: add listFonts to the engine/fonts allowlist.

Tool public shape unchanged (still returns {count, fonts: string[]})
so agents calling list_available_fonts see no change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:56:33 +03:00
Danila Poyarkov 1109c7498a chore: format 2026-04-15 13:45:40 +03:00
Danila Poyarkov 909dc115ec fix(hit): replace non-null assertions with nullish coalescing
editor.renderer?.zoom! → editor.renderer?.zoom ?? 1
2026-04-15 10:55:25 +03:00
jongwong 5f30ae39b7
fix(hit): fix hit area when parent rotation (#195) 2026-04-15 10:53:47 +03:00
Danila Poyarkov 714390b54b feat: copy node ID / XPath from context menu, CLI selection command
Add nodeToXPath() to core — builds a unique XPath selector for any node
by walking up the tree and using name predicates (or positional index
when names collide among same-type siblings). Properly escapes names
containing quotes using XPath concat() or double-quote delimiters.

UI: "Copy node ID" and "Copy XPath" items in the canvas right-click
"Copy/Paste as" submenu. Also i18n the entire submenu — all labels now
use menuMessages with translations for de, es, fr, it, pl, ru, zh-CN.

CLI: `open-pencil selection` command (app-mode only) returns selected
nodes with id, name, type, size, and xpath. Connects to the running
app via the automation WebSocket.

Automation: new `selection` RPC handler that returns selected nodes
with computed XPath selectors.
2026-04-15 10:09:52 +03:00
Danila Poyarkov 33fc19a593 fix(layout): bump yoga-layout to 3.3.0-grid.3 with Node.free() support
- Upgrade @open-pencil/yoga-layout to include free() and freeRecursive()
- freeYogaTree() in layout.ts now properly releases WASM memory
- FinalizationRegistry remains as a safety net for missed cleanup

Fixes #183
2026-04-13 12:09:45 +03:00
Danila Poyarkov cd4e1a2271 feat(mcp): add stdio transport for Claude Code / Cursor
- Add packages/mcp/src/stdio.ts — proper stdio MCP server that
  connects to the running app via WebSocket
- openpencil-mcp bin now points to stdio entry (was HTTP)
- openpencil-mcp-http bin added for the HTTP server
- Extract registerTools() from server.ts to share between transports
- Move index.ts banner output to stderr (prevents stdout corruption)
- Update Tauri shell spawn to use openpencil-mcp-http
- Update all docs (EN + 6 translations): correct binary names, source
  paths, port number

Fixes #194
2026-04-13 11:51:01 +03:00
Danila Poyarkov 082fc2c88e fix(kiwi): guard alias resolution against missing GUID
Fixes #192 — val.alias can exist as a stub without a guid in real
.fig files, causing guidToString(undefined) to crash on import.
2026-04-13 09:58:06 +03:00
Danila Poyarkov a2a5376b74 refactor(vue): extract useUndoBatch composable and add batch tests
- Move idle-timer batch logic from PropertyListRoot into reusable useUndoBatch composable
- Add unit tests for batch undo behavior (collapse, flush, redo, no-op)
2026-04-13 09:46:09 +03:00
tae.virus c829f69e1b
fix(vue): batch color picker drag updates into one undo entry (#187)
PropertyListRoot's update() and patch() pushed a new undo entry on
every call, so dragging a color slider or color area in the fill,
stroke, or effect pickers filled the undo stack with dozens of
intermediate entries per interaction. Cmd+Z had to be pressed many
times to revert a single conceptual color change.

Wrap update() and patch() in a debounced begin/commit batch keyed by
(op, propKey, index, nodeIds). Rapid events collapse into one undo
entry, and the batch commits after 300ms of idle. Changing prop key,
index, or selection flushes the previous batch immediately, and
add/remove/toggleVisibility flush any pending batch before running so
button actions never mix with drag history. The existing explicit
batch in toggleVisibility for multi-node is preserved.
2026-04-13 09:45:50 +03:00
Danila Poyarkov b3053601f7 Add arrow key nudge and fix undo/redo bugs
Nudge: Arrow keys move selection by 1px, Shift+Arrow by 10px.
Consecutive nudges within 300ms are batched into a single undo
entry so Cmd+Z undoes the entire sequence at once.

Fix duplicate redo creating nodes with new IDs, breaking
subsequent undo entries that reference the original IDs. Now
snapshotSubtree/restoreSubtree preserve node IDs across redo.

Fix delete undo not restoring children — deleteSelected only
snapshotted top-level nodes, so undoing a frame delete lost all
its children. Now snapshots the full subtree.

Fix draw tool not committing resize to undo — shapes created via
draw had their final dimensions lost on redo because handleDrawUp
never recorded the resize. Now uses beginBatch/commitBatch to
combine create+resize into a single undo entry.

Clean up use-keyboard.ts — remove shouldPreventDefault and opaque
PREVENT_* sets, move tool switch and nudge to dedicated keydown
listener, add shift() helper.

Closes #182
2026-04-10 00:26:13 +03:00
Danila Poyarkov a130d9ce1d fix(core): correct version to 0.11.6 in package.json 2026-04-08 22:28:00 +03:00
Danila Poyarkov f8f4af5937 Release v0.11.6 2026-04-08 22:04:22 +03:00
Danila Poyarkov 2c3191cf96 Release v0.11.5 2026-04-08 21:38:44 +03:00
Danila Poyarkov 26a7d2d454 Release v0.11.4 2026-04-08 20:52:09 +03:00
Danila Poyarkov 7e7036b422 fix(mcp): add save_file tool and fix stale dist imports
- Add save_file MCP tool that proxies to the browser app via RPC
- Add save_file RPC handler in the automation server
- Rebuild dist/ to fix stale @open-pencil/core/* self-referencing imports
  from before the domain module restructuring

Fixes #184
Closes #185 (save_file added; open_file requires File System Access API
which is browser-interactive; new_document needs a store reset method)
2026-04-08 20:49:29 +03:00
Danila Poyarkov 92efd70c62 refactor(core): move clipboard text helpers into text/ domain
- Move clipboard-text-outlines.ts → text/opentype.ts
- Move kiwi/serialize-clipboard-v4.ts → text/clipboard-derived-text.ts
- Delete types-opentype.d.ts in favor of @types/opentype.js
- Add opentype.js to core package dependencies
- Add Vite alias for opentype.js ESM resolution
- Update all imports across core, kiwi, clipboard, and tests
2026-04-08 20:45:43 +03:00
Danila Poyarkov a20d170956 Release v0.11.3 2026-04-08 16:06:37 +03:00
Danila Poyarkov 28272ebda0
Add clipboard text v4 export (#164)
* chore: remove unused pi context file

- Delete .pi/context.md
- Remove an unused local context artifact from the repository

* Add clipboard text v4 export

* fix(clipboard): rebase v4 export branch onto master

- Resolve conflicts after the core text module reorganization
- Fix clipboard text export imports and bundled font asset loading
- Update raw binary expectations for textUserLayoutVersion 4
- Re-run check and unit tests after the rebase
2026-04-08 15:35:00 +03:00
Danila Poyarkov e9f8fd352d Use short universal labels for inline sizing suffix
Long localized labels like 'По содержимому' overflow the input.
Add sizingHugShort/sizingFillShort i18n keys with abbreviated
translations for all locales. Dropdown items keep full labels.
2026-04-06 21:25:03 +03:00
Danila Poyarkov 2c5c612e0f Show auto-layout panel for components and instances
The layout section was gated on node.type === 'FRAME', hiding it for
COMPONENT, COMPONENT_SET, and INSTANCE nodes that also support
auto-layout. Also use copyStyleRuns instead of structuredClone per
project lint rules.
2026-04-06 16:05:18 +03:00
Danila Poyarkov 3be01dfdeb Include styleRuns in text edit undo
styleRuns were updated on the graph during editing (via syncText) but
commitTextEdit only captured text changes. Now both text and styleRuns
are snapshotted at startTextEditing and restored on undo.
2026-04-06 15:59:57 +03:00
Danila Poyarkov f77c42d798 Fix text edit not pushing to undo history
commitTextEdit compared prevText from the scene graph node, but
syncText already updates the node on every keystroke during editing.
This made prevText === newText, so the undo entry was never pushed.

Fix: capture the original text when startTextEditing is called and
use that snapshot for the undo comparison.
2026-04-06 15:57:11 +03:00
Danila Poyarkov e949118063 Fix ScrubInput Enter key not committing value
Guard commitEdit against double invocation: Enter triggers commitEdit
which sets editing=false, removing the input via v-if, which fires
blur → commitEdit again. The second call read from a detached input
(empty value → 0), overwriting the correct value.

Fixes #181
2026-04-06 15:25:25 +03:00
Danila Poyarkov ba5e8ce420 fix: check resize handles before rotation zone on mousedown (fixes #170) 2026-04-06 15:04:08 +03:00
Danila Poyarkov cc7f055dfc Format 2026-04-06 14:40:19 +03:00
Danila Poyarkov 07674d1919 Restructure @open-pencil/core into domain modules
Move 30+ loose files at src/ root into domain directories:
- scene-graph/ — SceneGraph class, instances, hit-test, copy, snap, undo
- color/ — parse/format, color management, OkHCL
- text/ — text editor, style runs, direction, fonts
- vector/ — vector network encode/decode, bezier math
- figma-api/ — FigmaAPI class, FigmaNodeProxy
- icons/ — Iconify API client, icon rendering
- canvas/ — SkiaRenderer (was renderer/)
- design-jsx/ — JSX-to-design renderer (was render/)

Also:
- Move fig-compress.ts into io/formats/fig/compress.ts
- Delete re-export shims (headless-render.ts, svg-export/)
- Convert all self-referencing @open-pencil/core/* imports to relative
- Clean up package.json exports (26 subpaths, no internal leaks)
- Update AGENTS.md subpath table

Fixes #179
2026-04-06 14:39:53 +03:00
Danila Poyarkov d73caafc8a Add 14 missing subpath exports to @open-pencil/core
Fixes #179
2026-04-06 14:24:35 +03:00
Danila Poyarkov 1c99e6a9c2
fix(acp): show install errors for missing agent CLIs (#176)
* fix(acp): show install errors for missing agent CLIs (#172)

* Use npm for install commands, toasts for errors, tighten error matching

- Replace bun add -g with npm i -g (universal baseline)
- Replace initError inline banner with toast.show() (consistent with rest of app)
- Remove overly broad 'not found' / 'no such file' from isMissingCommandError

* Refactor toast API to toast.info/warning/error
2026-04-06 14:19:38 +03:00
Danila Poyarkov 5f37515bb6 fix(i18n): sync all locales with English source of truth
- Rename panels.sizingFill → panels.fill for paint fill label (was
  colliding with the sizing fill key) in all 6 locales
- Add 9 missing keys to de, es, fr, it, pl: clipContent, colorFormat*,
  colorHint*, colorPreviewClipped
- Remove duplicate alignLeft/alignRight/alignTop/alignBottom keys from
  ru.json (already covered by the canonical alignment keys)
2026-04-01 21:27:49 +03:00
Danila Poyarkov 943995fb70 Fix MCP auth, collab state, and export bounds 2026-04-01 21:27:04 +03:00
jongwong fd15ab6636
feat(i18n): Add Simplified Chinese (zh-CN) localization support (#167)
* feat(i18n): Add Simplified Chinese (zh-CN) localization support

* feat(i18n): Update Simplified Chinese localization with additional alignment and color formatting terms
2026-04-01 21:26:35 +03:00
Danila Poyarkov d263d63da9 Fix rotated frame overlays and add snapshots 2026-03-31 16:44:35 +03:00
Danila Poyarkov b826202b09 Fix inline rename and rotated hit testing 2026-03-31 16:44:35 +03:00
tae.virus 69784953fb
fix: add missing StackWrap enum and fields to kiwi schema (#166)
The kiwi binary encoder silently drops layoutWrap (flex-wrap) and
counterAxisSpacing (row-gap) because they were missing from the
schema definition. The serialize/deserialize code already handles
these values correctly — only the schema was incomplete.

Adds:
- StackWrap enum (NO_WRAP, WRAP)
- stackWrap field (476) to NodeChange
- stackCounterSpacing field (477) to NodeChange
- STRETCH value (4) to StackAlign enum

Fixes open-pencil/open-pencil#165
2026-03-31 15:47:08 +03:00
Danila Poyarkov f90a376bf2 Fix core release build config 2026-03-30 18:54:18 +03:00
Danila Poyarkov 33d293b221 Release v0.11.2 2026-03-30 18:38:36 +03:00
Danila Poyarkov 9be3514f2b Release v0.11.1 2026-03-30 17:24:00 +03:00
Danila Poyarkov 36bc879421 Fix package publishing pipeline 2026-03-30 17:21:20 +03:00
Danila Poyarkov 911bed3ede Release v0.11.0 2026-03-30 16:48:27 +03:00
Danila Poyarkov 79357f3afe Add Vue package publishing in CI 2026-03-30 16:21:57 +03:00