- New AppSelect.vue: generic wrapper around reka-ui Select primitives
- Replace all 4 native <select> elements (font weight, effect type,
export scale, export format)
- Fix font picker dropdown truncating names (min-w-56)
- Show explanation when local fonts unavailable (Safari/Firefox)
13 tests using InMemoryTransport from @modelcontextprotocol/sdk:
- Tool registration (all 76 tools listed with descriptions)
- new_document, open_file, save_file lifecycle
- Tool execution through MCP protocol (create, fill, delete, query)
- Error paths: no document loaded, nonexistent node, invalid file path
- Full workflow: new → create → nest → query → delete
Fix @open-pencil/core dep in MCP package: workspace:* for local dev
(pnpm publish resolves this to the actual version at publish time).
Extract menu items into shared NodeContextMenuContent component,
used by both the canvas and layers panel context menus.
Right-clicking a layer selects it and shows the same menu.
Header row with app icon, double-click-to-rename document name, and
sidebar toggle button. Shows in both Tauri and browser.
When panels are hidden, a floating bar on the canvas provides the same
controls to restore the UI.
- AppToast component: top-center blue pill (info), red pill (errors)
- Global error handler catches window.error + unhandledrejection
- Link copied toast on share and copy link button
- use-toast composable: toast.show(message, variant)
- Cloudflare Worker + Durable Object server (packages/collab/)
- One DO per room with WebSocket Hibernation for near-zero idle cost
- Yjs sync protocol for document state (CRDT merge)
- Yjs awareness protocol for cursors, selection, presence
- Persists doc state to DO storage
- CORS headers for cross-origin WebSocket upgrade
- Client composable (src/composables/use-collab.ts)
- Y.Doc ↔ SceneGraph bidirectional binding
- Awareness: broadcasts cursor position, selection, name, color
- Auto-join from /share/:roomId URL
- Suppresses echo loops between Yjs and graph updates
- CollabPanel component (src/components/CollabPanel.vue)
- Share button generates room link, copies to clipboard
- Join flow: paste link or room ID
- Avatar stack with colored initials and tooltips
- Name input persisted to localStorage
- Remote cursor rendering in CanvasKit
- Colored arrow cursor + name label pill per remote peer
- Remote selection outlines in peer's color
- Only shows cursors on the same page
- SPA redirect for /share/* routes (Cloudflare Pages _redirects)
- Save As prompts for filename via prompt() before downloading
- Save reuses the filename from last Save As (no repeated prompts)
- Banner in Safari/Firefox explaining the limitation with Chrome link
- Banner is dismissible
Reka-ui Menubar in the left panel header, hidden when running in Tauri
(native menu handles it). Includes Open, Save, Save As, Export, Undo/Redo,
Zoom, Group/Ungroup, Component creation, Bring to front/Send to back,
Auto layout, Text formatting, Alignment.
- sceneNodeToJsx() in @open-pencil/core: converts SceneNode subtree to JSX
- CodePanel.vue with Prism.js highlighting, line numbers, copy button
- Third tab in properties panel: Design | Code | AI
- 14 new tests covering shapes, text, layout, effects, multi-selection
StyleRun model: array of {start, length, style} with
CharacterStyleOverride (fontWeight, italic, textDecoration, etc.).
Renderer uses ParagraphBuilder.pushStyle/pop for mixed-style text.
TextEditor delegates paragraph building to renderer.buildParagraph().
⌘B/I/U apply to selection range when text is selected, or toggle
whole-node style when no selection. Style runs adjust on
insert/delete to preserve formatting across edits.
Fig roundtrip: import characterStyleIDs + styleOverrideTable from
TextData, export back with deduped style table.
B/I/U/S toggle buttons in TypographySection.
- Column per mode (Name | Mode 1 | Mode 2 | ...)
- Color swatch inside value cells, not next to name
- Search bar in top-right header area
- Single '+ Create variable' button at bottom
- Wider dialog (800px) for multi-mode tables
The properties panel has overflow-y: auto which clips absolute
positioned popovers. Expand controls inline below the effect
row instead — simpler and always visible.
- Compact row: color swatch + type dropdown + eye toggle + minus
- Expanded controls in a floating popover anchored to the left
- Labeled rows: Position (X/Y), Blur, Spread, Color with hex + opacity
- Type dropdown in popover header with X close button
- Eye/minus icons hidden until row hover (matches fill/stroke rows)
Support adding, removing, and editing drop shadows, inner shadows,
layer blurs, background blurs, and foreground blurs. Each effect row
has visibility toggle, type dropdown, and remove button. Expanded
view shows offset, blur, spread, and color controls for shadows,
or just blur radius for blur effects.
- CanvasContextMenu now exposes onContextMenu via scoped slot instead
of wrapping in a <div> (which broke flex layout — the div had no
flex-1, so it collapsed and never received contextmenu events)
- EditorCanvas binds the handler directly on its own flex-1 div
- Fix lucide/diamonds icon not found — use lucide/component for
COMPONENT_SET (the actual 4-diamond component icon)
- FillPicker component: 3 category tabs (Solid, Gradient, Image), gradient
subtype dropdown (Linear/Radial/Angular/Diamond), draggable gradient stop
bar, per-stop color/position/opacity editing with ScrubInput
- FillSection uses FillPicker instead of ColorPicker for full Fill editing
- Default gradient transforms for each subtype so renderer picks them up
- colorToRgba255/rgba255ToColor helpers in color.ts
- Custom SVG icons for fill type tabs (solid square, gradient fade)
Document structure: Document (root) → Pages (CANVAS) → Nodes.
Each page has its own viewport (pan/zoom) and background color.
- CANVAS node type for pages
- SceneGraph: addPage(), getPages(), getAbsolutePosition stops at CANVAS
- Store: currentPageId, switchPage(), addPage(), deletePage(), renamePage()
- All operations (create, select, paste, hit test, render) scoped to current page
- Per-page viewport state saved/restored on page switch
- Pages list in LayersPanel with add/switch/double-click rename
- .fig import creates proper pages from DOCUMENT→CANVAS hierarchy
- Renderer takes pageId to render correct page's children
- Unit test for pages, updated E2E tests for page-scoped graph
When nothing is selected, the right panel shows Page section with
a color picker for the canvas background (like Figma). The page
color is stored in reactive state and passed to the renderer.
- PropertiesPanel.vue: 513 → 67 lines (composition only)
- Extracted: PositionSection, LayoutSection, AppearanceSection,
FillSection, StrokeSection, TypographySection
- Shared useNodeProps() composable for update/commit logic
- Typography panel: font family picker with search (queryLocalFonts),
weight select, font size, line height, letter spacing, text alignment
- Font picker shows each family in its own typeface
- Loads selected font into both CanvasKit and browser on pick