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
2026-06-06 22:07:42 +00:00
Pull requests must be reviewable without guessing the author's intent.
2026-06-06 21:36:47 +00:00
2026-06-06 22:07:42 +00:00
### PR title
- Write the title in English.
- Be specific about the actual change; avoid vague titles such as `fix` , `update` , `some fixes` , `changes` , or `WIP` .
- Use Conventional Commits when it fits the change, for example `fix: handle empty exports` or `docs: clarify CLI setup` .
### PR body
- Follow the PR template when one is provided.
- Explain what changed and why it changed.
- Include a concrete list or paragraph of meaningful changes.
- Document validation, such as `bun run check` , targeted tests, docs-only review, or an explicit reason validation was not run.
- Keep the body primarily in English. Code identifiers, file paths, logs, error messages, and short quoted examples may use their original language.
2026-06-07 05:51:37 +00:00
### Low-effort PRs
2026-06-06 22:07:42 +00:00
Do not submit placeholder PRs. Remove template comments before opening a PR. Do not leave dangling issue references such as `Fixes #` , `TODO` , `TBD` , empty headings, unfilled sections, or similar unfinished text.
2026-06-06 21:36:47 +00:00
2026-06-07 05:51:37 +00:00
CodeRabbit may flag PR Hygiene issues for maintainers to review. Missing template sections or validation details are normal review feedback; they are not, by themselves, a personal judgment on the contributor. Maintainers may close PRs manually when they are clearly low-effort, automated, not written in English, unrelated to the project, or impossible to review without substantial guesswork. If you are unsure how to fix something, please open a detailed issue instead of submitting a placeholder PR.
2026-06-06 21:36:47 +00:00
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.