openpencil/CLAUDE.md
2026-05-03 21:00:00 +08:00

192 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Detailed module docs are loaded automatically when working in subdirectories:
- **`packages/CLAUDE.md`** — Package overview (all packages at a glance)
- **`packages/pen-types/CLAUDE.md`** — Type definitions for PenDocument model
- **`packages/pen-core/CLAUDE.md`** — Document tree ops, layout engine, variables, boolean ops
- **`packages/pen-engine/CLAUDE.md`** — Headless design engine (document, selection, history, viewport)
- **`packages/pen-react/CLAUDE.md`** — React UI SDK (provider, hooks, panels, canvas)
- **`packages/pen-figma/CLAUDE.md`** — Figma .fig file parser and converter
- **`packages/pen-renderer/CLAUDE.md`** — Standalone CanvasKit/Skia renderer
- **`packages/pen-mcp/CLAUDE.md`** — MCP server (tools, routes, document manager)
- **`packages/pen-ai-skills/CLAUDE.md`** — AI prompt skill engine (phase-driven loading)
- **`packages/pen-sdk/CLAUDE.md`** — Umbrella SDK (re-exports all packages)
- **`packages/agent-native/CLAUDE.md`** — Zig agent runtime (NAPI addon)
- **`apps/web/CLAUDE.md`** — Web app (canvas engine, stores, components, AI services)
- **`apps/desktop/CLAUDE.md`** — Electron desktop app (IPC, file association, auto-updater)
- **`apps/cli/CLAUDE.md`** — CLI tool (`op` commands, input methods, connection)
## Commands
- **Dev server:** `bun --bun run dev` (runs on port 3000)
- **Build:** `bun --bun run build`
- **Preview production build:** `bun --bun run preview`
- **Run all tests:** `bun --bun run test` (Vitest)
- **Run a single test:** `bun --bun vitest run path/to/test.ts`
- **Type check:** `npx tsc --noEmit`
- **Lint:** `bun run lint` (oxlint)
- **Format:** `bun run format` (oxfmt)
- **Format check:** `bun run format:check` (oxfmt `--check`, required before commit)
- **Install dependencies:** `bun install`
- **Bump version:** `bun run bump <version>` (syncs all package.json files)
- **Electron dev:** `bun run electron:dev` (starts Vite + Electron together)
- **Electron compile:** `bun run electron:compile` (esbuild electron/ to out/desktop/)
- **Electron build:** `bun run electron:build` (full web build + compile + electron-builder package)
- **CLI compile:** `bun run cli:compile` (esbuild CLI to apps/cli/dist/)
- **CLI dev:** `bun run cli:dev` (run CLI from source via Bun)
- **MCP dev:** `bun run mcp:dev` (run MCP server from source)
- **Publish beta:** `bun run publish:beta [N]` (publish all npm packages with beta tag)
## Architecture
OpenPencil is an open-source vector design tool (alternative to Pencil.dev) with a Design-as-Code philosophy. Organized as a **Bun monorepo** with workspaces:
```text
openpencil/
├── apps/
│ ├── web/ TanStack Start full-stack React app (Vite + Nitro)
│ ├── desktop/ Electron desktop app (macOS, Windows, Linux)
│ └── cli/ CLI tool — control the design tool from the terminal
├── packages/
│ ├── pen-types/ Type definitions for PenDocument model
│ ├── pen-core/ Document tree ops, layout engine, variables, boolean ops, clone utilities
│ ├── pen-engine/ Headless design engine — framework-free document, selection, history, viewport
│ ├── pen-react/ React UI SDK — DesignProvider, DesignCanvas, hooks, panels, toolbar
│ ├── pen-codegen/ Multi-platform code generators
│ ├── pen-figma/ Figma .fig file parser and converter
│ ├── pen-renderer/ Standalone CanvasKit/Skia renderer
│ ├── pen-mcp/ MCP server — tools, routes, document manager for external CLI integration
│ ├── pen-sdk/ Umbrella SDK (re-exports all packages)
│ ├── pen-ai-skills/ AI prompt skill engine (phase-driven prompt loading + design memory)
│ └── agent-native/ Native AI agent runtime (Zig NAPI, multi-provider, agent teams)
├── scripts/ Build and publish scripts
└── .githooks/ Pre-commit version sync from branch name
```
**Key technologies:** React 19, CanvasKit/Skia WASM (canvas engine), Paper.js (boolean path operations), Zustand v5 (state management), TanStack Router (file-based routing), Tailwind CSS v4, shadcn/ui (UI primitives), Vite 7, Nitro (server), Electron 35 (desktop), Vercel AI SDK v6 (agent framework), i18next (15 locales), TypeScript (strict mode), oxlint/oxfmt (linting & formatting).
### Data Flow
```text
React Components (Toolbar, LayerPanel, PropertyPanel)
│ Zustand hooks
▼
┌─────────────────┐ ┌───────────────────┐
│ canvas-store │ │ document-store │ ← single source of truth
│ (UI state: │ │ (PenDocument) │
│ tool/selection │ │ CRUD / tree ops │
│ /viewport) │ │ │
└────────┬────────┘ └────────┬──────────┘
│ │
▼ ▼
CanvasKit/Skia canvas-sync-lock
(GPU-accelerated (prevents circular sync)
WASM renderer)
```
- **document-store** is the single source of truth. CanvasKit only renders.
- User edits on canvas → SkiaEngine events → update document-store
- User edits in panels → update document-store → SkiaEngine `syncFromDocument()` re-renders
- `canvas-sync-lock.ts` prevents circular updates when canvas events write to the store
### Multi-Page Architecture
```text
PenDocument
├── pages?: PenPage[] (id, name, children)
└── children: PenNode[] (default/single-page fallback)
```
### Design Variables Architecture
- **`$variable` references are preserved** in the document store (e.g. `$color-1` in fill color)
- `resolveNodeForCanvas()` resolves `$refs` on-the-fly before CanvasKit rendering
- Code generators output `var(--name)` for `$ref` values
- Multiple theme axes supported (e.g. Theme-1 with Light/Dark, Theme-2 with Compact/Comfortable)
### MCP Layered Design Workflow
External LLMs (Claude Code, Codex, Gemini CLI, etc.) can generate designs via MCP:
- **Single-shot**: `batch_design` or `insert_node` — one call
- **Layered**: `design_skeleton` → `design_content` × N → `design_refine` — phased generation with focused context
- **Segmented prompts**: `get_design_prompt(section=...)` loads focused subsets (schema, layout, roles, icons, etc.)
### Path Aliases
`@/*` maps to `./src/*` (configured in `apps/web/tsconfig.json` and `apps/web/vite.config.ts`).
### Styling
Tailwind CSS v4 imported via `apps/web/src/styles.css`. UI primitives from shadcn/ui. Icons from `lucide-react`.
### CLI (`apps/cli/`)
The `op` command-line tool controls the desktop app or web server from the terminal. Arguments that accept JSON or DSL support three input methods: inline string, `@filepath` (read from file), or `-` (read from stdin).
- **App control:** `op start [--desktop|--web]`, `op stop`, `op status`
- **Design:** `op design <dsl|@file|->` — batch design DSL operations
- **Document:** `op open`, `op save`, `op get`, `op selection`
- **Nodes:** `op insert`, `op update`, `op delete`, `op move`, `op copy`, `op replace`
- **Cross-platform:** macOS, Windows (NSIS/portable), Linux (AppImage/deb/snap/flatpak)
### CI / CD
- **`.github/workflows/ci.yml`** — Push/PR on `main` and `v*` branches: type check, tests, web build
- **`.github/workflows/build-electron.yml`** — Tag push (`v*`) or manual: builds Electron for all platforms, creates draft GitHub Release
- **`.github/workflows/publish-cli.yml`** — Tag push (`v*`) or manual: publishes all `@zseven-w/*` npm packages in topological order
- **`.github/workflows/docker.yml`** — Docker image build and push
### Version Sync
- **Pre-commit hook** (`.githooks/pre-commit`): extracts version from branch name (e.g. `v0.5.0` → `0.5.0`) and syncs to all `package.json` files
- **Manual bump:** `bun run bump <version>` to set a specific version across all workspaces
- Requires `git config core.hooksPath .githooks` (one-time setup per clone)
## Working Principles
Behavioral guidelines adapted from [Karpathy's coding principles](https://github.com/multica-ai/andrej-karpathy-skills) to OpenPencil's multi-layer, multi-app structure. These supplement (do not replace) the general system prompt.
### Think before coding
A single bug often spans layers. A canvas render issue may live in `pen-core/layout`, `apps/web/canvas/skia`, or `pen-react/hooks`. An LLM-generated-design issue may live in `pen-ai-skills/skills/`, the MCP tool, or the post-generation validator. Identify the owning layer before patching — don't fix the symptom at a higher layer when the cause is deeper. When the target surface is ambiguous (desktop / web / cli / MCP / docs), say so and ask rather than guessing.
### Simplicity first
Add abstractions when a second concrete case arrives, not in anticipation of one. No strategy classes for single-use logic, no speculative config options, no premature helper layers. The 800-line file limit is a ceiling, not a split target — a healthy 400-line file doesn't need slicing.
### Surgical changes
Fix only what the task names. A validation bug fix doesn't include added type hints, renamed identifiers, or reformatted siblings. Match existing style (shadcn tokens, kebab-case filenames, existing quote/brace style). When an LLM-generated design reproduces the same bug across many nodes, fix the prompt / role resolver / validator in `pen-ai-skills` — never touch up each instance via `batch_design`.
### Verify before declaring done
- **Any TS/JS change:** `bun run format:check && npx tsc --noEmit && bun --bun run test` must pass before commit or push. Docs-only changes in a separate `openpencil-docs` repo are exempt.
- **UI / canvas change:** start `bun --bun run dev` and exercise the feature in a browser. Type-checking proves compilation, not feature correctness.
- **MCP / `pen-ai-skills` change:** verify via the `op` CLI, `debug_validation_report`, or `debug_screenshot` — not by reading the diff.
- Report what was verified, not only what was edited.
## Code Style
- Single files must not exceed 800 lines. Split into smaller modules when they grow beyond this limit.
- One component per file, each with a single responsibility.
- `.ts` and `.tsx` files use kebab-case naming, e.g. `canvas-store.ts`, `use-keyboard-shortcuts.ts`.
- UI components must use shadcn/ui design tokens (`bg-card`, `text-foreground`, `border-border`, etc.). No hardcoded Tailwind colors like `gray-*`, `blue-*`.
- Toolbar button active state uses `isActive` conditional className (`bg-primary text-primary-foreground`), not Radix Toggle's `data-[state=on]:` selector (has twMerge conflicts).
## Git Commit Convention
Use [Conventional Commits](https://www.conventionalcommits.org/) format: `<type>(<scope>): <subject>`
**Types:** `feat`, `fix`, `refactor`, `perf`, `style`, `docs`, `test`, `chore`
**Scopes:** `editor`, `canvas`, `panels`, `history`, `ai`, `codegen`, `store`, `types`, `variables`, `figma`, `mcp`, `electron`, `renderer`, `sdk`, `cli`, `agent`, `i18n`
**Rules:** Subject in English, lowercase start, no period, imperative mood. Body is optional; explain **why** not what. One commit per change.
## License
MIT License. See [LICENSE](./LICENSE) for details.