Commit graph

537 commits

Author SHA1 Message Date
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
Danila Poyarkov 776d8969d4 fix(app): support cmd/ctrl+click for layers panel multi-select
Previously only Shift+click toggled additive selection in the layers
panel. Cmd+click (macOS) and Ctrl+click (Windows/Linux) had no effect
beyond replacing the selection with a single layer.

Treat Shift, Cmd, and Ctrl modifiers equally as additive toggle, matching
common design tool conventions (Figma, Sketch).

Co-authored-by: TKman <102001532+greekr4@users.noreply.github.com>
2026-04-22 13:28:25 +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
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 364f25f23e fix(app): suppress keyboard shortcuts while editing property panel inputs
useMagicKeys tracks key state globally, so plain/shift/space shortcuts
fired even when an <input> or <textarea> was focused. This caused
Backspace to delete selected nodes while editing values in the
property panel.

Track active element reactively via useActiveElement and gate plain(),
shift(), and spaceHeld computeds on !inputFocused.

Closes #200
2026-04-15 13:56:01 +03:00
Danila Poyarkov 1109c7498a chore: format 2026-04-15 13:45:40 +03:00
Danila Poyarkov 728274f4a7 fix: tooltip competing with popover trigger on Windows
Add disabled prop to Tip — forces tooltip closed when the parent popover
is open. Fixes the settings gear and variable-bind popovers not opening
on Windows WebView2 due to TooltipTrigger intercepting pointer events.

Closes #171
Co-authored-by: Anton Soldatov <sld0Ant@users.noreply.github.com>
2026-04-15 13:45:35 +03:00
Danila Poyarkov 837c09bd78 fix(tauri): ATS exception for localhost WebSocket on macOS
Allow insecure localhost connections via NSAllowsLocalNetworking so the
WebSocket bridge works in release builds on macOS.

Also add debug logging for WebSocket and MCP health check failures.

