* feat(boolean-operations): implement boolean operations in the editor - Added a new BooleanToolbar component for union, subtract, and intersect operations. - Integrated boolean operations into the layer context menu and keyboard shortcuts. - Enhanced the editor layout to include the boolean toolbar for improved user interaction. - Updated internationalization support with new translation keys for boolean operations. - Bumped version to 0.3.0 to reflect the addition of these features. * refactor(editor): update editor layout and panels for improved functionality - Replaced the PropertyPanel with a new RightPanel that includes both Property and Code panels. - Removed the CodePanel from the main editor layout and integrated it into the RightPanel. - Updated keyboard shortcuts to switch the right panel to the code tab. - Enhanced the LayerPanel with a resizable width feature for better user experience. - Added internationalization support for new right panel labels and code panel features. - Introduced new code generation capabilities for various frameworks in the CodePanel. - Improved overall layout structure for better responsiveness and usability. * feat(electron): implement .op file association and enhance file handling - Added support for .op file association in electron-builder, allowing OpenPencil documents to be opened directly from the file system. - Implemented IPC handlers for opening and reading .op files, ensuring proper loading of document content. - Enhanced the main process to handle file opening events on macOS and single-instance locking on Windows/Linux. - Updated the renderer to listen for file open events and load documents accordingly. - Improved README to reflect new file association feature. * fix(canvas): improve layout accuracy for AI-generated designs - Unify lineHeight default via canonical defaultLineHeight() function - Unify text measurement by removing duplicate estimators in generation-utils - Fix optical centering formula to scale proportionally with fontSize - Round layout positions to whole pixels to prevent sub-pixel artifacts - Recursively sanitize nested x/y in streaming layout containers - Fix input trailing icon alignment using fill_container instead of space_between * feat(canvas): right-align agent badge and add breathing glow border - Agent badge now right-aligned to frame's right edge instead of after label - Added breathing glow border around agent-owned frames during generation - Glow border uses same color and lifecycle as the agent badge - Removed unused BADGE_GAP constant and useDocumentStore import * feat(code-panel): enhance tab scrolling functionality and add scrollbar utility - Introduced left and right scroll buttons for tab navigation in the CodePanel, improving user experience for navigating long tab lists. - Added a custom utility to hide scrollbars for a cleaner interface. - Updated styles for better responsiveness and usability in the CodePanel layout. * fix(docs): update Discord invite links in multiple README files - Replaced outdated Discord invite links with the new link across all language-specific README files. - Ensured consistency in the documentation for community engagement. * feat(code-panel): enhance system prompt for responsive design - Updated the ENHANCE_SYSTEM_PROMPT to emphasize the importance of responsive design in code rewriting. - Added detailed guidelines for converting fixed pixel widths to relative units and using responsive Tailwind breakpoints. - Ensured that the output remains visually faithful on desktop while adapting gracefully across screen sizes. * feat(docs): add WeChat group information to README.zh.md and include group image - Introduced a new section in the Chinese README to provide details about the WeChat group for community engagement. - Added an image representing the WeChat group for better visibility and user interaction. * feat(electron): enhance theme management and title bar overlay for Windows/Linux - Updated the `setTheme` method in the Electron API to accept custom colors for the title bar overlay, improving theme synchronization across platforms. - Adjusted title bar overlay colors for Windows and Linux to ensure proper visibility and aesthetics. - Enhanced the top bar component to read computed CSS colors and apply them dynamically, ensuring a consistent user interface. - Improved handling of theme changes in the application to support background and foreground color customization. * fix(screenshot): update screenshot image for improved clarity and quality * fix(docs): update WeChat group image path in README.zh.md for consistency * fix(ai): fix post-generation validation pipeline and text centering - Fix Agent SDK validation: save temp screenshots inside project dir (.openpencil-tmp/) so Claude Code plan mode can read them, instead of /tmp/ which is outside the project sandbox - Enrich validation tree dump with fill colors, stroke, fontSize, fontWeight, textAlign, cornerRadius, opacity for comprehensive visual analysis - Add multi-round validation with quality scoring (threshold 8/10), 500ms stabilization delay between rounds - Add detailed debug logging to applyValidationFixes showing which nodes were found/skipped and property changes - Fix canvas sync needsTextbox check to also account for textAlign (matching isFixedWidthText in factory), preventing IText↔Textbox thrashing on every sync tick - Auto-center text in vertical+center layouts by expanding to full container width and injecting textAlign:'center' - Force Textbox for non-left-aligned text so textAlign is respected (IText ignores width and computes its own) * fix(canvas): use precise text width estimation for fit-content layout Remove the 14% safety factor from text width estimation when computing fit-content/natural-width text dimensions. IText auto-computes its own width and ignores our setting, so the safety margin only inflated the layout allocation, making text appear left-shifted within its container. * fix(canvas): center fit-content text in horizontal layouts For text nodes with fit-content width in horizontal layouts, set textAlign:'center' to compensate for width estimation inaccuracy. The estimated box is typically wider than the actual rendered text, causing left-aligned text to appear visually shifted. Centering distributes the estimation error evenly on both sides. * feat(ai): show validation details in checklist panel - Accumulate validation log (screenshot, analysis, fixes) instead of overwriting status messages, so the full process is visible - Preserve step thinking content in buildFinalStepTags (was discarded) - Add details field to pipeline items and render in checklist UI - Each validation step now shows: screenshot captured, issues found, quality score, fixes applied * feat(ai): add visual reference pipeline types and integration hooks - Add DesignSystem and VisualReference types to ai-types - Add 'visual-ref' mode to AIDesignRequest and SubTask.htmlReference - Detect visual-ref candidates in chat handlers (landing pages, websites) - Wire visual-ref mode in design-generator and orchestrator - Inject HTML reference snippets into sub-agent prompts * feat(ai): add modular design principles for sub-agent context - Add design-principles module with topic files: color, typography, spacing, composition, components - Selectively load relevant principles based on prompt content - Inject design principles into sub-agent system prompts * feat(ai): implement visual reference pipeline - Add design-system-generator: generates color/typography/spacing tokens - Add design-code-generator: generates HTML/CSS from design system - Add html-renderer: renders HTML to screenshot via html2canvas - Add visual-ref-orchestrator: coordinates the full pipeline (design system → HTML code → screenshot → enrich subtasks) - Add html2canvas dependency for client-side HTML rendering * feat(mcp): default filePath to live canvas and fix cross-platform issues - Default all MCP tool filePath to live://canvas when omitted, so tools operate on the real-time canvas instead of stale files - Remove filePath from required params in all tool schemas (21 interfaces) - Fix mcp-server-manager.ts using process.cwd() which fails in Electron production on Linux — now checks ELECTRON_RESOURCES_PATH first - Fix stopMcpHttpServer using SIGTERM on Windows — use taskkill instead - Force new children reference in applyExternalDocument to ensure canvas sync subscriber always detects MCP-pushed document updates * feat(mcp): enhance design prompt with semantic roles, CJK typography, and layout rules Add comprehensive design knowledge to MCP design prompt for better AI-generated designs: design type detection (mobile vs desktop), full semantic role reference with context-aware defaults, CJK typography rules, expanded text/layout/form guidelines, and detailed post-processing documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(ai): implement intent classification for chat handlers - Replace hardcoded keyword matching with a lightweight LLM call to classify user intent in chat messages. - Introduce a new function `classifyIntent` to determine if the request is for design generation or conversation. - Update design request handling in `useChatHandlers` to utilize the new classification method. - Enhance design prompt documentation to reflect changes in design type detection based on intent rather than keywords. * fix(ai): handle string qualityScore in validation response parsing The LLM sometimes returns qualityScore as a string (e.g. "8" instead of 8), causing it to fall through to 0. Also hide misleading "quality: 0/10" display when the score couldn't be determined, and log raw response for debugging. * fix(ai): increase validation timeout to 90s and fix quality score parsing Agent SDK validation requires spawning a process, reading the image, and analyzing it — 30s was consistently timing out. Also handle string qualityScore values from LLM responses and hide misleading 0/10 display. * fix(ai): fix validation timeout and response parsing - Increase validation timeout from 30s to 180s (Agent SDK needs time for subprocess spawn + OAuth auth + multi-turn image reading) - Strip <tool_use> XML blocks from Agent SDK response before extracting JSON — the tool call XML was confusing the regex, causing qualityScore to parse as 0 despite valid JSON being present - Handle string qualityScore values and hide misleading "quality: 0/10" - Revert unnecessary direct API key approach for validation * fix(ai): prevent node ID collisions between generations When generating new content on a canvas with existing nodes, AI-generated IDs (e.g. brand-spacer) would collide with previous generations. Now captures pre-existing node IDs at generation start and checks against them during upsert sanitization. Remapped IDs are tracked in generationRemappedIds so progressive streaming updates can still find their nodes. * fix(ai): require styleGuide in orchestrator plan and fix validation detail icons - Add fallback default styleGuide when orchestrator LLM omits it - Strengthen prompt to mark styleGuide as REQUIRED - Replace emoji icons in validation details with [done]/[pending]/[error] markers for consistent styling with the checklist design system * feat(server): add port file plugin for server instance discovery - Introduce a new Nitro plugin that writes a port file on server startup to allow the MCP server to discover the running instance, whether it's a development server or Electron. - Implement error handling in the Electron main process for writing the port file, logging any failures. - Update Vite configuration to include additional external dependencies in the rollup configuration. * feat(electron): implement IPC for retrieving pending file paths - Added a new IPC handler `file:getPending` to retrieve and clear the pending file path when the React app mounts. - Updated the Electron API to include `getPendingFile` for renderer access. - Enhanced the `useElectronMenu` hook to load any pending file on application startup. - Updated UI components to reflect changes in file handling and improved user experience. * fix(panels): replace emoji icons with styled icons in validation checklist - Parse [done]/[pending]/[error] prefixes in detail lines and render as styled circle icons matching the parent checklist design system - Replace remaining emoji markers in design-validation.ts with text prefixes - Fix isApplied detection to recognize new [done] Applied marker * refactor(electron): update settings path to use platform-standard app data directory - Changed the settings file path to utilize Electron's user data directory for better cross-platform compatibility. - Updated the settings writing function to ensure the user data directory is created if it doesn't exist. - Added comments to clarify the storage location for different operating systems. - Implemented a fixed partition for localStorage/cookies to maintain data across server port changes. * feat(ai): enhance validation with pre-checks, structural fixes, and border detection - Add design-pre-validation.ts: pure code checks before LLM validation - Invisible container detection (same fill as parent → auto-add border) - Sibling consistency (majority-rule for height/cornerRadius) - Add structural fixes to validation: addChild/removeNode operations - Icon injection via lookupIconByName with server fallback - autoFixParentLayout with child count guard to prevent layout breakage - Add strokeColor/strokeWidth to safe fix properties for border fixes - Simplify intent classification: all design requests use visual-ref pipeline - Fix checklist: "Found N issues" now shows [done] instead of [pending] - Fix qualityScore: only update when > 0 to preserve valid round scores * fix(ai): cherry-pick safe validation improvements, drop aggressive pre-checks Keep: stroke tree dump bug fix (object not array), qualityScore=0 false positive detection, fit_content→fixed safety guard, empty path removal, type-specific sibling consistency, repeated fix filtering, screenshot extraction to design-screenshot.ts. Drop: detectForcedFixedHeight (destroyed input/button heights), MAX_VALIDATION_ROUNDS 5 (too many rounds), removal of quality threshold early stop, section regeneration phase. --------- Co-authored-by: Fini <fini.yang@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
25 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, Fabric.js v7 (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) │ │ │
└────────┬────────┘ └────────┬──────────┘
│ │
▼ ▼
Fabric.js Canvas canvas-sync-lock
(imperative render) (prevents circular sync)
- document-store is the single source of truth. Fabric.js only renders.
- User edits on canvas → Fabric events → update document-store (with sync lock)
- User edits in panels → update document-store →
use-canvas-syncupdates Fabric canvas-sync-lock.tsprevents circular updates when Fabric 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 Fabric.js 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[])
Key Modules
src/canvas/— Fabric.js integration (30 files):fabric-canvas.tsx— Canvas component initializationuse-fabric-canvas.ts— Canvas initialization hookcanvas-object-factory.ts— Creates Fabric objects from PenNodes (rect, ellipse, line, polygon, path, text, image, frame, group)canvas-object-sync.ts— Syncs individual object properties between Fabric and storecanvas-sync-lock.ts— Prevents circular sync loopscanvas-sync-utils.ts—forcePageResync()utility for page-aware canvas re-synccanvas-controls.ts— Custom rotation controls and cursor stylingcanvas-constants.ts— Default colors, zoom limits, stroke widthsuse-canvas-events.ts— Drawing events, shape creation, smart guides activation, tool-basedskipTargetFindmanagementcanvas-node-creator.ts—createNodeForTool,isDrawingTool,toScenehelpers extracted from use-canvas-eventscanvas-object-modified.ts—syncObjToStore,syncSelectionToStore,handleObjectModified— Fabricobject:modifiedhandler logicuse-canvas-sync.ts— Bidirectional PenDocument ↔ Fabric.js sync, node flattening with parent offsets, variable resolution viaresolveNodeForCanvas()canvas-layout-engine.ts— Auto-layout computation:resolvePadding,getNodeWidth/Height,computeLayoutPositions,Paddinginterfacecanvas-text-measure.ts— Text width/height estimation, CJK detection,parseSizing,getTextOpticalCenterYOffsetuse-canvas-viewport.ts— Wheel zoom, space+drag panning, tool cursor switching, selection toggling per tooluse-canvas-selection.ts— Selection sync between Fabric objects and canvas-storeuse-canvas-hover.ts— Hover state management for objectsuse-canvas-guides.ts— Smart alignment guides with snappingguide-utils.ts— Guide calculation and renderingpen-tool.ts— Bezier pen tool: anchor points, control handles, path closure, preview renderingparent-child-transform.ts— Propagates parent transforms (move/scale/rotate) to children proportionallyuse-dimension-label.ts— Shows size/position labels during object manipulationuse-frame-labels.ts— Renders frame names and boundaries on canvasuse-entered-frame-overlay.ts— Visual overlay when entering a frame for editinguse-layout-indicator.ts— Layout indicator rendering during drag operationsinsertion-indicator.ts— Insertion point indicator for layout drop targetsdrag-into-layout.ts— Drag-and-drop into auto-layout frames with insertion detectiondrag-reparent.ts— Reparenting nodes during drag operationslayout-reorder.ts— Reorder children within layout frames during dragselection-context.ts— Selection context management for multi-select operations
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 (8 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 persistence
src/types/— Type system (8 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)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 (withAgentStatusButton), status-bar, page-tabs (multi-page navigation with context menu), update-ready-banner (Electron auto-updater notification)src/components/panels/— Panels (26 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)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 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 (9 files): ColorPicker, NumberInput, SectionHeader, ExportDialog, SaveDialog, AgentSettingsDialog, IconPickerDialog, VariablePicker, FigmaImportDialogsrc/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 (20 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 limitsdesign-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 issuesgeneration-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 (11 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 references
src/services/codegen/— React+Tailwind and HTML+CSS code generators (outputvar(--name)for$variablerefs), CSS variables generatorsrc/hooks/— Hooks (2 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 association
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 registrationdocument-manager.ts— MCP utility for reading, writing, and caching PenDocuments from disktools/— Individual MCP tool implementations:batch-design.ts,batch-get.ts,find-empty-space.ts,open-document.ts,snapshot-layout.ts,variables.ts,pages.ts(page CRUD: add/remove/rename/reorder/duplicate)utils/— Shared utilities:id.ts,node-operations.ts(page-awaregetDocChildren/setDocChildren)
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)server/api/ai/— Nitro server API (7 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),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 (5 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 CLI
Fabric.js v7 Gotchas
- Default origin is
center/center— always setoriginX: 'left',originY: 'top'on objects soleft/topmeans top-left corner - Pointer capture — Fabric captures pointers on
upperCanvasEl; attach pointer listeners there, not ondocument - Coordinate conversion — use
canvas.getScenePoint(e)withcanvas.calcOffset()for accurate pointer-to-scene mapping - Default strokeWidth is 1 — explicitly set
strokeWidth: 0when no stroke is desired - Tool isolation — when a drawing tool is active, set both
canvas.selection = falseandcanvas.skipTargetFind = trueto prevent Fabric from selecting existing objects during draw. Restore both when switching back to select tool. - Parent-child transforms — nodes are flattened to absolute coordinates for Fabric;
nodeRenderInfostores parent offsets for converting back to relative coordinates.parent-child-transform.tshandles propagating transforms to descendants during drag/scale/rotate.
Canvas Tool State Management
When switching tools, two subscribers manage canvas state:
use-canvas-events.ts— setsselection/skipTargetFindbased on drawing vs select tooluse-canvas-viewport.ts— also managesselection/skipTargetFindfor tool switches and space-key panning
Both must stay consistent: only select tool (without space pressed) should have selection = true and skipTargetFind = false.
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, native application menu, auto-updater, macOS traffic-light padding (auto-hidden in fullscreen),.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-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.