Find a file
Danila Poyarkov 6545f20c53
Refactor architecture boundaries across core, app, and packages (#234)
* refactor(core): decompose editor factory and action modules

Split the monolithic editor factory and large action modules into focused
domain helpers:

- create.ts assembles context through bridge modules (clipboard,
  components, structure, undo) and delegates to graph-reads, graph-events,
  layout-runner, component-sync, and state factory
- structure.ts delegates to group, container-wrap, auto-layout-wrap,
  reorder, and state-toggle helpers
- selection.ts delegates to hit-test, overlays, container navigation,
  and read helpers
- clipboard.ts delegates to subtree-history, images, export, copy,
  fonts, and placement helpers
- shapes.ts delegates to pen actions and section-adopt
- components.ts delegates to focus and instances helpers
- alignment.ts delegates to flip-rotate helper
- text.ts uses explicit TextEditSession for snapshot comparison

New focused modules: nudge, variable-bindings, layout-mode,
page-viewports, tool-registry, color-space

Undo: history/position and history/snapshot helpers, hardened
batch/rollback with nested batch support and configurable limit

* refactor(core): split tool definitions by domain

Split the monolithic tool registry into domain-specific modules:

- read/ — selection, find, pages, fonts, components, nodes, query, jsx
- create/ — basic shapes, components, vector, JSX render
- modify/ — paint, effects, geometry, layout, state, text, update
- structure/ — basic, arrange, batch, hierarchy, replace, tree
- variables/ — bindings, collections, read, values
- vector/ — boolean, path, export, viewport
- analyze/ — colors, typography, spacing, clusters, diff, eval
- describe/ — summaries, tree, roles, layout-issues
- stock-photo/ — providers, requests, apply
- codegen/ — component-map, tokens

Split registry into core/extended tiers; refine schema and AI adapter

* refactor(core): restructure kiwi codec and instance overrides

Reorganize the Kiwi .fig codec into domain subdirectories:

- binary/ — codec, schema, protocol
- fig/ — file, import, parse (core, worker, transfer)
- node-change/ — convert, export-node, serialize, plugin-data
- instance-overrides/ — constraints, dsd, populate, props, resolve,
  symbol-overrides, symbol-props, sync, types

Vendored kiwi-schema/ left isolated

* refactor(core): split profiler, icons, IO, and add subpath exports

Profiler: speedscope-export, capture-session, hud-controller
Icons: api, svg, types, render, create-icons tool
IO: format registry and subpath exports
Canvas/color/text/vector: targeted cleanup

Add deliberate subpath exports: random, xpath, vector, color, canvas,
scene-graph, kiwi, design-jsx, io, tools, editor, layout, canvaskit,
profiler, text, lint, rpc, figma-api, constants

* refactor(vue): decompose canvas input, surface lifecycle, and controls

Canvas surface: gl-surface, kit-loader, render-loop, resize-observer

Canvas input handlers:
- move: drop-target, move-snap, duplicate-drag
- select: select-move, select-hover, select-hit
- resize: resize-rect, resize-vector, resize-start
- transform: rotation, marquee, pan, text-selection
- text-edit: navigation, clipboard, textarea lifecycle
- Shared: click-count, space-key, pan, pan-zoom, draw, raf-scheduler

Editor composition:
- commands split: actions, context, metadata, edit, selection, view
- menu-model split: command-groups, builders, types
- Gradient stop composable reuse in primitive root

Controls: fill, layout, typography, appearance, effects, stroke,
okhcl, prop-scrub, node-props, undo-batch, color-variable-binding
Variables/i18n/document/export helpers

Organize canvas, primitives, controls, editor, and variables into
cohesive module directories with package-local import aliases

Expose MenuActionNode/MenuSeparatorNode from public API

* refactor(app): split document IO, editor session, and automation bridge

Document IO: source-state, naming, writer, reload-source, reload-state,
imported-document, watch-targets, save-targets

Editor session: create, modules, types, accessors, computed, refs
Editor canvas: loader-overlay, collaboration-awareness,
context-selection, menu-actions, menu-model

Automation bridge: eval, tools, exports, files, selection, RPC fallback

AI/ACP: transport, map-update, permission, debug, chat effects/storage
Collab: awareness, graph-bindings, yjs-sync, follow, session, types
Shell keyboard: actions, bindings, clipboard, focus, nudging,
raw-events, registry, reserved, shortcuts, space-tool
Shell menu: app-menu, document-name, entry, files

Demo: colors, effects, helpers, section builders (components,
app-preview, effects, standalone, variables) — document.ts reduced
from 981 to 32 lines as pure orchestrator

Move app modules under src/app/ with organized domain structure:
editor, document, ai, collab, shell, automation, demo, tabs

* refactor(app): decompose UI components with provide/inject context

Split monolithic components using Reka UI-inspired namespace folders
with scoped provide/inject context — no prop drilling:

- CollabPanel/ — context, avatars, share, connected, join
- ColorPickerPanel/ — context, area, format, field groups, sliders
- MobileHud/ — context, action toast, tool badge, file menu, presence
- ProviderSettings/ — context, API key/type, endpoint, tokens, photos
- Toolbar/ — actions, types, desktop, mobile, tool button, flyout
- LayoutSection/ — types, auto-layout, flex, grid, padding, size, clip

Properties helpers: fill-okhcl adapter, fill-label, color-style-row
Menu: entry helpers, document-name rename, stale type removal

* refactor(mcp): split server into focused modules

- browser-rpc — WebSocket client management
- mcp-sessions — session lifecycle
- tool-output — response formatting
- tool-schema — Zod schema generation from ToolDefs
- jsx-preprocess — JSX source transformation
- result — result helpers
- tool-registration — MCP tool wiring
- auth — API key validation
- http-options — CORS/request handling
- stdio-bridge — stdio transport adapter

* refactor(cli): split analyze subcommands and shared helpers

- Analyze subcommands: clusters, colors, spacing, typography
- RPC data loading helper
- Migrate imports to targeted core subpath exports

* refactor(docs): split VitePress config and shared table component

Config helpers: sdk-sidebar, seo, labels, sidebars, locale-theme,
root-theme, locales

Shared SdkDataTable component replaces duplicated table markup in
SdkPropsTable, SdkEventsTable, and SdkSlotsTable

Update contributing and testing docs

* refactor(tauri): decompose desktop entrypoint

Split lib.rs into focused service modules:

- fig_container.rs — .fig archive/compression commands
- fonts.rs — font cache and system font enumeration
- menu.rs — native menu construction
- menu_events.rs — menu event dispatch and devtools toggle
- window.rs — main window show/focus lifecycle

* test: share domain test factories and migrate fixtures

New shared helpers:
- tests/helpers/scene.ts — makeSceneGraph factory
- tests/helpers/vector-network.ts — vertex/segment/network builders
- tests/helpers/fig-traversal.ts — all-node collection, type counts
- tests/helpers/undo.ts — undo test utilities
- tests/helpers/editor-history.ts — editor history test helpers

Migrate render, vector, fig-roundtrip, and undo tests to use shared
factories instead of inline fixture construction

* build: add structural lint rules, split vite config, update docs

Structural lint (oxlint.structure.json + lint/plugin.js):
- 20+ custom rules enforcing package boundaries, lifecycle patterns,
  naming conventions, and import discipline

Vite config split: raw-markdown, canvaskit-assets, pwa, server,
aliases, automation plugins

Remove legacy shims and utils superseded by SDK/core modules
Update AGENTS.md, CONTRIBUTING.md, eval-command docs, tsconfig

* fix(vue): normalize canvas directory casing and remove duplicate export

- Rename Canvas/ to canvas/ in git index to match #vue/canvas/* imports
  (PascalCase was correct for component primitives but canvas/ is a
  non-component domain directory)
- Remove duplicate ./random subpath export in core package.json

* fix: add #vue and #core Vite resolve aliases for dev server

* refactor(core): reduce remaining large modules

Split the remaining large core hotspots into cohesive domain modules while preserving public facades and behavior.

- Extract scene graph types, variables, node defaults, and vector-network helpers
- Decompose canvas renderer orchestration, state, paints, colors, lifecycle, labels, and delegated domain methods into renderer/ and labels/ subfolders
- Split Kiwi node-change, binary variable binding, layout, RPC, vector, JSX export, clipboard, design JSX, and Figma proxy helpers
- Replace collision-driven *Fn import aliases with namespace imports and enforce the pattern in lint

Validation:
- bun run check
- bun --filter @open-pencil/vue build
- bun run test:dupes

* fix(app): forward color input attrs

* fix(app): cover section drawing errors

* fix(editor): undo option-drag duplicates

* docs: document domain subfolder convention

* fix(app): handle undo redo on keydown

* refactor(app): dispatch shortcuts from keydown

* refactor: group prefixed domain modules

* refactor(app): use tinykeys for shortcuts

* refactor(core): group symbol override modules

* refactor(core): group fig kiwi container helper

* refactor(canvas): split overlay rendering modules

* refactor(vue): remove unused internal barrels

* fix(app): lay out demo components before instancing

* fix(app): restore demo badge spacing

* perf(canvas): split scene and overlay rendering

* refactor(vue): wrap wheel gesture lifecycle

* fix(canvas): wait for fonts before hiding loader

* docs: update unreleased changelog
2026-04-30 15:14:19 +03:00
.github ci: create GitHub releases as drafts 2026-04-23 17:16:58 +03:00
.vscode Initial commit: Tauri + CanvasKit + React editor scaffold 2026-02-27 15:48:29 +03:00
desktop Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
docs Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
lint Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
packages Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
public fix(kiwi): improve figma export fidelity 2026-04-25 15:57:15 +03:00
scripts Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
src Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
tests Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
vite Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
.gitattributes Fix CJK text garbled when font unavailable (#89) 2026-03-10 16:58:57 +03:00
.gitignore Harden ACP transport, MCP server, and add permission dialog 2026-03-15 16:49:43 +03:00
.lfsconfig Use Cloudflare R2 for Git LFS storage 2026-03-04 01:25:33 +03:00
.oxfmtrc.json Align app UI primitives with shared ui overrides 2026-03-24 09:50:20 +03:00
AGENTS.md Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
bun.lock Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
CHANGELOG.md Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
CONTRIBUTING.md Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
index.html Fix PWA manifest error in dev, handle invalid font data 2026-03-06 21:43:16 +03:00
LICENSE Add MIT license 2026-03-02 08:31:54 +03:00
oxlint.json Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
oxlint.structure.json Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
package.json Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
PLAN.md Release v0.9.0 2026-03-09 17:43:11 +03:00
playwright.config.ts Fix flaky E2E tests: layers panel, zoom-to-fit, snapshot stability 2026-03-07 11:08:22 +03:00
README.md docs: update README and AGENTS.md for new features 2026-04-22 18:12:15 +03:00
SECURITY.md Add CI for PRs, issue/PR templates, CONTRIBUTING and SECURITY docs 2026-03-03 17:24:45 +03:00
tsconfig.json Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00
tsconfig.node.json Initial commit: Tauri + CanvasKit + React editor scaffold 2026-02-27 15:48:29 +03:00
vite.config.ts Refactor architecture boundaries across core, app, and packages (#234) 2026-04-30 15:14:19 +03:00

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

OpenPencil

Installation

macOS (Homebrew):

brew install open-pencil/tap/open-pencil

Or download from the releases page, or use the web app — no install needed.

What it does

  • Opens .fig and .pen files — read and write native Figma files, open Pencil documents, 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
  • 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

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:

open-pencil tree design.fig
open-pencil find design.pen --type TEXT
open-pencil node design.fig --id 1:23
open-pencil 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:

open-pencil query design.fig "//FRAME"                              # All frames
open-pencil query design.fig "//FRAME[@width < 300]"                # Frames under 300px
open-pencil query design.fig "//TEXT[contains(@name, 'Button')]"     # Text with 'Button' in name
open-pencil query design.fig "//*[@cornerRadius > 0]"               # Rounded corners
open-pencil 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:

open-pencil export design.fig                           # PNG
open-pencil export design.fig -f jpg -s 2 -q 90        # JPG at 2x, quality 90
open-pencil export design.fig -f fig --page "Page 1"   # Export a page as .fig
open-pencil export design.fig -f jsx --style tailwind   # Tailwind JSX
open-pencil 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:

open-pencil lint design.fig
open-pencil lint design.pen --preset strict
open-pencil lint design.fig --rule color-contrast
open-pencil 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:

open-pencil analyze colors design.fig
open-pencil analyze typography design.fig
open-pencil analyze spacing design.fig
open-pencil analyze clusters design.fig
open-pencil 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:

open-pencil eval design.fig -c "figma.currentPage.children.length"
open-pencil 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:

open-pencil tree                               # Inspect the live document
open-pencil export -f png                      # Screenshot the current canvas
open-pencil 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):

  1. Install the ACP adapter: npm i -g @agentclientprotocol/claude-agent-acp
  2. Add MCP permission to ~/.claude/settings.json:
    {
      "permissions": {
        "allow": ["mcp__open-pencil"]
      }
    }
    
  3. 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):

bun add -g @open-pencil/mcp
{
  "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.

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 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.

Roadmap

  • 100% Figma compatibility — full import/export fidelity, rendering parity, and broader automated compatibility coverage
  • Prototyping — frame transitions, interaction triggers, overlay management, preview mode
  • Shader effects (SkSL) — custom visual effects via GPU shaders
  • Raster tile caching — instant zoom/pan for complex documents
  • Component libraries — publish, share, and consume design systems across files
  • CI tools — design linting, code export, visual regression in pipelines
  • Grid child positioning UI — column/row span controls, grid overlay on canvas
  • Windows code signing (Azure Authenticode certificates)

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