Closes #190
Co-authored-by: Anton Soldatov <sld0Ant@users.noreply.github.com>
2026-04-15 13:16:29 +03:00
Danila Poyarkov 82c390c2d2 refactor(tauri): clean up external link helper
- Move use-external-link.ts → utils/external-link.ts (plain function, not a composable)
- Remove non-null assertions on providerDef.keyURL (v-if already guards)
- Remove test scaffolding from production global.d.ts
2026-04-15 11:03:31 +03:00
Anton Soldatov 7b5a5bdeef
fix(tauri): use opener plugin for external links instead of shell (#197)
External links (<a target="_blank">) in the AI panel triggered
"Command plugin:shell|open not allowed by ACL" on Tauri desktop.

Root cause: Tauri v2 intercepts <a target="_blank"> via the shell plugin,
but only the opener plugin had ACL permissions. Replace all external links
with programmatic openUrl() from @tauri-apps/plugin-opener, which is already
whitelisted via "opener:default" in capabilities.

- Extract openExternalLink() composable for Tauri/browser switch
- Replace <a target="_blank"> with <button @click> in ProviderSetup/Settings
- Add E2E test verifying window.open is called in browser context

Fixes #193

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-04-15 11:02:00 +03:00
jongwong 5f30ae39b7
fix(hit): fix hit area when parent rotation (#195) 2026-04-15 10:53:47 +03:00
Danila Poyarkov 4b091f5936 fix(toast): restart dismiss timer on duplicate error
Bumping the id re-keys the ToastRoot component, so reka-ui remounts it
and restarts the auto-dismiss countdown. Without this, a duplicate error
arriving at t=9s would vanish 1s later despite the fresh ×N badge.
2026-04-15 10:28:37 +03:00
Chris Gliddon 2b4b8c0f63
fix(toast): error toasts auto-dismiss + dedupe + cap stack at 5 (#209)
Error toasts had `duration: 0` which in reka-ui means "never auto-
dismiss." Combined with the global `window.error` /
`unhandledrejection` handler in `setupGlobalErrorHandler`, any error-
emitting source — even a transient one called repeatedly per frame
(wheel events, render-loop callbacks) — could pile up over the
canvas with no way to recover except a hard-restart.

Three small changes that compose:

- Errors now use ERROR_TOAST_DURATION (10s) instead of 0. Long
  enough to read; short enough to self-clean.
- Dedupe by (message, variant): repeating the same error increments
  a count badge ("×3") on the existing toast instead of stacking
  duplicates. Prevents the cascade-on-every-frame failure mode.
- Hard cap at 5 stacked toasts; oldest drop off when a 6th would
  arrive. Belt-and-suspenders against any source we missed.

The Toast interface gains a non-optional `count: number` (defaults
to 1). External callers (info/warning/error) are unaffected — they
go through the new internal `push()` which sets it.
2026-04-15 10:27:32 +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 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 a4f30325eb refactor(app): move tool switch and space-hold to useMagicKeys
Only raw handler left is nudge (needs keydown repeat events).
Tool shortcuts now use whenever(plain(code)) loop over TOOL_SHORTCUTS.
Space hold uses watch(spaceHeld) on a computed reactive boolean.
2026-04-10 00:51:15 +03:00
Danila Poyarkov 6d1eec8ad0 refactor(app): extract named actions from keyboard shortcuts
Extract inline callbacks into named functions (smartDelete,
confirmOrEnterText, escapeOrDeselect, toggleAutoLayout, toggleUI,
toggleAI, exportSelectionPng) so the shortcut mapping section is
a clean key→action table with no branching logic.
2026-04-10 00:45:17 +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 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 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 862c0f28dc Unify UI styling to slot-based useXxxUI() pattern
- Convert button, input, icon-button, toast, surface from flat-string
  functions to slot-based { base: string } return pattern
- Convert section from bare TV exports to useSectionUI() with
  { wrapper, label } slots
- Remove standalone select helpers (selectTrigger, selectContent,
  selectItem) in favor of useSelectUI()
- Delete use-component-ui.ts — no longer needed
- Update all 20+ call sites across property panels and chat components
2026-04-06 22:58:34 +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 467a2e1dc1 Unify size input with inline sizing dropdown
Replace the separate ScrubInput + AppSelect pair with a single
combined control: the sizing mode (Hug/Fill) shows as an inline
label inside the scrub input, swapping to a chevron on hover.

- ScrubInput: add group class and #suffix slot (stays generic)
- LayoutSection: embed reka-ui Select in the suffix slot with
  group-hover toggle between label and chevron
2026-04-06 19:52:27 +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 0287808b87 Use static icon components in layout direction buttons
Dynamic component :is with template literals aren't resolved by
unplugin-icons, so arrow-right/arrow-down/layout-grid were missing.
2026-04-06 15:50:57 +03:00
Danila Poyarkov fcee3117e1 Use FRAME instead of RECTANGLE for gradient cards with children
RECTANGLE is not a container type, so hit-testing and double-click
never recurse into its children. This prevented selecting the text
labels inside gradient cards by double-clicking on the canvas.
2026-04-06 15:42:28 +03:00
Danila Poyarkov b818ddeff0 Enter key opens text editing with select-all on TEXT nodes
Matches Figma behavior: pressing Enter when a single TEXT node is
selected starts text editing mode and selects all text. Uses rAF
to defer text editing start so the Enter keydown event completes
before the textarea is created and focused, preventing the newline
from being inserted into the selected text.

Closes #181
2026-04-06 15:40:55 +03:00
Danila Poyarkov cc7f055dfc Format 2026-04-06 14:40:19 +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 943995fb70 Fix MCP auth, collab state, and export bounds 2026-04-01 21:27:04 +03:00
Danila Poyarkov b826202b09 Fix inline rename and rotated hit testing 2026-03-31 16:44:35 +03:00
Danila Poyarkov fd0124b6f8 Add Arabic and RTL support
# Conflicts:
#	packages/core/src/renderer/text.ts
2026-03-29 21:06:29 +03:00
Danila Poyarkov 25e656d68c Add zoom dropdown 2026-03-29 19:46:21 +03:00
Danila Poyarkov 581abf3b93 Fix export panel target handling 2026-03-29 19:16:45 +03:00
Danila Poyarkov d0d6ba9def Move reusable color picker logic into Vue SDK 2026-03-29 18:54:14 +03:00
Danila Poyarkov 1f11392da6 Refactor color picker field formats 2026-03-29 18:46:55 +03:00
Mahmoud Almontasser 9ed102b706 feat: add Arabic and RTL support 2026-03-29 11:42:30 +02:00
Danila Poyarkov 04c4c208e7 Add OkHCL color picker support 2026-03-28 20:33:35 +03:00
Danila Poyarkov 7d1e7cb86e Update Z.ai and MiniMax model lists
# Conflicts:
#	CHANGELOG.md
2026-03-28 15:29:22 +03:00
Danila Poyarkov 76767c5ea3 Update Z.ai and MiniMax model lists 2026-03-28 15:26:37 +03:00
Danila Poyarkov e396df3224 Add .pen document import
Co-authored-by: Anton Soldatov <eddclyde@yandex.ru>
2026-03-28 13:57:32 +03:00
Danila Poyarkov 0001802dda Add shared IO format registry 2026-03-28 01:58:05 +03:00
Ilya Nikitin cbae1eaccf lint:
- Add parentheses in bezier-math.ts ternary conditions
- Add parentheses in editor.ts align calculation expression
2026-03-27 21:44:59 +05:00
Ilya Nikitin 0ff5a87ccb update pen tool space drag spec
Adds a new scenario to the pen tool specifications defining the behavior for relocating the active vertex with spacebar during a curve drag, including asserting that the cursor remains a crosshair instead of switching to the hand tool.

Made-with: Cursor
2026-03-27 21:27:45 +05:00
Ilya Nikitin 455f04d6f1 Merge branch 'up_master' into feat/vector-editor
# Conflicts:
#	src/composables/use-keyboard.ts
2026-03-27 19:33:07 +05:00
Ilya Nikitin cedd88813b Use KeyboardEvent.code instead of KeyboardEvent.key across editor and input handling. 2026-03-27 17:13:35 +03:00
Ilya Nikitin ae2fbccf01 fix(components): update Lucide icon names in properties sections
Ensure proper semantic alignment between icon names and their visual representation
2026-03-27 17:09:34 +03:00
Ilya Nikitin e393ca7a07 fix(properties): update Lucide icon names for alignment and flip controls
Ensure proper semantic alignment between icon names and their visual representation
2026-03-27 17:09:02 +03:00
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