* fix(ui): anchor AppSelect dropdown to its trigger AppSelect wraps its SelectTrigger inside <Tip> (a TooltipTrigger as-child). reka-ui's Select popper captures its trigger element via useForwardExpose on mount, but that capture resolves to null when the trigger sits inside another primitive's as-child slot. With no anchor, floating-ui positioned the menu at the viewport origin and flipped it off-screen (~y:-412), so clicking the dropdown appeared to do nothing. Wrap the trigger in a layout-neutral span, keep the styled <Tip> tooltip, and pass that span to SelectContent's `reference` prop so the popper anchors explicitly on every open (PopperContent prefers props.reference over the broken auto-capture, and re-reads it on each open/reopen). Fixes every dropdown built on AppSelect across the inspector (export scale/format, typography, effects, stroke, flex/grid layout, variants, fills, gradients, color format). Verified live: menu opens directly below the trigger, fully on-screen, across repeated open/select/reopen cycles; tooltip still shows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(export): editable custom scale with 1024x cap The export scale was a preset-only dropdown (0.5x–4x). Make it Figma-like: an editable field where you can type any multiplier (e.g. 9x, 1.5x) in addition to picking a preset from the chevron menu. Custom values are used as-is and are never added to the preset list. Typed input is clamped to [0.01x, 1024x] — an unbounded multiplier would allocate an enormous canvas and crash the renderer. The clamp lives in the export data model (clampExportScale, applied in updateScale) so it defends every caller, and is reused by the input for immediate display feedback (9999999 -> 1024x, 0.0000001 -> 0.01x). Invalid/zero input reverts. New ExportScaleInput.vue pairs a text input with a reka DropdownMenu for presets (mirrors ZoomDropdown; not wrapped in <Tip>, so it positions correctly). The active preset shows a checkmark. Verified live across custom entry, clamping (both bounds), decimals, invalid input, and preset selection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(export): store export settings per node Export settings were global and transient: every selection showed a default 1x PNG row, and nothing was remembered per layer or persisted with the document. Store export settings on each SceneNode (exportSettings: ExportSetting[]), defaulting to empty so the panel shows only its header and add button until the user adds a row. Settings apply across multi-select and target the current page when nothing is selected; add/edit/remove are undoable. Persist settings with the document via open-pencil pluginData in .fig (lossless, including webp). On import, prefer app pluginData; otherwise map native Figma exportSettings (PNG/JPEG/SVG/PDF + content scale) without overwriting raw native fields on re-export. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(export): working format dropdown, JPG/WEBP export, and zip bundling - Anchor AppSelect dropdown via native title so the format selector is clickable in multi-row export panels (was rendering off-screen) - Add a browser-canvas encode fallback on the renderer for JPG/WEBP, which CanvasKit's encodeToBytes returns null for in this build (fixes the "Nothing to export" error) - Bundle multi-format exports into a single zip; a single export still downloads the file directly - Default each added export row to 2x the previous scale (1x -> 2x -> 4x) - Add e2e coverage for the multi-row dropdown, zip bundling, and direct single-file download Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * revert(export): drop AppSelect native-title workaround, superseded by #325 The shared <Tip> path is now handled generally by #325; #321 should not carry the AppSelect change. Reverts src/components/ui/AppSelect.vue to master. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(export): address review feedback (#321) - Clear raw native exportSettings when the user edits/clears export rows so they don't resurrect from the import fallback on reopen (scene-graph/source-metadata). - Clamp export scale at the .fig import boundary (plugin + native CONTENT_SCALE), not just in the UI; centralize the bounds in core/scene-graph/export-scale and reuse them from the vue helpers (single source of truth). - Export the rows the panel shows (activeSettings) for every target so a multi-selection is WYSIWYG — no hidden rows export, and the "mixed" notice shows whenever targets diverge. - Sanitize zip entry names (strip separators, parent refs, control chars) so layer names can't escape or corrupt the archive. - Verify toDataURL() honored the requested MIME in the JPEG/WEBP fallback; reject a silent PNG so we never write PNG bytes under a .jpg/.webp extension. Add regression tests for the native-settings clear and import-scale clamp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .github | ||
| .vscode | ||
| desktop | ||
| lint | ||
| packages | ||
| public | ||
| scripts | ||
| src | ||
| tests | ||
| vite | ||
| .gitattributes | ||
| .gitignore | ||
| .lfsconfig | ||
| .oxfmtrc.json | ||
| AGENTS.md | ||
| bun.lock | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| index.html | ||
| LICENSE | ||
| oxlint.json | ||
| package.json | ||
| playwright.config.ts | ||
| README.md | ||
| SECURITY.md | ||
| steiger.config.ts | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
OpenPencil
Open-source design editor. Opens .fig and .pen design files, includes built-in AI, and ships as a programmable toolkit with a headless Vue SDK for building custom editors.
Status: Active development. Not ready for production use.
Note: There is another open-source project with the same name — OpenPencil by ZSeven-W, focused on AI-native design-to-code workflows. This project focuses on Figma-compatible visual design with real-time collaboration.
Try it online → · Download · Documentation · llms.txt
Installation
macOS (Homebrew):
brew install openpencil
Or download from the releases page, or use the web app — no install needed.
What it does
- Opens
.figand.penfiles — read and write native Figma files, open supported Pencil documents from the app or OS file browser, copy & paste nodes between apps - AI builds designs — describe what you want in chat, 90+ tools create and modify nodes. Connect OpenRouter, Anthropic, OpenAI, Google AI, Z.ai, MiniMax, or compatible endpoints
- Fully programmable — headless CLI, XPath queries, Figma Plugin API via
eval, MCP server for AI agents, and desktop agent integrations for Claude Code, Codex, and Gemini CLI - Lint, convert, and extract tokens — inspect documents, lint naming/layout/accessibility, convert between supported formats, analyze colors/typography/spacing/clusters, and extract design tokens
- Components and variants — create reusable components, group variants into component sets, insert local assets as instances, and switch variants from the inspector
- Design-to-code export — export selections as JSX/Tailwind, generate token outputs, and map designs into component-oriented code workflows
- Vue SDK for custom editors — headless components and composables for embedding OpenPencil into other apps or building workflow-specific editing surfaces. Read the SDK docs →
- Real-time collaboration — P2P via WebRTC, no server, no account. Cursors, presence, follow mode
- Auto layout & CSS Grid — flex and grid layout via Yoga WASM, with gap, padding, alignment, track sizing
- ~7 MB desktop app — Tauri v2 for macOS, Windows, Linux. Also runs in the browser as a PWA
CLI
npm install -g @open-pencil/cli
# or: bun add -g @open-pencil/cli
Inspect design files
Browse node trees, search by name or type, dig into properties — all without opening the editor:
openpencil tree design.fig
openpencil find design.pen --type TEXT
openpencil node design.fig --id 1:23
openpencil info design.fig
[0] [page] "Getting started" (0:46566)
[0] [section] "" (0:46567)
[0] [frame] "Body" (0:46568)
[0] [frame] "Introduction" (0:46569)
[0] [frame] "Introduction Card" (0:46570)
[0] [frame] "Guidance" (0:46571)
Query with XPath
Use XPath selectors to find nodes by type, attributes, and structure:
openpencil query design.fig "//FRAME" # All frames
openpencil query design.fig "//FRAME[@width < 300]" # Frames under 300px
openpencil query design.fig "//TEXT[contains(@name, 'Button')]" # Text with 'Button' in name
openpencil query design.fig "//*[@cornerRadius > 0]" # Rounded corners
openpencil query design.fig "//SECTION//TEXT" # Text inside sections
Export
Render to PNG, JPG, WEBP, SVG, .fig, or JSX — or export selections/pages as .fig and convert whole documents between supported formats:
openpencil export design.fig # PNG
openpencil export design.fig -f jpg -s 2 -q 90 # JPG at 2x, quality 90
openpencil export design.fig -f fig --page "Page 1" # Export a page as .fig
openpencil export design.fig -f jsx --style tailwind # Tailwind JSX
openpencil convert design.pen output.fig # Convert between document formats
<div className="flex flex-col gap-4 p-6 bg-white rounded-xl">
<p className="text-2xl font-bold text-[#1D1B20]">Card Title</p>
<p className="text-sm text-[#49454F]">Description text</p>
</div>
Lint design files
Catch naming, layout, structure, and accessibility issues from the terminal:
openpencil lint design.fig
openpencil lint design.pen --preset strict
openpencil lint design.fig --rule color-contrast
openpencil lint design.fig --list-rules
Analyze and extract design tokens
Audit an entire design system from the terminal — find inconsistencies, extract the real palette, and spot components waiting to be extracted:
openpencil analyze colors design.fig
openpencil analyze typography design.fig
openpencil analyze spacing design.fig
openpencil analyze clusters design.fig
openpencil variables design.fig
#1d1b20 ██████████████████████████████ 17155×
#49454f ██████████████████████████████ 9814×
#ffffff ██████████████████████████████ 8620×
#6750a4 ██████████████████████████████ 3967×
3771× frame "container" (100% match)
size: 40×40, structure: Frame > [Frame]
2982× instance "Checkboxes" (100% match)
size: 48×48, structure: Instance > [Frame]
Script with Figma Plugin API
eval gives you the full Figma Plugin API. Modify the file, write it back:
openpencil eval design.fig -c "figma.currentPage.children.length"
openpencil eval design.fig -c "figma.currentPage.selection.forEach(n => n.opacity = 0.5)" -w
Control the running app
When the desktop app is running, omit the file argument — the CLI connects via RPC and operates on the live canvas. Useful for automation scripts, CI pipelines, or AI agents that need to interact with the editor:
openpencil tree # Inspect the live document
openpencil export -f png # Screenshot the current canvas
openpencil eval -c "figma.currentPage.name" # Query the editor
All commands support --json for machine-readable output.
AI & MCP
Built-in chat
Press ⌘J to open the AI assistant. It has 100+ tools that can create shapes, set fills and strokes, manage auto-layout, work with components and variables, run boolean operations, analyze design tokens, and export assets. Bring your own API key for OpenRouter, Anthropic, OpenAI, Google AI, Z.ai, MiniMax, or compatible endpoints. No backend, no account.
Coding agents (desktop)
Use Claude Code, Codex, or Gemini CLI directly in the chat panel. The agent connects to the editor's MCP server and uses all 100+ design tools. Requires the desktop app and the agent CLI installed locally.
Setup (Claude Code):
- Install the ACP adapter:
npm install -g @agentclientprotocol/claude-agent-acp - Add MCP permission to
~/.claude/settings.json:{ "permissions": { "allow": ["mcp__open-pencil__*"] } } - Open the desktop app → CtrlJ → select Claude Code from the provider dropdown
MCP server
Connect Claude Code, Cursor, Windsurf, or any MCP client to inspect, modify, and export design documents headlessly. 100+ tools. Full docs →
Stdio (Claude Code, Cursor, Windsurf):
npm install -g @open-pencil/mcp
claude mcp add --scope user open-pencil -- openpencil-mcp
For other MCP clients:
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
HTTP (scripts, CI):
openpencil-mcp-http # http://localhost:3100/mcp
File access: Set OPENPENCIL_MCP_ROOT to scope file operations (open_file, new_document, export path param) to a directory. Defaults to the current working directory.
AI agent skill
Teach your AI coding agent to use OpenPencil — inspect designs, export assets, analyze tokens, modify .fig files:
npx skills add open-pencil/skills@open-pencil
Works with Claude Code, Cursor, Windsurf, Codex, and any agent that supports skills.
For documentation-aware agents, the docs site publishes llms.txt, llms-full.txt, and per-page Markdown files generated from the VitePress docs.
Collaboration
Share a link to co-edit in real time. No server, no account — peers connect directly via WebRTC.
- Click the share button in the top-right panel
- Share the generated link (
app.openpencil.dev/share/<room-id>) - Collaborators see your cursor, selection, and edits in real time
- Click a peer's avatar to follow their viewport
Why
Figma is a closed platform that actively fights programmatic access. Their MCP server is read-only. figma-use added full read/write automation via CDP — then Figma 126 killed CDP. Your design files are in a proprietary binary format that only their software can fully read. Your workflows break when they decide to ship a point release.
OpenPencil is the alternative: open source (MIT), reads .fig files natively, every operation is scriptable, and your data never leaves your machine.
See the roadmap for product direction and current Figma compatibility gaps.
Contributing
Setup
bun install
bun run dev # Dev server at localhost:1420
bun run tauri dev # Desktop app (requires Rust)
Quality gates
| Command | Description |
|---|---|
bun run check |
Lint + typecheck |
bun run test |
E2E visual regression |
bun run test:unit |
Unit tests |
bun run format |
Code formatting |
Project structure
packages/
core/ @open-pencil/core — engine (scene graph, renderer, layout, file formats, tools)
vue/ @open-pencil/vue — headless Vue SDK
cli/ @open-pencil/cli — headless CLI
mcp/ @open-pencil/mcp — MCP server (stdio + HTTP)
docs/ Documentation site (openpencil.dev)
src/ Vue app (components, composables, stores)
desktop/ Tauri v2 (Rust + config)
tests/ E2E (188 tests) + unit (764 tests)
Tech stack
| Layer | Tech |
|---|---|
| Rendering | Skia (CanvasKit WASM) |
| Layout | Yoga WASM (flex + grid via fork) |
| UI | Vue 3, Reka UI, Tailwind CSS 4 |
| File format | Kiwi binary + Zstd + ZIP |
| Collaboration | Trystero (WebRTC P2P) + Yjs (CRDT) |
| Desktop | Tauri v2 |
| AI/MCP | Multi-provider (Anthropic, OpenAI, Google AI, OpenRouter), MCP SDK, Hono |
Desktop builds
Requires Rust and platform-specific prerequisites (Tauri v2 guide).
bun run tauri build
Acknowledgments
Thanks to @sld0Ant (Anton Soldatov) for creating and maintaining the documentation site.
License
MIT
