* 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>
32 KiB
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.tsprevents 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,duplicatePagecanvas-store.ts—activePageIdstate,setActivePageIdactioncanvas-sync-utils.ts—forcePageResync()triggers page-aware canvas re-syncpage-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
$variablereferences are preserved in the document store (e.g.$color-1in fill color)normalize-pen-file.tsdoes NOT resolve$refs— only fixes format issuesresolveNodeForCanvas()resolves$refson-the-fly before CanvasKit rendering- Code generators output
var(--name)for$refvalues - 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 formatlayout— Flexbox layout engine rulesroles— Semantic role listing with defaultstext— Typography, CJK, copywriting rulesstyle— Visual style policy, paletteicons— Available icon names + usageexamples— Design examplesguidelines— General design tipsplanning— 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 overlayskia-engine.ts— Core rendering engine:SkiaEngineclass,syncFromDocument(), viewport transform, node flattening with layout resolution,SpatialIndexintegration, zoom/pan, dirty-flag rendering loopskia-renderer.ts— GPU-accelerated draw calls: rectangles, ellipses, text, paths, images, frames, groups, selection handles, guides, agent indicatorsskia-init.ts— CanvasKit WASM loader with CDN fallbackskia-hit-test.ts—SpatialIndexfor spatial queries:hitTest(),searchRect(), R-tree backedskia-viewport.ts— Viewport math:screenToScene,sceneToScreen,viewportMatrix,zoomToPointskia-paint-utils.ts— Color parsing, gradient creation, text line wrapping for CanvasKitskia-path-utils.ts— SVG pathdstring to CanvasKit Path conversionskia-image-loader.ts— Async image loading and caching for CanvasKitskia-overlays.ts— Selection overlays, hover highlights, dimension labelsskia-pen-tool.ts— Pen tool implementation for Skia: anchor points, control handles, path building
- Legacy Fabric.js files (retained for
zoomToFitContentutility, 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 loopscanvas-sync-utils.ts—forcePageResync()utility for page-aware canvas re-synccanvas-constants.ts— Default colors, zoom limits, stroke widthscanvas-node-creator.ts—createNodeForTool,isDrawingToolhelperscanvas-layout-engine.ts— Auto-layout computation:resolvePadding,getNodeWidth/Height,computeLayoutPositions,Paddinginterfacecanvas-text-measure.ts— Text width/height estimation, CJK detection,parseSizingcanvas-zoom-cache.ts— Bitmap caching during zoom/pan for performanceuse-canvas-sync.ts— PenDocument ↔ canvas sync, node flattening, variable resolution viaresolveNodeForCanvas()use-canvas-events.ts— Drawing events, shape creationuse-canvas-viewport.ts— Wheel zoom, space+drag panning, tool cursor switchinguse-canvas-selection.ts— Selection sync between canvas and storeuse-canvas-hover.ts— Hover state managementuse-canvas-guides.ts/guide-utils.ts— Smart alignment guides with snappingpen-tool.ts— Bezier pen tool (Fabric-based, Skia version inskia/skia-pen-tool.ts)parent-child-transform.ts— Propagates parent transforms to children proportionallyuse-dimension-label.ts/use-frame-labels.ts/use-entered-frame-overlay.ts/use-layout-indicator.ts— Visual overlaysinsertion-indicator.ts/drag-into-layout.ts/drag-reparent.ts/layout-reorder.ts— Drag-and-drop supportselection-context.ts— Multi-select context managementagent-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$variablereferences to concrete values for canvas rendering with circular reference guardsreplace-refs.ts—replaceVariableRefsInTree: recursively walk node tree to replace/resolve$refswhen 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,variablesPanelOpentoggle,activePageId,figmaImportDialogOpendocument-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,duplicatePagedocument-tree-utils.ts— Pure tree helpers extracted from document-store:findNodeInTree,findParentInTree,removeNodeFromTree,updateNodeInTree,flattenNodes,insertNodeInTree,isDescendantOf,getNodeBounds,findClearX,scaleChildrenInPlace,rotateChildrenInPlace,createEmptyDocument,DEFAULT_FRAME_IDhistory-store.ts— Undo/redo (max 300 states), batch mode for grouped operationsai-store.ts— Chat messages, streaming state, generated code, model selection,pendingAttachmentsfor image uploadsagent-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 persistenceuikit-store.ts— UIKit management: imported kits, component browser state (search, category filters), localStorage persistencetheme-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.pagescanvas.ts— ToolType (select, frame, rectangle, ellipse, line, polygon, path, text, hand), ViewportState, SelectionState, CanvasInteractionstyles.ts— PenFill (solid, linear_gradient, radial_gradient), PenStroke, PenEffect (shadow, blur), BlendMode, StyledTextSegmentvariables.ts—VariableDefinition(type + value),ThemedValue(value per theme),VariableValueuikit.ts— UIKit, KitComponent, ComponentCategory types for reusable component organization and browsingagent-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 presetsopencode-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 (withAgentStatusButton), 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, renameproperty-panel.tsx— Unified property panelfill-section.tsx— Solid + gradient fill, variable picker integration for color bindingstroke-section.tsx— Stroke color/width/dash, variable picker for stroke color bindingcorner-radius-section.tsx— Unified or 4-point corner radiussize-section.tsx— Position, size, rotationtext-section.tsx— Font, size, weight, spacing, alignmenttext-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 pickereffects-section.tsx— Shadow and blurexport-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 bindinglayout-padding-section.tsx— Extracted padding controls: single/axis/T-R-B-L modes with popover mode switcherappearance-section.tsx— Opacity, visibility, lock, flip; variable picker for opacity bindingai-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.ts—useChatHandlershook,isDesignRequest,buildContextStringhelpers extracted from ai-chat-panelai-chat-checklist.tsx—FixedChecklistcomponent for AI generation progress displaycode-panel.tsx— Code generation output (React/Tailwind, HTML/CSS, CSS Variables, Vue, Svelte, Flutter, SwiftUI, Compose, React Native)image-section.tsx— Image property panel sectionnode-preview-svg.tsx— Node SVG preview componentright-panel.tsx— Right panel containercomponent-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 searchvariables-panel.tsx— Design variables management: theme axes as tabs, variant columns, resizable floating panel, add/rename/delete themes and variantsvariable-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, LanguageSelectorsrc/components/icons/— Provider/brand logos: ClaudeLogo, OpenAILogo, OpenCodeLogo, CopilotLogo, FigmaLogosrc/components/ui/— shadcn/ui primitives: Button, Select, Separator, Slider, Switch, Toggle, Tooltipsrc/services/ai/— AI services (31 files +role-definitions/subdir with 9 files):ai-service.ts— Main AI chat API wrapper, model negotiation, provider selectionai-prompts.ts— System prompts for design generation, context buildingai-types.ts—ChatMessage(withattachments?: ChatAttachment[]),ChatAttachment(id, name, mediaType, data, size),AIDesignRequest,OrchestratorPlan, streaming response typesai-runtime-config.ts— Configuration constants for AI timeouts, thinking modes, effort levels, prompt length limitsmodel-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 generationdesign-generator.ts— Top-levelgenerateDesign/generateDesignModificationwith orchestrator fallback, re-exports from design-parser and design-canvas-opsdesign-parser.ts— Pure JSON/JSONL parsing:extractJsonFromResponse,extractStreamingNodes,parseJsonlToTree, node validation and scoringdesign-canvas-ops.ts— Canvas mutation operations:insertStreamingNode,applyNodesToCanvas,upsertNodesToCanvas,animateNodesToCanvas, generation state management, sanitization and heuristicsdesign-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 nodesdesign-validation.ts— Post-generation screenshot validation using vision API to detect and auto-fix visual issuesdesign-validation-fixes.ts— Auto-fix strategies for validation-detected issuesdesign-pre-validation.ts— Pre-validation heuristics before vision API calldesign-screenshot.ts— Canvas screenshot capture for validation pipelinedesign-type-presets.ts— Design type detection and section presets (landing page, dashboard, mobile, etc.)design-code-generator.ts— AI-powered code generation from design nodesdesign-code-prompts.ts— Prompts for design-to-code generationdesign-system-generator.ts— Design system token extraction from generated designsdesign-system-prompts.ts— Prompts for design system generationhtml-renderer.ts— PenNode tree to HTML rendering for validation screenshotsvisual-ref-orchestrator.ts— Visual reference-based orchestration for image-guided generationgeneration-utils.ts— Pure utilities for text measurement, size/padding parsing, phone placeholder generation, color extractionicon-resolver.ts— Auto-resolves AI-generated icon path nodes by name to verified Lucide SVG pathsrole-resolver.ts— Registry-based system for applying role-specific defaults (button padding, card gaps) and tree post-pass fixesrole-definitions/— Modular role definition files: index, content, display, interactive, layout, navigation, media, typography, tableorchestrator.ts— Orchestrator entry point:executeOrchestration,callOrchestrator, plan parsingorchestrator-sub-agent.ts— Sub-agent execution:executeSubAgentsSequentially,executeSubAgent, prompt building, retry/fallback logicorchestrator-progress.ts—emitProgress,buildFinalStepTagsfor streaming progress updatesorchestrator-prompts.ts— Ultra-lightweight orchestrator prompt for spatial decompositionorchestrator-prompt-optimizer.ts— Prompt preparation, compression, timeout calculation, fallback plan generationcontext-optimizer.ts— Chat history trimming, sliding window to prevent unbounded context growth
src/services/figma/— Figma.figfile import pipeline (14 files):fig-parser.ts— Binary.figfile parserfigma-types.ts— Figma internal type definitionsfigma-node-mapper.ts— Maps Figma nodes to PenNodesfigma-fill-mapper.ts— Converts Figma fills to PenFillfigma-stroke-mapper.ts— Converts Figma strokes to PenStrokefigma-effect-mapper.ts— Converts Figma effects to PenEffectfigma-layout-mapper.ts— Maps Figma auto-layout to PenNode layout propsfigma-text-mapper.ts— Converts Figma text stylesfigma-vector-decoder.ts— Decodes Figma vector geometryfigma-color-utils.ts— Color space conversion utilitiesfigma-image-resolver.ts— Resolves image blob referencesfigma-clipboard.ts— Figma clipboard paste handlingfigma-node-converters.ts— Figma node conversion utilitiesfigma-tree-builder.ts— Figma document tree building
src/services/codegen/— Multi-platform code generators (9 files, outputvar(--name)for$variablerefs):react-generator.ts— React + Tailwind CSShtml-generator.ts— HTML + CSScss-variables-generator.ts— CSS Variables from design tokensvue-generator.ts— Vue 3 + CSSsvelte-generator.ts— Svelte + CSSflutter-generator.ts— Flutter/Dartswiftui-generator.ts— SwiftUIcompose-generator.ts— Android Jetpack Composereact-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 handlesonOpenFilefor.opfile associationuse-figma-paste.ts— Handle Figma clipboard paste into canvasuse-file-drop.ts— Handle file drag-and-drop onto canvasuse-mcp-sync.ts— MCP live canvas synchronization hook
src/lib/— Utility functions (utils.tswithcn()for class merging)src/uikit/— UI kit system (3 files +kits/subdir):built-in-registry.ts— Default built-in UIKit with standard UI componentskit-import-export.ts— Import/export UIKits from .pen files with variable reference collectionkit-utils.ts— UIKit utilities: extract components from documents, find reusable nodes, deep clonekits/— Default kit data:default-kit.ts,default-kit-meta.ts
src/mcp/— MCP server integration (2 files +tools/andutils/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 APItools/— 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
- Core:
utils/— Shared utilities:id.ts,node-operations.ts(page-awaregetDocChildren/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$variablerefs), 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 filesserver/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-detectsnodeavailability — 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 providersserver/utils/— Server utilities (7 files):resolve-claude-cli.ts— Resolves standaloneclaudebinary path (handles Nitro bundling issues with SDK'simport.meta.url)resolve-claude-agent-env.ts— Builds Claude Agent SDK environment: merges~/.claude/settings.jsonenv, validatesANTHROPIC_CUSTOM_HEADERS, handles auth token compatopencode-client.ts— Shared OpenCode client manager, reuses server on port 4096 with random port fallbackcodex-client.ts— Codex CLI client wrapper with JSON streaming, thinking mode support, timeout handling, optionalimageFilesfor vision queriescopilot-client.ts— Resolves standalonecopilotbinary path to avoid Bun'snode:sqliteincompatibility with bundled CLImcp-server-manager.ts— MCP HTTP server lifecycle management: spawn/track/stop detached process, PID file persistencemcp-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, andSpatialIndexfor hit testing - Dirty-flag rendering —
markDirty()schedules arequestAnimationFrameredraw; no continuous rendering loop - Node flattening —
syncFromDocument()walks the PenDocument tree, resolves auto-layout positions viacanvas-layout-engine.ts, and produces flatRenderNode[]with absolute coordinates - SpatialIndex (
skia-hit-test.ts) — R-tree backed spatial queries forhitTest()(click) andsearchRect()(marquee selection) - Coordinate conversion —
screenToScene()/sceneToScreen()inskia-viewport.tshandle viewport ↔ scene transforms - Bitmap caching —
canvas-zoom-cache.tscaches 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.tspropagates transforms to descendants during drag/scale/rotate - Legacy Fabric.js —
fabricpackage remains as a dependency forzoomToFitContentutility; 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,.opfile association handling (open-fileevent 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/readFilefor 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 periodicallyelectron/constants.ts— Electron-specific constantselectron/logger.ts— Main process loggingelectron-builder.yml— Packaging config: macOS (dmg/zip), Windows (nsis/portable), Linux (AppImage/deb),.opfile 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 build→bun run electron:compile→npx 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.tsxshows download progress and "Restart & Install" prompt - File association:
.opfiles are registered as OpenPencil documents viafileAssociationsinelectron-builder.yml. On macOS theopen-fileapp event handles double-click/drag; on Windows/LinuxrequestSingleInstanceLock+second-instanceevent forwards CLI args to the existing window. Pending file paths are queued until the renderer is ready, then sent viafile:openIPC channel. The renderer (use-electron-menu.ts) listens viaonOpenFile, reads the file throughfile:readIPC, and callsloadDocument.
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.
.tsand.tsxfiles 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 likegray-*,blue-*. - Toolbar button active state uses
isActiveconditional className (bg-primary text-primary-foreground), not Radix Toggle'sdata-[state=on]:selector (has twMerge conflicts).
Git Commit Convention
Use Conventional Commits format:
<type>(<scope>): <subject>
<body>
Type
feat— New featurefix— Bug fixrefactor— Refactoring (no behavior change)perf— Performance optimizationstyle— Code formatting (no logic change)docs— Documentationtest— Testschore— 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.