* feat(cli): export components as Storybook stories Add `openpencil export -f storybook`, which writes one CSF3 `.stories.ts` file per component set or component. Each variant becomes a story and the variant properties become select controls, so the story renders the matching variant; an unknown combination throws instead of showing another variant. Stories embed the existing inline-style HTML projection, so consumers need no OpenPencil runtime. `--framework react|vue|html` only changes the render wrapper and the Meta/StoryObj import. When the document sits under the current directory, stories carry an `openpencil://` design link for @storybook/addon-designs. Refs #727 * fix(pen): size auto-width text from its content on import Text without a width in an auto-layout parent was imported 10000px wide, a placeholder the app's text measurer replaces. Headless layout keeps stored sizes, so CLI HTML and Storybook exports stretched hugging frames to over 10000px. Import the width as 0 so the importer's existing text-length estimate applies, and headless layout estimates the rest. * feat(app): follow layer links to other pages openpencil:// and web ?node= links only searched the current page, so a Storybook story linking to a component on another page reported it missing. When the current page has no match, load the other pages without showing them and switch to the first that carries the name. * feat(cli): add design images and watch mode to Storybook export Each story now links to its own variant when the layer name is unique, and carries a 2x PNG of the variant for @storybook/addon-designs, imported so Vite bundles it. --watch re-exports on every save. Re-exports replace the stories a previous export of the same document generated, including those of deleted components, and refuse to overwrite hand-written stories or another document's. Refs #727 * fix(cli): reference Storybook design images without ambient PNG types Import design images with new URL(..., import.meta.url) instead of an import declaration, so consumers need no vite/client types to typecheck the stories. Document that exports should run from the same directory. * fix(app): search other pages for a link without cancelling page switches The cross-page layer search prepared each page with preparePage, which advances the page-switch generation, so a page switch the user had in progress could be dropped, and every searched page paid for fonts and layout. Add loadPageNodes, which populates a page's layers through the same worker path without touching the switch generation, and report a failed search as an error instead of a missing layer. * fix(pen): never import width-less text zero wide Text without a width now imports at width 0 and relies on the importer's text-length estimate, which skipped single-glyph text. Estimate zero-width text of any length. * fix(cli): harden Storybook export ownership, titles, and links - A --page export replaces only its own stories, and names files as a full export does, so it cannot delete or overwrite other pages' stories. - Same-named components on a page get distinct titles, so Storybook story ids do not collide. - Read the generated header through CRLF line endings, and refuse a source containing a line break, which would end the header comment and start code. - Link a story only to a layer name no other layer carries. - Document the --page default for Storybook export. Refs #727 * fix(app): let a page switch overtake a link's layer search A link search that loads other pages could resume after the user started switching pages and move them to the matching page. Expose pageSwitchCount, which advances whenever a page switch starts, and abandon the search when it changes. An overtaken search reports neither a match nor a missing layer. * fix(pen): estimate only omitted text widths Estimate a width-less text node's width when it is imported, instead of estimating every zero-width text node afterwards, so an explicit width of 0 is kept. * fix(cli): track Storybook story ownership by document path and page - Identify the document by its path relative to the output directory rather than a basename or cwd-relative path, so same-named documents do not share stories and the export no longer depends on the working directory. - Record the page in each story's header; a --page export replaces all of that page's stories and asks for a full export when renumbered file names land on another page's. - Check every target, including design images, before removing anything, and refuse to overwrite files this export does not own. - Quote the header fields as JSON with U+2028/U+2029 escaped, so any path stays inside the comment, instead of refusing line breaks. - Deduplicate titles by Storybook id, which ignores case and punctuation. Refs #727 * fix(app): focus a searched page only after its switch committed A page switch the user starts while the link search's own switch is pending can keep that switch from committing. Check that the search's switch was the only one and landed on its page before focusing; otherwise report the search as superseded. * fix(pen): keep empty text without a width at zero * fix(cli): remove only the design images a Storybook export generated Replacing a story removed its whole .design folder, including files someone else put there. Read the images each owned story references, remove just those, and remove a .design folder only once it is empty. Refs #727 * test(app): cover a page switch still pending during a link search The previous test committed the overtaking switch, so the page check alone caught it. Advance the switch count without committing, so the test fails without the count check. * fix(cli): stage Storybook exports and refuse linked design folders - Write every file to a staging folder inside the output before removing the previous export, then move them into place, so a failed write no longer leaves the export half replaced. - Refuse a .design path that is not a real folder, such as a symbolic link, before removing or writing images through it, so an export cannot reach outside the output directory. Refs #727 * refactor(dom-css): print Storybook stories from a parsed template Story modules were assembled from string fragments, so quoting and layout were an implicit contract: the CLI found design images with a regex that only matched double-quoted `new URL("…")` paths. A story module is now one TypeScript template, parsed once with acorn and its TypeScript plugin. Data is filled into `$placeholder` nodes and the module is printed with esrap, which owns quoting and escaping. The CLI reads referenced design images back through `storyImagePaths()` instead of matching text. Tests import generated modules and assert values rather than formatting. * refactor(storybook): track generated files in a manifest The export recovered which files it owned by parsing its own output: a header regex over JSON-quoted strings, line-separator escaping, CRLF handling, an AST walk for design images, and a path regex in the CLI. A `.openpencil-stories.json` manifest now records the document and page behind each generated file. The CLI validates it with Valibot, including that every listed path stays inside the output folder, and the story header is a plain note. Story ids use a copy of Storybook's `sanitize`, tested against the installed Storybook; the previous rule treated `A§B` and `A-B` as the same story. Export names use es-toolkit's `pascalCase`. The CLI export command moves into `commands/export/`, dom-css splits grouping and naming out of the Storybook exporter, and the CLI takes the framework list from dom-css. * fix(pen): keep explicit narrow text widths A post-import pass widened every multi-character text narrower than two font sizes, including widths the `.pen` file set on purpose, such as `width: 0`. Omitted widths are now estimated when the text node is created, so the pass only overrode explicit widths and is removed. --------- Co-authored-by: Danila Poyarkov <dev@dannote.net>
295 lines
14 KiB
Markdown
295 lines
14 KiB
Markdown
# OpenPencil
|
||
|
||
[](LICENSE)
|
||
[](https://www.npmjs.com/package/@open-pencil/cli)
|
||
[](https://discord.gg/4wXc9fuZfm)
|
||
[](https://github.com/open-pencil/open-pencil/discussions)
|
||
|
||
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. Usable today, with some rough edges as features evolve.
|
||
|
||
**[Try it online →](https://app.openpencil.dev/demo)** · [Download](https://github.com/open-pencil/open-pencil/releases/latest) · [Documentation](https://openpencil.dev) · [Roadmap](https://openpencil.dev/development/roadmap) · [llms.txt](https://openpencil.dev/llms.txt)
|
||
|
||

|
||
|
||
## Installation
|
||
|
||
**macOS (Homebrew):**
|
||
|
||
```sh
|
||
brew install --cask openpencil
|
||
```
|
||
|
||
Or download from the [releases page](https://github.com/open-pencil/open-pencil/releases/latest), or [use the web app](https://app.openpencil.dev) — no install needed.
|
||
|
||
Requires macOS 13 or later with current Safari updates, Windows 10 or later, or Linux with WebKitGTK 2.40+; the web app needs Chrome 111, Edge 111, Firefox 128, or Safari 16.4 or later. See [system requirements](https://openpencil.dev/getting-started#system-requirements).
|
||
|
||
## What it does
|
||
|
||
- **Opens `.fig` and `.pen` files** — 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, 100+ tools create and modify nodes. Connect OpenRouter, Anthropic, OpenAI, Google AI, DeepSeek, 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
|
||
- **Image vectorization** — convert image layers into editable vector layers with Recraft or fal.ai
|
||
- **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 →](https://openpencil.dev/programmable/sdk/)
|
||
- **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
|
||
- **~15 MB desktop app** — Tauri v2 for macOS, Windows, Linux. Also runs in the browser as a PWA
|
||
|
||
## CLI
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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, PDF, PPTX, HTML, JSX, Storybook stories, or `.fig` — or export selections/pages as `.fig` and convert whole documents between supported formats:
|
||
|
||
```sh
|
||
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 export design.fig -f html --css tailwind # Tailwind HTML fragment
|
||
openpencil export design.fig -f html --html standalone --assets external # HTML + assets
|
||
openpencil export design.fig -f storybook --framework vue # Storybook stories per component
|
||
openpencil convert design.pen output.fig # Convert between document formats
|
||
openpencil import page.html --css styles.css -o page.fig # HTML/CSS → editable .fig
|
||
```
|
||
|
||
DOM/CSS input flows through `@open-pencil/dom-css`, so HTML, authored CSS, and Tailwind utility CSS can become editable OpenPencil layers:
|
||
|
||
```sh
|
||
openpencil import card.html --css card.css -o card.fig
|
||
openpencil import card.html --tailwind "flex flex-col gap-3 w-80 p-6 rounded-xl bg-white" -o card.fig
|
||
```
|
||
|
||
```html
|
||
<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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
openpencil analyze colors design.fig
|
||
openpencil analyze typography design.fig
|
||
openpencil analyze spacing design.fig
|
||
openpencil analyze clusters design.fig
|
||
openpencil analyze overlaps 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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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 <kbd>⌘</kbd><kbd>J</kbd> (<kbd>Ctrl</kbd><kbd>J</kbd> on Windows and Linux) 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, DeepSeek, Z.ai, MiniMax, or compatible endpoints. No backend, no account.
|
||
|
||
Not every provider works in the browser, and not every model streams tool calls correctly. See [BYOK provider & model compatibility](packages/docs/programmable/byok-provider-compatibility.md) for measured results — contributions welcome.
|
||
|
||
### 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.
|
||
|
||
Pi is also available as an optional AI SDK Harness provider. Install its companion CLI with `npm install -g @open-pencil/harness`, then add a **Pi** model profile in **Settings → AI & agents**. The companion is installed separately so OpenPencil does not bundle a JavaScript runtime for users who do not enable Harness providers.
|
||
|
||
**Setup (Claude Code):**
|
||
|
||
1. Install the ACP adapter: `npm install -g @agentclientprotocol/claude-agent-acp`
|
||
2. Add MCP permission to `~/.claude/settings.json`:
|
||
```json
|
||
{
|
||
"permissions": {
|
||
"allow": ["mcp__open-pencil__*"]
|
||
}
|
||
}
|
||
```
|
||
3. Open the desktop app → <kbd>⌘</kbd><kbd>J</kbd> → 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 →](https://openpencil.dev/programmable/mcp-server)
|
||
|
||
**Stdio** (Claude Code, Cursor, Windsurf):
|
||
|
||
```sh
|
||
npm install -g @open-pencil/mcp
|
||
claude mcp add --scope user open-pencil -- openpencil-mcp
|
||
```
|
||
|
||
For other MCP clients:
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"open-pencil": {
|
||
"command": "openpencil-mcp"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**HTTP** (scripts, CI):
|
||
|
||
```sh
|
||
openpencil-mcp-http # Unix socket on macOS/Linux + http://127.0.0.1:7600/mcp
|
||
```
|
||
|
||
Local clients discover the private Unix socket automatically and fall back to localhost TCP. Set `PORT=0` to disable TCP on macOS/Linux.
|
||
|
||
**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](skills/open-pencil/SKILL.md)
|
||
|
||
Teach your AI coding agent to use OpenPencil — inspect designs, export assets, analyze tokens, modify .fig files:
|
||
|
||
```sh
|
||
npx skills add open-pencil/open-pencil
|
||
```
|
||
|
||
Works with Claude Code, Cursor, Windsurf, Codex, and any agent that supports [skills](https://skills.sh).
|
||
|
||
For documentation-aware agents, the docs site publishes [llms.txt](https://openpencil.dev/llms.txt), [llms-full.txt](https://openpencil.dev/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.
|
||
|
||
1. Click the share button in the top-right panel
|
||
2. Share the generated link (`app.openpencil.dev/share/<room-id>`)
|
||
3. Collaborators see your cursor, selection, and edits in real time
|
||
4. 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](https://github.com/dannote/figma-use) added full read/write automation via CDP — then [Figma 126 killed CDP](https://forum.figma.com/report-a-problem-6/remote-debugging-port-not-working-in-figma-desktop-126-1-2-50858). 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](https://openpencil.dev/development/roadmap) for product direction and current Figma compatibility gaps.
|
||
|
||
## Community
|
||
|
||
- **[Discord](https://discord.gg/4wXc9fuZfm)** — chat, quick questions, and showing a problem live
|
||
- **[GitHub Discussions](https://github.com/open-pencil/open-pencil/discussions)** — [Q&A](https://github.com/open-pencil/open-pencil/discussions/categories/q-a) for help, [Ideas](https://github.com/open-pencil/open-pencil/discussions/categories/ideas) for feature proposals, [Show and tell](https://github.com/open-pencil/open-pencil/discussions/categories/show-and-tell) for what you built; maintainers post [Announcements](https://github.com/open-pencil/open-pencil/discussions/categories/announcements) there
|
||
- **[Issues](https://github.com/open-pencil/open-pencil/issues)** — reproducible bugs; report security problems through a [private advisory](https://github.com/open-pencil/open-pencil/security/advisories/new)
|
||
|
||
## Contributing
|
||
|
||
```sh
|
||
bun install
|
||
bun run dev:portless # Web editor at https://open-pencil.localhost
|
||
bun run tauri dev # Desktop app (requires Rust)
|
||
```
|
||
|
||
[CONTRIBUTING.md](CONTRIBUTING.md) covers setup, quality gates, pull requests, and commits. [AGENTS.md](AGENTS.md) maps the repository and links the guide inside each package. Desktop builds need [Rust](https://rustup.rs/) and the [Tauri v2 prerequisites](https://v2.tauri.app/start/prerequisites/); run `bun run tauri build`.
|
||
|
||
### Tech stack
|
||
|
||
| Layer | Tech |
|
||
| ------------- | --------------------------------------------------------------------------------- |
|
||
| Rendering | Skia (CanvasKit WASM) |
|
||
| Layout | Yoga WASM (flex + grid via [fork](https://github.com/open-pencil/yoga/tree/grid)) |
|
||
| 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 | Vercel AI SDK (multi-provider BYOK), MCP SDK, Hono |
|
||
|
||
## Acknowledgments
|
||
|
||
Thanks to [@sld0Ant](https://github.com/sld0Ant) (Anton Soldatov) for creating and maintaining the [documentation site](https://openpencil.dev).
|
||
|
||
## License
|
||
|
||
OpenPencil is licensed under the [MIT License](./LICENSE).
|
||
|
||
Copyright (c) 2026 Danila Poyarkov and OpenPencil contributors.
|