openpencil/CLAUDE.md
Kayshen Xu 90bbcb16fd
V0.4.0 (#44)
* perf(canvas): bitmap cache during zoom/pan, fix save for large files

- Add canvas-zoom-cache: captures pixel snapshot on first viewport change,
  draws cached bitmap with transform delta (~0.1ms vs ~15ms per frame).
  Refreshes snapshot every 200ms during continuous interaction.
- Integrate zoom cache into wheel zoom and space+drag panning.
- Set renderOnAddRemove: false on Fabric canvas to avoid per-object renders.
- Remove depth/size LOD from viewport culling (zoom cache handles perf).
- Rewrite syncCanvasPositionsToStore: single tree walk + single store set
  instead of per-object updateNode (was O(n²) with 200+ history pushes).
- Unify save/save-as: .op files save in-place, non-.op triggers save-as.
- Add Electron filePath storage for reliable native IPC saves.
- Add error handling with fallback for all save paths.

* feat(canvas): integrate CanvasKit for enhanced rendering and layout

- Added support for CanvasKit WASM to improve rendering performance and capabilities.
- Introduced a new SkiaCanvas component for rendering using CanvasKit.
- Implemented spatial indexing with RBush for efficient hit testing in the Skia engine.
- Enhanced text measurement and layout handling using Canvas 2D for accurate word wrapping.
- Updated layout engine to accommodate badge and overlay nodes without affecting layout flow.
- Bumped version from 0.3.3 to 0.3.4 to reflect these significant changes.

* fix(electron): add graceful-fs to devDependencies for Node.js v25 compat (#38)

* feat(figma): enhance instance override handling and derived data processing

- Added support for symbol tree context in instance conversion.
- Improved the application of instance overrides by filtering derived data to exclude nested instances.
- Implemented a two-strategy approach for resolving overrides and derived data, accommodating both direct index mapping and expanded DFS for nested instances.
- Updated FigmaSymbolOverride interface to extend FigmaNodeChange, enhancing the handling of overridable node properties.

* refactor(canvas): remove loading state and enhance error handling in SkiaCanvas

- Eliminated the loading state management from SkiaCanvas, simplifying the component.
- Updated error handling to directly display error messages without loading indicators.
- Adjusted the EditorLayout to directly render SkiaCanvas without suspense, improving performance.
- Introduced drag-and-drop file handling in the canvas store for better user experience.
- Enhanced Figma import dialog to auto-process pending files from drag-and-drop.

* fix(ai): improve error messages for API authentication and connection issues

- Updated error hints in chat.ts to provide clearer instructions for API authentication, including running "claude login" or setting the ANTHROPIC_API_KEY in settings.json.
- Enhanced friendly error messages in connect-agent.ts to guide users on authentication steps when encountering connection errors.
- Refactored resolve-claude-agent-env.ts to improve environment variable normalization, allowing for better handling of ANTHROPIC_CUSTOM_HEADERS and ensuring proper serialization of object values.

* refactor(env): streamline environment variable reading and enhance settings handling

- Renamed and refactored the function for reading Claude settings to improve clarity and maintainability.
- Introduced a new function to read settings from both `settings.json` and `settings.local.json`, ensuring local settings take precedence.
- Updated test setup to use the system's temporary directory for security tests, enhancing compatibility across environments.

* feat(ai): add fallback models for third-party API proxies in connect-agent

- Introduced FALLBACK_CLAUDE_MODELS to provide default model options when supportedModels() fails, enhancing connectivity with third-party API proxies.
- Updated error handling in connectClaudeCode to return fallback models on specific connection errors, ensuring users can still connect and select a model.

* chore: bump version from 0.3.4 to 0.4.0 in package.json

* feat(ai): enhance prompt handling for basic-tier models

- Introduced logic to inline system prompts into user messages for basic-tier models (e.g., MiniMax, GLM) to ensure proper instruction visibility.
- Updated design modification and orchestrator functions to accommodate this change, improving interaction with third-party routers.
- Added a utility function to strip non-standard XML-like tags from AI responses, enhancing JSON extraction reliability.

* fix(server): add ESM-compatible __dirname polyfill (#42)

- Add fileURLToPath/dirname polyfill to mcp-install.ts
- Add fileURLToPath/dirname polyfill to mcp-server-manager.ts

Fixes browser version crash with "__dirname is not defined"
Fixes MCP over HTTP transport failure since v3.3

Closes #37

* fix(server): make MCP HTTP server survive parent process lifecycle (#43)

## Problem
The MCP HTTP server process died when:
1. User closed the Settings/Agent dialog in the UI
2. User interacted with the editor canvas
3. Nitro server hot-reloaded or restarted
4. Electron app sent SIGTERM to Nitro on window close

This made the MCP server unusable for HTTP transport, requiring users
to keep the settings dialog open constantly.

## Root Cause Analysis
The MCP server was spawned as a regular child process without detached
mode. This created several fatal dependencies:

1. **Signal propagation**: Lines 29-34 registered handlers for SIGINT,
   SIGTERM, SIGHUP that killed the MCP process when the parent received
   these signals (e.g., Electron closing Nitro)

2. **Parent-child lifecycle binding**: Without detached mode, the child
   process is tied to the parent's event loop. Any parent instability
   (hot reload, dialog closure causing microtasks, etc.) could affect
   the child

3. **In-memory process tracking**: The mcpProcess variable was stored
   in module memory, so Nitro restarts/hot-reloads would lose track of
   the running server

## Solution
1. **Detached spawn mode**: Use `detached: true` + `unref()` to make
   the MCP process completely independent of the parent

2. **PID file persistence**: Track the process via files in /tmp
   instead of in-memory variables, surviving parent restarts

3. **Remove signal handlers**: Delete the SIGINT/SIGTERM/SIGHUP
   handlers that were killing the MCP process

4. **Cross-platform kill**: Use process.kill() instead of execSync
   with taskkill for safer process termination

## Testing
### Test Case 1: Settings Dialog Close
- Open OpenPencil → Settings → Start MCP HTTP Server
- Close settings dialog
- Reopen settings → MCP server still running 

### Test Case 2: Editor Interaction
- Start MCP HTTP Server
- Draw on canvas, create shapes, use all tools
- Check MCP server status → Still running 

### Test Case 3: Browser Version
- Run `npx vite --port 3000`
- Start MCP from browser UI
- Close tab, reopen → Server still running 

### Test Case 4: Electron App
- Start MCP from installed app
- Close app window
- Reopen app → Server still running (tracked via PID file) 

### Test Case 5: Stop/Restart Cycle
- Start → Stop → Start again
- Works correctly 

## Files Changed
- server/utils/mcp-server-manager.ts - Complete rewrite with detached mode
- server/api/ai/mcp-install.ts - Add ESM __dirname polyfill

Fixes #37

Co-authored-by: Kayshen Xu <kayshen.xu@gmail.com>

* fix(canvas): respect node-level theme overrides during variable resolution (#41)

Previously, resolveNodeForCanvas was called with a single activeTheme
for all nodes, ignoring any per-node theme property. This meant nodes
with theme: {"Mode": "Light"} would still render using Dark mode colors.

This fix merges each node's theme property with the default activeTheme,
allowing individual nodes to override theme axes for proper themed
variable resolution.

Fixes design documents where some panels need different theme variants
than the default (e.g., Light mode components in a Dark mode document).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Kayshen Xu <kayshen.xu@gmail.com>

* feat(ai): enhance text processing by stripping tool call blocks

- Added a new function to strip fake tool call blocks emitted by basic-tier models, ensuring cleaner JSON extraction.
- Updated the existing text processing flow to incorporate this new function, improving the handling of non-standard model artifacts.
- Modified orchestrator prompts to disallow function calls and tool call syntax in JSON outputs, enhancing response consistency.

* fix(server): resolve merge conflict and clean up ESM __dirname polyfill

- Removed merge conflict markers from mcp-install.ts and mcp-server-manager.ts.
- Ensured consistent implementation of ESM-compatible __dirname polyfill across both files.

* feat(ai): add node availability check and HTTP fallback for MCP server installation

- Implemented a function to check for the availability of Node.js on the system, enhancing compatibility for environments without Node.js.
- Added a fallback mechanism to install the MCP server using an HTTP URL when Node.js is not found, ensuring functionality remains intact.
- Updated the installation response to include a flag indicating if the HTTP fallback was used, allowing the UI to reflect the server status accurately.
- Enhanced the agent settings dialog to synchronize the MCP server status when the HTTP server is auto-started.

* refactor(canvas): replace Fabric.js with CanvasKit/Skia for enhanced rendering performance

- Removed Fabric.js dependencies and related code, transitioning to CanvasKit/Skia as the primary canvas engine.
- Updated documentation and README files to reflect the new canvas technology.
- Adjusted data flow and component interactions to accommodate the new rendering engine.
- Ensured backward compatibility by retaining legacy Fabric.js files for specific utilities until full removal is feasible.

* refactor(canvas): update layout engine and remove Fabric.js dependencies

- Enhanced the canvas layout engine by integrating Skia and CanvasKit, replacing Fabric.js references.
- Improved type safety by refining type assertions and imports across various components.
- Added a new function to retrieve canvas dimensions, defaulting to 800x600 if no engine is mounted.
- Removed obsolete rendering logic related to Fabric.js, streamlining the layout indicator functionality.
- Updated multiple components to utilize the new canvas size retrieval method, ensuring consistent behavior across the application.

* fix(figma): resolve multiple .fig import rendering issues

- Fix layout properties in preserve mode: only apply auto-layout (gap,
  padding, justify, align) for frames with stackMode, use absolute
  positioning for non-auto-layout frames
- Reverse children order for auto-layout frames in preserve mode to
  match Figma's layout flow order (tree builder sorts descending for
  z-stacking but layout needs ascending)
- Fix instance inlining: always inline symbol content when instance has
  no local children, regardless of override/derived data presence
- Fix derived data mapping: use direct GUID matching (Strategy 0) when
  derived guidPath GUIDs are actual symbol node GUIDs, preventing the
  off-by-one size/transform misassignment from index-based mapping
- Add styleIdForFill/styleIdForStrokeFill resolution: resolve fill style
  references to inline paints before tree building, fixing missing fills
  on 448+ nodes including active menu items and styled elements
- Fix collectImageBlobs to detect JPEG/GIF/WebP in addition to PNG
- Fix instance override application: apply overrides even when derived
  data is absent

* fix(figma): skip opacity=0 nodes during import to fix breadcrumb visibility

Nodes with opacity <= 0 are now skipped in convertChildren, along with
all their descendants.  This fixes the breadcrumb "Page / Page / Page"
text showing through despite the parent items having opacity=0, since
the Skia renderer does not propagate parent opacity to child nodes.

* fix(figma): resolve styleIdForFill in instance override entries

Style references (styleIdForFill, styleIdForStrokeFill) inside
symbolOverrides were not being resolved to inline paints.  This caused
text nodes in overridden instances (e.g. "All Products" card on blue
background) to retain the symbol's default colors instead of the
instance-specific white fills.

The resolveStyleReferences pre-processing step now iterates into each
node's symbolData.symbolOverrides array and resolves style references
there as well.

* fix(figma): apply icon colors and fix arc rendering in .fig import

- Fix donut chart missing segment: swap start/end angles when
  endingAngle < startingAngle instead of adding 2π, producing correct
  clockwise arc equivalents for counter-clockwise Figma arcs

- Fix icon stroke thickness: scale strokeWeight proportionally in
  scaleTreeChildren, applyInstanceOverrides derived sizing, and
  convertVector for Lucide icons rendered smaller than 24×24

- Fix nested instance color overrides: build nestedOverrideMap from
  multi-guid override paths (e.g. instanceGuid/childGuid) and inject
  child-scoped overrides into nested INSTANCE symbolOverrides, enabling
  Dashboard Summary Card icons to receive white/blue stroke colors

- Fix override-only instances: when derivedSymbolData is empty but
  symbolOverrides exist (e.g. sidebar icon instances with stroke color
  overrides but no size changes), fall through to direct GUID matching
  so stroke paints are correctly applied

- Include strokePaints in hasVisualOverrides check so instances with
  stroke-only overrides are properly inlined with their symbol children

* feat(canvas): add vector text rendering with bundled fonts for Figma import

Replace bitmap text fallback with CanvasKit Paragraph API for true vector
text rendering. Bundle 11 popular font families locally via @fontsource
packages to ensure reliable rendering in regions where Google Fonts CDN
is unavailable. Key changes:

- SkiaFontManager: local-first font loading with Google Fonts fallback
- Paragraph API rendering with caching, styled segments, and alignment
- Fixed-width text tolerance to prevent unwanted line wrapping from font
  metric differences between Figma and CanvasKit
- Auto-width text manual alignment offset (center/right) to avoid
  infinite layout width issues
- Figma text mapper: support RAW lineHeight units
- Figma import dialog: pre-load fonts after conversion

* fix(canvas): align text centering with CanvasKit and improve font fallback

- Replace Fabric.js FONT_SIZE_MULT (1.13) with actual paragraph height
  (fontSize * lineHeight) for cross-axis text centering, fixing icon
  vertical misalignment in horizontal layouts
- Remove Fabric-specific optical correction (getTextOpticalCenterYOffset)
  since CanvasKit halfLeading handles this correctly
- Add font fallback chain with separate Inter Ext family for latin-ext
  glyph coverage (₦ U+20A6 etc.)
- Cache failed font loads to prevent repeated fetch attempts per frame
- Skip Google Fonts requests for known system/proprietary fonts
  (PingFang SC, Microsoft YaHei, D-DIN-PRO, etc.)

* fix(figma): recursive nested instance expansion and CJK font support

- Fix 3 bugs in applyInstanceOverrides() that broke deeply nested Figma
  component instances (6+ levels): propagate multi-guid derivedSymbolData
  to nested instances, resolve virtual GUIDs via pkToNodeGuid map, and
  build nestedDerivedMap alongside nestedOverrideMap for recursive expansion
- Bundle Noto Sans SC (chinese-simplified + latin subsets, 400/700 weights)
  for offline CJK vector text rendering without Google Fonts dependency
- Add Noto Sans SC to font fallback chain for CJK glyph coverage
- Add China CDN mirror (fonts.font.im) as fallback when Google Fonts is
  inaccessible, with 4s timeout on primary CDN
- Increase .fig file size limits to 150MB compressed / 300MB decompressed

* fix(canvas): enhance CJK font support and pre-load Noto Sans SC

- Update SkiaEngine to pre-load Noto Sans SC alongside Inter for improved CJK glyph coverage in the font fallback chain, preventing rendering issues with system fonts.
- Modify FigmaImportDialog to ensure Noto Sans SC is included when primary fonts are system fonts, ensuring proper rendering of CJK text.

* refactor(figma): optimize instance override handling with full DFS strategy

- Replace hybrid skip-INSTANCE and expanded DFS with a unified full DFS approach for handling all node types, including INSTANCE nodes.
- Update the mapping logic to ensure correct localID assignments and improve the handling of overflow entries during instance expansion.
- Enhance the overall structure and readability of the applyInstanceOverrides function by consolidating the traversal methods.

* refactor(figma): improve layout mapping and instance override logic

- Enhance the mapFigmaLayout function to conditionally set gap based on stackSpacing and justifyContent, ensuring compatibility with Figma's layout behavior.
- Simplify the applyInstanceOverrides function by removing redundant logic and focusing on a unified full DFS approach for node mapping, improving clarity and performance.
- Ensure derivedSymbolData is correctly replaced with nested data from outer instances, preventing incorrect merging of entries.

* refactor(figma): enhance instance override logic with root-inclusive DFS

- Introduce a root-inclusive depth-first search (DFS) to accurately detect and skip overrides targeting the symbol root during instance processing.
- Update the applyInstanceOverrides function to improve clarity and performance by refining the handling of derived data and overrides.
- Ensure that overrides targeting the root are excluded from child nodes, preventing incorrect application of overrides in nested instances.

---------

Co-authored-by: Fini <fini.yang@gmail.com>
Co-authored-by: Related8919 <191752213+Related8919@users.noreply.github.com>
Co-authored-by: Hrijul Dey <44521405+hr1juldey@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-15 10:01:35 +08:00

32 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

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
  • Install dependencies: bun install
  • Electron dev: bun run electron:dev (starts Vite + Electron together)
  • Electron compile: bun run electron:compile (esbuild electron/ to electron-dist/)
  • Electron build: bun run electron:build (full web build + compile + electron-builder package)

Architecture

OpenPencil is an open-source vector design tool (alternative to Pencil.dev) with a Design-as-Code philosophy. Built as a TanStack Start full-stack React application with Bun runtime. Server API powered by Nitro. Also ships as an Electron desktop app for macOS, Windows, and Linux.

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), TypeScript (strict mode).

Data Flow

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

PenDocument
  ├── pages?: PenPage[]   (id, name, children)
  └── children: PenNode[] (default/single-page fallback)
  • document-store-pages.ts — page CRUD actions: addPage, removePage, renamePage, reorderPage, duplicatePage
  • canvas-store.tsactivePageId state, setActivePageId action
  • canvas-sync-utils.tsforcePageResync() triggers page-aware canvas re-sync
  • page-tabs.tsx — tab bar UI for multi-page navigation with context menu

Design Variables Architecture

PenDocument (source of truth)
  ├── variables: Record<string, VariableDefinition>   ($color-1, $spacing-md, ...)
  ├── themes: Record<string, string[]>                ({Theme-1: ["Default","Dark"]})
  └── children: PenNode[]                             (nodes with $variable refs)
                │
     ┌──────────┴──────────┐
     ▼                      ▼
  Canvas Sync             Code Generation
  resolveNodeForCanvas()  $ref → var(--name)
  $ref → concrete value   CSS Variables block
  • $variable references are preserved in the document store (e.g. $color-1 in fill color)
  • normalize-pen-file.ts does NOT resolve $refs — only fixes format issues
  • 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)
  • Each theme axis has variants; variables can have per-variant values (ThemedValue[])

MCP Layered Design Workflow

External LLMs (Claude Code, Codex, Gemini CLI, etc.) can generate designs via MCP using two approaches:

Single-shot (existing): batch_design or insert_node — generate entire design in one call. Simple but lower fidelity for complex designs.

Layered (new): Break generation into phases, each with focused context and per-section post-processing:

get_design_prompt(section="planning")     → Load planning-specific guidelines
        │
        ▼
design_skeleton(rootFrame, sections)      → Create root + section frames
        │                                    Returns: section IDs, content width,
        │                                    per-section guidelines, suggested roles
        ▼
design_content(sectionId, children) ×N    → Populate each section independently
        │                                    Runs: role resolution, icon resolution,
        │                                    sanitization per section
        ▼
design_refine(rootId)                     → Full-tree validation + auto-fixes
                                             Returns: fix report, layout snapshot

get_design_prompt segmented retrieval: Instead of loading the full ~8K char prompt at once, external LLMs can request focused subsets:

  • schema — PenNode types, fill/stroke format
  • layout — Flexbox layout engine rules
  • roles — Semantic role listing with defaults
  • text — Typography, CJK, copywriting rules
  • style — Visual style policy, palette
  • icons — Available icon names + usage
  • examples — Design examples
  • guidelines — General design tips
  • planning — Design type detection, section decomposition, style guide template, layered workflow guide

design_skeleton section guidelines: The tool generates context-specific content guidelines for each section based on its name/role (nav → navbar layout tips, hero → headline sizing, form → input width rules, etc.), reducing per-call cognitive load.

Key Modules

  • src/canvas/ — Canvas engine (33 files + skia/ subdir with 11 files):
    • skia/ — CanvasKit/Skia WASM renderer (primary canvas engine):
      • skia-canvas.tsx — Main canvas React component (SkiaCanvas), mouse/keyboard event handling, drawing tools, select/drag/resize/rotate, marquee, pen tool, text editing overlay
      • skia-engine.ts — Core rendering engine: SkiaEngine class, syncFromDocument(), viewport transform, node flattening with layout resolution, SpatialIndex integration, zoom/pan, dirty-flag rendering loop
      • skia-renderer.ts — GPU-accelerated draw calls: rectangles, ellipses, text, paths, images, frames, groups, selection handles, guides, agent indicators
      • skia-init.ts — CanvasKit WASM loader with CDN fallback
      • skia-hit-test.tsSpatialIndex for spatial queries: hitTest(), searchRect(), R-tree backed
      • skia-viewport.ts — Viewport math: screenToScene, sceneToScreen, viewportMatrix, zoomToPoint
      • skia-paint-utils.ts — Color parsing, gradient creation, text line wrapping for CanvasKit
      • skia-path-utils.ts — SVG path d string to CanvasKit Path conversion
      • skia-image-loader.ts — Async image loading and caching for CanvasKit
      • skia-overlays.ts — Selection overlays, hover highlights, dimension labels
      • skia-pen-tool.ts — Pen tool implementation for Skia: anchor points, control handles, path building
    • Legacy Fabric.js files (retained for zoomToFitContent utility, pending removal):
      • fabric-canvas.tsx, use-fabric-canvas.ts, canvas-object-factory.ts, canvas-object-sync.ts, canvas-object-modified.ts, canvas-controls.ts
    • Shared modules (used by both Skia and legacy code):
      • canvas-sync-lock.ts — Prevents circular sync loops
      • canvas-sync-utils.tsforcePageResync() utility for page-aware canvas re-sync
      • canvas-constants.ts — Default colors, zoom limits, stroke widths
      • canvas-node-creator.tscreateNodeForTool, isDrawingTool helpers
      • canvas-layout-engine.ts — Auto-layout computation: resolvePadding, getNodeWidth/Height, computeLayoutPositions, Padding interface
      • canvas-text-measure.ts — Text width/height estimation, CJK detection, parseSizing
      • canvas-zoom-cache.ts — Bitmap caching during zoom/pan for performance
      • use-canvas-sync.ts — PenDocument ↔ canvas sync, node flattening, variable resolution via resolveNodeForCanvas()
      • use-canvas-events.ts — Drawing events, shape creation
      • use-canvas-viewport.ts — Wheel zoom, space+drag panning, tool cursor switching
      • use-canvas-selection.ts — Selection sync between canvas and store
      • use-canvas-hover.ts — Hover state management
      • use-canvas-guides.ts / guide-utils.ts — Smart alignment guides with snapping
      • pen-tool.ts — Bezier pen tool (Fabric-based, Skia version in skia/skia-pen-tool.ts)
      • parent-child-transform.ts — Propagates parent transforms to children proportionally
      • use-dimension-label.ts / use-frame-labels.ts / use-entered-frame-overlay.ts / use-layout-indicator.ts — Visual overlays
      • insertion-indicator.ts / drag-into-layout.ts / drag-reparent.ts / layout-reorder.ts — Drag-and-drop support
      • selection-context.ts — Multi-select context management
      • agent-indicator.ts / use-agent-indicators.ts — Agent visual indicators on canvas during concurrent AI generation
  • src/variables/ — Design variables system (2 files):
    • resolve-variables.ts — Core resolution utilities: resolveVariableRef, resolveNodeForCanvas, getDefaultTheme, isVariableRef; resolves $variable references to concrete values for canvas rendering with circular reference guards
    • replace-refs.tsreplaceVariableRefsInTree: recursively walk node tree to replace/resolve $refs when renaming or deleting variables (covers opacity, gap, padding, fills, strokes, effects, text)
  • src/stores/ — Zustand stores (9 files):
    • canvas-store.ts — UI/tool/selection/viewport/clipboard/interaction state, variablesPanelOpen toggle, activePageId, figmaImportDialogOpen
    • document-store.ts — PenDocument tree CRUD: addNode, updateNode, removeNode, moveNode, reorderNode, duplicateNode, groupNodes, ungroupNode, toggleVisibility, toggleLock, scaleDescendantsInStore, rotateDescendantsInStore, getNodeById, getParentOf, getFlatNodes, isDescendantOf; Variable CRUD: setVariable, removeVariable, renameVariable, setThemes (all with history support)
    • document-store-pages.ts — Page actions extracted from document-store: addPage, removePage, renamePage, reorderPage, duplicatePage
    • document-tree-utils.ts — Pure tree helpers extracted from document-store: findNodeInTree, findParentInTree, removeNodeFromTree, updateNodeInTree, flattenNodes, insertNodeInTree, isDescendantOf, getNodeBounds, findClearX, scaleChildrenInPlace, rotateChildrenInPlace, createEmptyDocument, DEFAULT_FRAME_ID
    • history-store.ts — Undo/redo (max 300 states), batch mode for grouped operations
    • ai-store.ts — Chat messages, streaming state, generated code, model selection, pendingAttachments for image uploads
    • agent-settings-store.ts — AI provider config (Anthropic/OpenAI/OpenCode/Copilot), MCP CLI integrations (Claude Code, Codex CLI, Gemini CLI, OpenCode CLI, Kiro CLI, Copilot CLI), localStorage persistence
    • uikit-store.ts — UIKit management: imported kits, component browser state (search, category filters), localStorage persistence
    • theme-preset-store.ts — Theme preset management and persistence
  • src/types/ — Type system (9 files):
    • pen.ts — PenDocument/PenNode (frame, group, rectangle, ellipse, line, polygon, path, text, image, ref), ContainerProps, PenPage; PenDocument.variables, PenDocument.themes, PenDocument.pages
    • canvas.ts — ToolType (select, frame, rectangle, ellipse, line, polygon, path, text, hand), ViewportState, SelectionState, CanvasInteraction
    • styles.ts — PenFill (solid, linear_gradient, radial_gradient), PenStroke, PenEffect (shadow, blur), BlendMode, StyledTextSegment
    • variables.tsVariableDefinition (type + value), ThemedValue (value per theme), VariableValue
    • uikit.ts — UIKit, KitComponent, ComponentCategory types for reusable component organization and browsing
    • agent-settings.ts — AI provider config types (AIProviderType: anthropic/openai/opencode/copilot, AIProviderConfig, MCPCliIntegration, GroupedModel)
    • electron.d.ts — Electron IPC bridge types (file dialogs, save operations, updater: UpdaterState/UpdaterStatus, getState/checkForUpdates/quitAndInstall/onStateChange)
    • theme-preset.ts — Type definitions for theme presets
    • opencode-sdk.d.ts — Type declarations for @opencode-ai/sdk
  • src/components/editor/ — Editor UI (9 files): editor-layout, toolbar (with variables panel toggle), boolean-toolbar (contextual floating toolbar for union/subtract/intersect, shown when 2+ compatible shapes selected), tool-button, shape-tool-dropdown (rectangle/ellipse/line/path + icon picker + image import), top-bar (with AgentStatusButton), status-bar, page-tabs (multi-page navigation with context menu), update-ready-banner (Electron auto-updater notification)
  • src/components/panels/ — Panels (29 files):
    • layer-panel.tsx / layer-item.tsx / layer-context-menu.tsx — Tree view with drag-and-drop reordering and drop-into-children (above/below/inside), visibility/lock toggles, context menu, rename
    • property-panel.tsx — Unified property panel
    • fill-section.tsx — Solid + gradient fill, variable picker integration for color binding
    • stroke-section.tsx — Stroke color/width/dash, variable picker for stroke color binding
    • corner-radius-section.tsx — Unified or 4-point corner radius
    • size-section.tsx — Position, size, rotation
    • text-section.tsx — Font, size, weight, spacing, alignment
    • text-layout-section.tsx — Text node layout controls (auto/fixed-width/fixed-height modes, fill width/height)
    • icon-section.tsx — Icon property panel section: current icon name, library dropdown, icon picker
    • effects-section.tsx — Shadow and blur
    • export-section.tsx — Per-layer export to PNG/SVG with scale options (1x/2x/3x)
    • layout-section.tsx — Auto-layout (none/vertical/horizontal), gap, padding, justify, align; variable picker for gap/padding binding
    • layout-padding-section.tsx — Extracted padding controls: single/axis/T-R-B-L modes with popover mode switcher
    • appearance-section.tsx — Opacity, visibility, lock, flip; variable picker for opacity binding
    • ai-chat-panel.tsx / chat-message.tsx — AI chat with markdown, design block collapse, apply design, image attachment upload (paperclip button, preview strip, 5MB/4-image limit)
    • ai-chat-handlers.tsuseChatHandlers hook, isDesignRequest, buildContextString helpers extracted from ai-chat-panel
    • ai-chat-checklist.tsxFixedChecklist component for AI generation progress display
    • code-panel.tsx — Code generation output (React/Tailwind, HTML/CSS, CSS Variables, Vue, Svelte, Flutter, SwiftUI, Compose, React Native)
    • image-section.tsx — Image property panel section
    • node-preview-svg.tsx — Node SVG preview component
    • right-panel.tsx — Right panel container
    • component-browser-panel.tsx / component-browser-grid.tsx / component-browser-card.tsx — Resizable floating panel for browsing, importing, and inserting UIKit components with category tabs and search
    • variables-panel.tsx — Design variables management: theme axes as tabs, variant columns, resizable floating panel, add/rename/delete themes and variants
    • variable-row.tsx — Individual variable row: type icon, editable name, per-theme-variant value cells (color picker, number input, text input), context menu
  • src/components/shared/ — Reusable UI (10 files): ColorPicker, NumberInput, SectionHeader, ExportDialog, SaveDialog, AgentSettingsDialog, IconPickerDialog, VariablePicker, FigmaImportDialog, LanguageSelector
  • src/components/icons/ — Provider/brand logos: ClaudeLogo, OpenAILogo, OpenCodeLogo, CopilotLogo, FigmaLogo
  • src/components/ui/ — shadcn/ui primitives: Button, Select, Separator, Slider, Switch, Toggle, Tooltip
  • src/services/ai/ — AI services (31 files + role-definitions/ subdir with 9 files):
    • ai-service.ts — Main AI chat API wrapper, model negotiation, provider selection
    • ai-prompts.ts — System prompts for design generation, context building
    • ai-types.tsChatMessage (with attachments?: ChatAttachment[]), ChatAttachment (id, name, mediaType, data, size), AIDesignRequest, OrchestratorPlan, streaming response types
    • ai-runtime-config.ts — Configuration constants for AI timeouts, thinking modes, effort levels, prompt length limits
    • model-profiles.ts — Model capability profiles: adapts thinking mode, effort, timeouts, prompt complexity per model tier (full/standard/basic)
    • agent-identity.ts — Agent identity assignment (color, name) for concurrent multi-agent generation
    • design-generator.ts — Top-level generateDesign/generateDesignModification with orchestrator fallback, re-exports from design-parser and design-canvas-ops
    • design-parser.ts — Pure JSON/JSONL parsing: extractJsonFromResponse, extractStreamingNodes, parseJsonlToTree, node validation and scoring
    • design-canvas-ops.ts — Canvas mutation operations: insertStreamingNode, applyNodesToCanvas, upsertNodesToCanvas, animateNodesToCanvas, generation state management, sanitization and heuristics
    • design-node-sanitization.ts — Node cloning and merging utilities: deepCloneNode, setNodeChildren, mergeNodeForProgressiveUpsert (extracted from design-canvas-ops)
    • design-animation.ts — Fade-in animation coordination for generated design nodes
    • design-validation.ts — Post-generation screenshot validation using vision API to detect and auto-fix visual issues
    • design-validation-fixes.ts — Auto-fix strategies for validation-detected issues
    • design-pre-validation.ts — Pre-validation heuristics before vision API call
    • design-screenshot.ts — Canvas screenshot capture for validation pipeline
    • design-type-presets.ts — Design type detection and section presets (landing page, dashboard, mobile, etc.)
    • design-code-generator.ts — AI-powered code generation from design nodes
    • design-code-prompts.ts — Prompts for design-to-code generation
    • design-system-generator.ts — Design system token extraction from generated designs
    • design-system-prompts.ts — Prompts for design system generation
    • html-renderer.ts — PenNode tree to HTML rendering for validation screenshots
    • visual-ref-orchestrator.ts — Visual reference-based orchestration for image-guided generation
    • generation-utils.ts — Pure utilities for text measurement, size/padding parsing, phone placeholder generation, color extraction
    • icon-resolver.ts — Auto-resolves AI-generated icon path nodes by name to verified Lucide SVG paths
    • role-resolver.ts — Registry-based system for applying role-specific defaults (button padding, card gaps) and tree post-pass fixes
    • role-definitions/ — Modular role definition files: index, content, display, interactive, layout, navigation, media, typography, table
    • orchestrator.ts — Orchestrator entry point: executeOrchestration, callOrchestrator, plan parsing
    • orchestrator-sub-agent.ts — Sub-agent execution: executeSubAgentsSequentially, executeSubAgent, prompt building, retry/fallback logic
    • orchestrator-progress.tsemitProgress, buildFinalStepTags for streaming progress updates
    • orchestrator-prompts.ts — Ultra-lightweight orchestrator prompt for spatial decomposition
    • orchestrator-prompt-optimizer.ts — Prompt preparation, compression, timeout calculation, fallback plan generation
    • context-optimizer.ts — Chat history trimming, sliding window to prevent unbounded context growth
  • src/services/figma/ — Figma .fig file import pipeline (14 files):
    • fig-parser.ts — Binary .fig file parser
    • figma-types.ts — Figma internal type definitions
    • figma-node-mapper.ts — Maps Figma nodes to PenNodes
    • figma-fill-mapper.ts — Converts Figma fills to PenFill
    • figma-stroke-mapper.ts — Converts Figma strokes to PenStroke
    • figma-effect-mapper.ts — Converts Figma effects to PenEffect
    • figma-layout-mapper.ts — Maps Figma auto-layout to PenNode layout props
    • figma-text-mapper.ts — Converts Figma text styles
    • figma-vector-decoder.ts — Decodes Figma vector geometry
    • figma-color-utils.ts — Color space conversion utilities
    • figma-image-resolver.ts — Resolves image blob references
    • figma-clipboard.ts — Figma clipboard paste handling
    • figma-node-converters.ts — Figma node conversion utilities
    • figma-tree-builder.ts — Figma document tree building
  • src/services/codegen/ — Multi-platform code generators (9 files, output var(--name) for $variable refs):
    • react-generator.ts — React + Tailwind CSS
    • html-generator.ts — HTML + CSS
    • css-variables-generator.ts — CSS Variables from design tokens
    • vue-generator.ts — Vue 3 + CSS
    • svelte-generator.ts — Svelte + CSS
    • flutter-generator.ts — Flutter/Dart
    • swiftui-generator.ts — SwiftUI
    • compose-generator.ts — Android Jetpack Compose
    • react-native-generator.ts — React Native
  • src/hooks/ — Hooks (5 files):
    • use-keyboard-shortcuts.ts — Global keyboard event handling: tools, clipboard, undo/redo, save, select all, delete, arrow nudge, z-order, boolean operations (Cmd+Alt+U/S/I)
    • use-electron-menu.ts — Electron native menu IPC listener: dispatches menu actions (new, open, save, save-as, undo, redo, etc.) to Zustand stores; also handles onOpenFile for .op file association
    • use-figma-paste.ts — Handle Figma clipboard paste into canvas
    • use-file-drop.ts — Handle file drag-and-drop onto canvas
    • use-mcp-sync.ts — MCP live canvas synchronization hook
  • src/lib/ — Utility functions (utils.ts with cn() for class merging)
  • src/uikit/ — UI kit system (3 files + kits/ subdir):
    • built-in-registry.ts — Default built-in UIKit with standard UI components
    • kit-import-export.ts — Import/export UIKits from .pen files with variable reference collection
    • kit-utils.ts — UIKit utilities: extract components from documents, find reusable nodes, deep clone
    • kits/ — Default kit data: default-kit.ts, default-kit-meta.ts
  • src/mcp/ — MCP server integration (2 files + tools/ and utils/ subdirs):
    • server.ts — MCP server entry point, tool registration (stdio + HTTP modes)
    • document-manager.ts — MCP utility for reading, writing, and caching PenDocuments from disk; live canvas sync via Nitro API
    • tools/ — Individual MCP tool implementations:
      • Core: open-document.ts, batch-get.ts, get-selection.ts, batch-design.ts (DSL operations), node-crud.ts (insert/update/delete/move/copy/replace)
      • Layout: snapshot-layout.ts, find-empty-space.ts, import-svg.ts
      • Variables: variables.ts, theme-presets.ts
      • Pages: pages.ts (add/remove/rename/reorder/duplicate)
      • Layered design: design-prompt.ts (segmented retrieval), design-skeleton.ts, design-content.ts, design-refine.ts, layered-design-defs.ts
    • utils/ — Shared utilities: id.ts, node-operations.ts (page-aware getDocChildren/setDocChildren), sanitize.ts, svg-node-parser.ts
  • src/utils/ — File operations (save/open .pen), export (PNG/SVG), node clone, pen file normalization (format fixes only, preserves $variable refs), SVG parser (import SVG to editable PenNodes), syntax highlight, boolean operations (union/subtract/intersect via Paper.js), app-storage.ts (application storage/persistence), arc-path.ts (SVG arc utilities), theme-preset-io.ts (theme preset file I/O)
  • src/constants/ — Application constants: app.ts (MCP default port, app-level config)
  • src/i18n/ — Internationalization setup and locale files
  • server/api/ai/ — Nitro server API (8 files): chat.ts (streaming SSE with thinking state, multimodal image attachments per provider), generate.ts (non-streaming generation), connect-agent.ts (Claude Code/Codex CLI/OpenCode/Copilot connection), models.ts (model definitions), validate.ts (vision-based post-generation validation), mcp-install.ts (MCP server install/uninstall into CLI tool configs; auto-detects node availability — if missing, falls back to HTTP URL config { type: "http", url } and auto-starts the MCP HTTP server), install-agent.ts (agent installation endpoint), icon.ts (icon name → SVG path resolution via local Iconify sets). Supports Anthropic API key or Claude Agent SDK (local OAuth) as dual providers
  • server/utils/ — Server utilities (7 files):
    • resolve-claude-cli.ts — Resolves standalone claude binary path (handles Nitro bundling issues with SDK's import.meta.url)
    • resolve-claude-agent-env.ts — Builds Claude Agent SDK environment: merges ~/.claude/settings.json env, validates ANTHROPIC_CUSTOM_HEADERS, handles auth token compat
    • opencode-client.ts — Shared OpenCode client manager, reuses server on port 4096 with random port fallback
    • codex-client.ts — Codex CLI client wrapper with JSON streaming, thinking mode support, timeout handling, optional imageFiles for vision queries
    • copilot-client.ts — Resolves standalone copilot binary path to avoid Bun's node:sqlite incompatibility with bundled CLI
    • mcp-server-manager.ts — MCP HTTP server lifecycle management: spawn/track/stop detached process, PID file persistence
    • mcp-sync-state.ts — In-memory MCP state: current document/selection, SSE broadcast to connected clients

CanvasKit/Skia Architecture

  • GPU-accelerated WASM rendering — CanvasKit (Skia compiled to WASM) renders all canvas content via WebGL surface
  • SkiaEngine class (skia-engine.ts) is the core: owns the render loop, viewport transforms, node flattening, and SpatialIndex for hit testing
  • Dirty-flag renderingmarkDirty() schedules a requestAnimationFrame redraw; no continuous rendering loop
  • Node flatteningsyncFromDocument() walks the PenDocument tree, resolves auto-layout positions via canvas-layout-engine.ts, and produces flat RenderNode[] with absolute coordinates
  • SpatialIndex (skia-hit-test.ts) — R-tree backed spatial queries for hitTest() (click) and searchRect() (marquee selection)
  • Coordinate conversionscreenToScene() / sceneToScreen() in skia-viewport.ts handle viewport ↔ scene transforms
  • Bitmap cachingcanvas-zoom-cache.ts caches rendered frames during zoom/pan for smoother interaction
  • Event handling — all mouse/keyboard events are handled directly in skia-canvas.tsx (no Fabric abstractions)
  • Parent-child transforms — nodes are flattened to absolute coordinates; parent-child-transform.ts propagates transforms to descendants during drag/scale/rotate
  • Legacy Fabric.jsfabric package remains as a dependency for zoomToFitContent utility; canvas rendering no longer uses Fabric

Routing

File-based routing via TanStack Router. Routes in src/routes/, auto-generated tree in src/routeTree.gen.ts (do not edit).

  • / — Landing page
  • /editor — Main design editor

Path Aliases

@/* maps to ./src/* (configured in tsconfig.json and vite.config.ts).

Styling

Tailwind CSS v4 imported via src/styles.css. UI primitives from shadcn/ui (src/components/ui/). Icons from lucide-react. shadcn/ui config in components.json.

Electron Desktop App

  • electron/main.ts — Main process: window creation, Nitro server fork, IPC for native file dialogs, .op file association handling (open-file event on macOS, CLI args + single-instance lock on Windows/Linux)
  • electron/preload.ts — Context bridge for renderer ↔ main IPC (file dialogs, menu actions, updater state, onOpenFile/readFile for file association)
  • electron/app-menu.ts — Native application menu configuration (File, Edit, View, Help)
  • electron/auto-updater.ts — Auto-updater implementation: checks GitHub Releases on startup and periodically
  • electron/constants.ts — Electron-specific constants
  • electron/logger.ts — Main process logging
  • electron-builder.yml — Packaging config: macOS (dmg/zip), Windows (nsis/portable), Linux (AppImage/deb), .op file association (fileAssociations)
  • scripts/electron-dev.ts — Dev workflow: starts Vite → waits for port 3000 → compiles electron/ with esbuild → launches Electron
  • Build flow: BUILD_TARGET=electron bun run buildbun run electron:compilenpx electron-builder
  • In production, Nitro server is forked as a child process on a random port; Electron loads http://127.0.0.1:{port}/editor
  • Auto-updater checks GitHub Releases on startup and every hour; update-ready-banner.tsx shows download progress and "Restart & Install" prompt
  • File association: .op files are registered as OpenPencil documents via fileAssociations in electron-builder.yml. On macOS the open-file app event handles double-click/drag; on Windows/Linux requestSingleInstanceLock + second-instance event forwards CLI args to the existing window. Pending file paths are queued until the renderer is ready, then sent via file:open IPC channel. The renderer (use-electron-menu.ts) listens via onOpenFile, reads the file through file:read IPC, and calls loadDocument.

CI / CD

  • .github/workflows/ci.yml — Push/PR: type check (tsc --noEmit), tests (vitest), web build
  • .github/workflows/build-electron.yml — Tag push (v*) or manual: builds Electron for macOS, Windows, Linux in parallel, creates draft GitHub Release with all artifacts

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 format:

<type>(<scope>): <subject>

<body>

Type

  • feat — New feature
  • fix — Bug fix
  • refactor — Refactoring (no behavior change)
  • perf — Performance optimization
  • style — Code formatting (no logic change)
  • docs — Documentation
  • test — Tests
  • chore — Build / tooling / dependency changes

Scope

By module: editor, canvas, panels, history, ai, codegen, store, types, variables, figma, mcp, electron.

Rules

  • Subject in English, lowercase start, no period, imperative mood (e.g. add, fix, remove).
  • Body is optional; explain why not what.
  • One commit per change. Do not mix unrelated changes in a single commit.

License

MIT License. See LICENSE for details.