The selection size pill was drawn in local node space after
canvas.concat(worldMatrix * viewMatrix), causing it to scale
with the canvas zoom. Now computed in screen coordinates like
the multi-selection badge.
Non-Figma clipboard content (plain text, images) matched the figmeta
regex but failed at decodeMessage, flooding the console with unhandled
promise rejections on every paste.
Auto-generated release notes from CHANGELOG.md are often wrong.
Create as draft so the changelog section can be reviewed and
pasted manually before publishing.
navigator.userAgent may not contain 'Windows' in all WebView2
contexts. navigator.platform.includes('Win') is more reliable.
Verified working on Windows x64 VM — MCP server spawns via
cmd /c and connects successfully.
IS_TAURI is evaluated at module load time and may be false if
__TAURI_INTERNALS__ isn't injected yet. This prevented the
automation WebSocket from connecting on Windows and some Linux
configurations.
- Windows: npm installs .cmd wrappers, not direct executables. Spawn
via 'cmd /c openpencil-mcp-http' on Windows to resolve .cmd files.
- macOS/Linux: add fix-path-env-rs to inherit shell PATH in GUI apps.
- Add 'cmd' to shell spawn capabilities for Windows.
Fixes#226
macOS GUI apps don't inherit shell PATH from .bashrc/.zshrc, so
openpencil-mcp-http installed via bun/npm/nvm couldn't be found.
Added fix-path-env-rs which reads /etc/paths and shell config
to populate PATH before spawning child processes.
Fixes#226
- Export getAbsolutePositionFull from @open-pencil/core/canvas subpath
- Update vue import from @open-pencil/core/canvas/coordinate to @open-pencil/core/canvas
- Formatting fixes from oxfmt in pen/convert.ts, pen/read.ts, figma-factory.ts
- Update tool count from 90 to 100+
- Document OPENPENCIL_MCP_ROOT env var for file path scoping
- Document MCP-only tools, export path param, and prompt markdown files
in AGENTS.md
- Move CODEGEN_PROMPT to codegen.md, loaded via raw-md bundler plugin
- Add JSX_REFERENCE as jsx-reference.md with full prop/tag/example docs
- Copy JSX Reference button (book icon) in Code panel header
- Multi-root JSX: try parsing as-is, wrap in fragment on failure
- Component and Instance tag aliases in JSX renderer
- renderJSX returns RenderResult[] to support fragments
- raw-md plugin for both tsdown and Vite
Co-authored-by: sld0Ant <sld0Ant@users.noreply.github.com>
- open_file: opens a .fig/.pen from disk into a new tab via MCP
- new_document: creates an empty doc with optional save path
- export_image, export_svg, get_jsx: optional path param writes output
to disk instead of returning base64/string (avoids flooding AI context)
- OPENPENCIL_MCP_ROOT env var scopes all file paths (defaults to cwd)
- setPlannedFilePath + startWatchingCurrentFile on editor store
- Extract openFileFromPath helper in use-menu.ts
- Add fs:allow-mkdir and scoped fs:allow-watch Tauri capabilities
- Better 'app not connected' error in stdio — instructs agent to stop
Co-authored-by: Jais Pedersen <jais@pedersens.net>
The Tip (Tooltip) wrapper around PopoverTrigger caused floating-ui
to miscalculate the popover position as translate(0px, -200%),
rendering it off-screen. Replaced with a plain title attribute
for the hover hint.
Fixes#218
- Use applyStyleToRange from text/style-runs.ts instead of naively
appending runs (which created overlaps and invalid state)
- Apply fontWeight from style name (e.g. 'Bold' → 700)
- Apply color as fills on the style override
- Previously the color param was accepted but silently ignored
Fixes#214
Previously the error was silently caught with console.error, leaving
Windows users with no indication why the automation bridge was missing.
Now shows a visible toast with install instructions.
tsgo with `include: ["src"]` sometimes skips standalone entry points
like stdio.ts that aren't imported by other files. This caused the
published npm package to be missing dist/stdio.js, breaking the
`openpencil-mcp` stdio binary.
Fixes#224 (npm publish issue)
JSX renderer: support position="absolute", top, and left props for
placing children inside auto-layout containers without breaking flow.
MCP server: send notifications/tools/list_changed when the desktop
app connects or disconnects, so MCP clients can refresh their tool
list instead of discovering disconnection per-call.
Addresses #208 (findings 2 and 5)
MCP clients often serialize numeric arguments as JSON strings.
Previously this caused validation errors on tools like node_move.
Now both the MCP server (zod) and AI adapter (valibot) coerce
string-encoded numbers to numbers, rejecting only genuinely
non-numeric values like 'abc'.
Fixes#207
create_vector now validates the path JSON before accepting it:
checks vertices have numeric coordinates and segment indices are
in range. Returns a clear error message instead of silently
creating a malformed node that crashes on save.
The scene graph also normalizes vectorNetwork on updateNode as
a safety net for other code paths.
Vectors created via MCP with straight-line segments (no tangent
handles) crashed the .fig serializer on save. Segments without
tangentStart/tangentEnd are now normalized to zero vectors at
creation time, producing straight line segments as intended.
Fixes#206
When right-clicking a component, the context menu now shows
'Create Instance' instead of 'Create Component'. Previously both
were shown simultaneously.
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
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.
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>
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
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>
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).
* 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>
* 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)
* 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>