2026-03-03 14:24:45 +00:00
# Contributing
## Setup
```bash
git clone https://github.com/open-pencil/open-pencil.git
cd open-pencil
Harden ACP transport, MCP server, and add permission dialog
- Extract mapUpdate to testable module, dynamic import for @tauri-apps/plugin-shell
- Add warnings for unhandled ACP content types and empty tool titles
- MCP server: session limit (max 10), fix null WS comparison, guard JSX preprocessing
- MCP server: read version from package.json instead of hardcoded 0.0.0
- MCP tests: use port 0 (OS-assigned) to prevent collision
- Connection error handling with user-friendly messages, stale session recovery
- Agent crash detection via close handler, destroying flag, buildCrashChunks
- Port collision: detect EADDRINUSE in vite-plugin stderr and log clear error
- Production Tauri: spawn openpencil-mcp via shell plugin, orphan reuse via health check
- Permission confirmation dialog (reka-ui AlertDialog) with queue, 60s auto-reject timeout
- Health check in ProviderSelect hides ACP agents when MCP server unavailable
- Move DESIGN_CONTEXT to app constants, add ACP_PERMISSION_TIMEOUT_MS
- 36 tests across 3 files (acp-transport, acp-permission, mcp-server)
- Update CHANGELOG, README, CONTRIBUTING, AGENTS.md
2026-03-15 13:37:15 +00:00
git clone https://github.com/open-pencil/vue-stream-markdown.git
2026-03-03 14:24:45 +00:00
bun install
```
## Development
```bash
bun run dev # Vite dev server on localhost:1420
bun run tauri dev # Tauri desktop app with hot reload
2026-04-22 10:01:38 +00:00
# For macOS release builds with ad-hoc signing (no Apple Developer account, local testing only):
2026-05-25 21:14:08 +00:00
APPLE_SIGNING_IDENTITY=- bun run tauri build -c '{"bundle": { "createUpdaterArtifacts": false }}'
2026-03-03 14:24:45 +00:00
```
2026-06-06 21:36:47 +00:00
## Pull requests
Pull requests must be written in English and must follow the PR template. The title and body should clearly explain what changed, why it changed, and how it was validated.
Do not submit placeholder PRs. Remove template comments before opening a PR. Do not leave dangling issue references such as `Fixes #` , `TODO` , `TBD` , or similar unfinished text.
PRs from external contributors that ignore the template, omit validation, are not written in English, or otherwise do not follow these guidelines may be labeled `invalid` and closed automatically.
2026-03-03 14:24:45 +00:00
## Quality checks
Run all of these before submitting a PR:
```bash
bun run check # oxlint + typecheck
bun run format # oxfmt with import sorting
bun run test:dupes # jscpd < 3 % duplication
Port analyze/diff tools from figma-use, split tools into domain files
Analyze: colors, typography, spacing, clusters
Diff: diff_create (tree diff), diff_show (preview changes)
Utility: get_components, get_current_page, arrange, node_to_component
Split schema.ts (2600 lines) into read, create, modify, structure,
variables, vector, analyze, registry — each under 600 lines.
Clean up inline types (use Color, Vector, SceneNode from existing defs).
Update AGENTS.md and CONTRIBUTING.md with code quality guidelines.
2026-03-05 16:00:40 +00:00
bun run test:unit # bun:test (tests/engine/)
bun run test # Playwright E2E (auto-starts dev server)
2026-03-03 14:24:45 +00:00
```
## Project structure
- `packages/core` — scene graph, renderer, layout, codec (zero DOM deps)
- `packages/cli` — headless CLI for .fig inspection and export
- `packages/mcp` — MCP server for AI tools (stdio + HTTP)
Port analyze/diff tools from figma-use, split tools into domain files
Analyze: colors, typography, spacing, clusters
Diff: diff_create (tree diff), diff_show (preview changes)
Utility: get_components, get_current_page, arrange, node_to_component
Split schema.ts (2600 lines) into read, create, modify, structure,
variables, vector, analyze, registry — each under 600 lines.
Clean up inline types (use Color, Vector, SceneNode from existing defs).
Update AGENTS.md and CONTRIBUTING.md with code quality guidelines.
2026-03-05 16:00:40 +00:00
- `packages/docs` — VitePress documentation site (openpencil.dev)
2026-03-03 14:24:45 +00:00
- `src/` — Tauri/Vite desktop editor
## Conventions
Port analyze/diff tools from figma-use, split tools into domain files
Analyze: colors, typography, spacing, clusters
Diff: diff_create (tree diff), diff_show (preview changes)
Utility: get_components, get_current_page, arrange, node_to_component
Split schema.ts (2600 lines) into read, create, modify, structure,
variables, vector, analyze, registry — each under 600 lines.
Clean up inline types (use Color, Vector, SceneNode from existing defs).
Update AGENTS.md and CONTRIBUTING.md with code quality guidelines.
2026-03-05 16:00:40 +00:00
See [`AGENTS.md` ](./AGENTS.md ) for the full architecture reference, code conventions, and quality checklist. Key points:
2026-03-03 14:24:45 +00:00
- Bun runtime, not Node
- Tailwind 4 for styles, no inline CSS or `<style>` blocks
- No `any` , no `!` non-null assertions
- `@/` import alias for app code, relative imports within core
- Use `crypto.getRandomValues()` , never `Math.random()`
- Icons via unplugin-icons (`< icon-lucide- * > `)
Port analyze/diff tools from figma-use, split tools into domain files
Analyze: colors, typography, spacing, clusters
Diff: diff_create (tree diff), diff_show (preview changes)
Utility: get_components, get_current_page, arrange, node_to_component
Split schema.ts (2600 lines) into read, create, modify, structure,
variables, vector, analyze, registry — each under 600 lines.
Clean up inline types (use Color, Vector, SceneNode from existing defs).
Update AGENTS.md and CONTRIBUTING.md with code quality guidelines.
2026-03-05 16:00:40 +00:00
- Use existing deps and Reka UI components before hand-rolling (see AGENTS.md → Code quality)
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 12:14:19 +00:00
- Follow the Reka UI-inspired file structure: PascalCase component namespace folders and Vue files, lowercase/kebab non-component domains, and multi-file root components colocated inside their namespace folder
2026-05-17 11:25:46 +00:00
- Keep UI labels translatable. Do not hardcode user-facing strings in Vue templates when an i18n namespace exists.
- Keep shortcuts out of labels/translations. Put command shortcut tokens and keyboard bindings in `packages/vue/src/editor/commands/registry.ts` , then render them with `formatShortcut()` so macOS and Windows/Linux display correctly.
- Canvas context-menu grouping belongs in `packages/vue/src/editor/menu-model/canvas.ts` ; components should render the menu model instead of hand-building command groups.
2026-03-03 14:24:45 +00:00
feat: mobile layout, PWA support (#27) (#27)
- Mobile-responsive editor with bottom drawer, ribbon nav, HUD overlay
- Touch support: single-finger tools, two-finger pinch-zoom
- PWA: manifest, service worker, installability
- Extract LayerTree from LayersPanel, shared utils (colorToCSS, initials, toolIcons)
- Constants moved to src/constants.ts, clipboard state to editor store
- reka-ui Popover/DropdownMenu in MobileHud
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-05 16:51:51 +00:00
## Test IDs (`data-test-id`)
Every interactive or structurally significant element must have a `data-test-id` attribute. These are used by Playwright E2E tests and must follow the naming convention below.
### Naming rules
- **kebab-case**, all lowercase
- Pattern: `{component}-{element}` or `{component}-{element}-{variant}`
- Mobile counterparts are prefixed with `mobile-`
- Dynamic IDs use template literals: `` :data-test-id="`toolbar-tool-${key.toLowerCase()}`" ``
### Nomenclature
| Prefix | Component | Examples |
|--------|-----------|---------|
| `toolbar-` | Desktop toolbar | `toolbar` , `toolbar-tool-select` , `toolbar-flyout-frame` , `toolbar-flyout-item-ellipse` |
| `mobile-toolbar-` | Mobile toolbar | `mobile-toolbar` , `mobile-toolbar-prev` , `mobile-toolbar-next` , `mobile-toolbar-tool-select` , `mobile-toolbar-flyout-frame` , `mobile-toolbar-copy` , `mobile-toolbar-front` |
| `mobile-toolbar-tools` | Mobile tools category | Container for drawing tools |
| `mobile-toolbar-edit` | Mobile edit category | Container for edit actions (copy, paste, cut, duplicate, delete) |
| `mobile-toolbar-arrange` | Mobile arrange category | Container for arrange actions (front, back, group, ungroup, lock) |
| `mobile-drawer-` | Mobile bottom drawer | `mobile-drawer` , `mobile-drawer-handle` , `mobile-drawer-pages` , `mobile-drawer-content` , `mobile-drawer-layers` , `mobile-drawer-design` , `mobile-drawer-code` , `mobile-drawer-ai` |
| `mobile-ribbon-` | Mobile bottom tab bar | `mobile-ribbon` , `mobile-ribbon-layers` , `mobile-ribbon-design` , `mobile-ribbon-code` , `mobile-ribbon-ai` |
| `layers-` | Layers panel | `layers-panel` , `layers-header` , `layers-tree` , `layers-item` |
| `pages-` | Pages panel | `pages-panel` , `pages-header` , `pages-item` , `pages-item-input` , `pages-add` |
| `properties-` | Properties panel | `properties-panel` , `properties-tab-design` , `properties-tab-code` , `properties-tab-ai` , `properties-zoom` |
| `design-` | Design tab | `design-node-header` , `design-multi-header` , `design-panel-single` , `design-panel-multi` , `design-panel-empty` |
| `position-` | Position section | `position-section` , `position-align-left` , `position-flip-horizontal` , `position-rotate-90` |
| `layout-` | Layout section | `layout-section` , `layout-add-auto` , `layout-remove-auto` , `layout-direction-horizontal` |
| `fill-` | Fill section | `fill-section` , `fill-section-add` , `fill-item` |
| `stroke-` | Stroke section | `stroke-section` , `stroke-section-add` , `stroke-item` |
| `effects-` | Effects section | `effects-section` , `effects-section-add` , `effects-item` |
| `export-` | Export section | `export-section` , `export-section-add` , `export-button` , `export-item` |
| `typography-` | Typography section | `typography-section` , `typography-missing-font` |
| `variables-` | Variables | `variables-section` , `variables-dialog` , `variables-add-variable` |
| `context-` | Context menu | `context-copy` , `context-paste` , `context-delete` , `context-group` |
| `color-` | Color picker | `color-picker-popover` , `color-picker-swatch` , `color-hex-input` |
| `fill-picker-` | Fill picker | `fill-picker-swatch` , `fill-picker-tab-solid` , `fill-picker-tab-gradient` |
| `font-picker-` | Font picker | `font-picker-trigger` , `font-picker-search` , `font-picker-item` |
| `chat-` | Chat / AI panel | `chat-panel` , `chat-input` , `chat-send-button` , `chat-messages` |
| `code-` | Code panel | `code-panel` , `code-panel-header` , `code-panel-copy` |
| `collab-` | Collaboration | `collab-popover` , `collab-share-button` , `collab-copy-link` |
| `canvas-` | Canvas | `canvas-area` , `canvas-element` , `canvas-loading` |
| `editor-` | Editor root | `editor-root` , `editor-document-name` , `editor-show-ui` |
| `app-` | App chrome | `app-logo` , `app-document-name` , `app-toggle-ui` , `app-select-trigger` |
| `tabbar-` | Tab bar | `tabbar-tab` , `tabbar-new` , `tabbar-close` |
| `scrub-input` | Scrub input | `scrub-input` , `scrub-input-field` |
| `toast-` | Toast notifications | `toast-item` , `toast-close` , `toast-copy-error` |
| `safari-banner` | Safari warning | `safari-banner` , `safari-banner-dismiss` |
2026-03-03 14:24:45 +00:00
## Test fixtures
`.fig` fixtures in `tests/fixtures/` are Git LFS. Use `git push --no-verify` to skip the slow LFS pre-push hook unless you changed `.fig` files.
## Commits
Follow the existing style in `git log` . Keep messages concise. Update `CHANGELOG.md` for user-facing changes.