- 'Design System' section containing Desktop and Mobile frames
- Desktop: header, hero card, avatar, two content cards
- Mobile: status bar, banner, list items, FAB button
- Color palette swatches (Primary, Success, Warning, Danger, Purple)
- Updated E2E tests for new demo structure
- Reverted section title pill to always use fill color (not blue)
Per Figma behavior, sections can only live in pages (CANVAS) or
other sections. Reparent guard skips sections dropped into frames/
groups. Drop target highlight suppressed when dragging sections
over non-section containers.
Moved section title hit testing from scene-graph (no font access) to
renderer where exact pill width is computed from glyph widths. Only
the title pill area is clickable, not the full width above the section.
Hit test passed through use-canvas → use-canvas-input via callback.
- Top-level section titles render above the body
- Nested section titles render inside at the top-left
- Drawing a section auto-reparents any siblings fully contained
within its bounds, adjusting coordinates to relative
- Undoable adopt operation with snapshotted positions
Title pill rendered in screen space (UI overlay layer), not canvas
space. Font size stays constant regardless of zoom. Text truncates
with ellipsis when section is narrower than the full name.
Pill width capped to section's screen width.
Sections (Figma-like):
- SECTION node type with rounded rect body + title pill above
- S shortcut, Frame flyout includes Section
- Default dark gray fill + subtle stroke, 5px corner radius
- Sections are containers with click-through hit testing
- Drag outside section reparents to parent
Toolbar flyouts with Reka UI DropdownMenu:
- Split button: tool icon activates tool, chevron opens dropdown
- Frame flyout: Frame / Section
- Rectangle flyout: Rectangle / Ellipse / Line
- Active sub-tool icon shown on parent button
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
On macOS these live in the app submenu. On Windows/Linux:
- File → Quit (at bottom, with separator)
- Help → About OpenPencil (at bottom, with separator)
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.
- Dragging a child fully outside its parent frame's bounds
automatically reparents it to the grandparent (like Figma)
- clipsContent property on SceneNode (default true for frames)
- Renderer respects clipsContent — unclipped frames show
children that overflow
- Clip content checkbox in Layout panel for frames
- Force TreeRoot re-mount via :key when tree structure changes,
fixing Reka UI not reacting to items prop updates
- Only reparent into frames on mouseup when user actually dragged,
preventing click-to-select from moving nodes into overlapping frames
- Expose store on window for test introspection
- 6 new E2E tests for layers panel
Nodes are sorted top-to-bottom (then left-to-right) before
reparenting into auto-layout frame. Positions are snapshotted
as plain IDs before mutation to avoid reactive proxy issues.
Property panel sections guard against null node with v-if,
preventing crashes during selection transitions.
Group wraps selected nodes in a GROUP node, preserving
z-order and visual position. Ungroup dissolves a group,
reparenting children to the group's parent. Both undoable.
Rust emits 'menu-event' with item ID to webview.
useMenu() composable listens and dispatches actions.
Extract openFileDialog() to use-menu.ts (shared with use-keyboard).
Horizontal and vertical rulers rendered directly in CanvasKit:
- Dark background, tick marks at adaptive intervals
- Coordinate labels scale with zoom level
- Blue highlight showing selected node extent on each axis
- Corner square at ruler intersection
- Value + suffix area is now the drag handle (cursor: ew-resize)
- Click without drag enters edit mode (2px dead zone)
- Suffix rendered as separate muted span (right-aligned)
- No more tiny icon-only drag area
- 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
- Font service (src/engine/fonts.ts) loads system fonts on demand
- Same font bytes registered in both CanvasKit and browser (FontFace API)
- Eliminates rendering mismatch between canvas and text editing overlay
- queryLocalFonts() for enumeration and blob() for font data
- Bundled Inter-Regular.ttf as fallback when API unavailable
- Renderer uses font service instead of hardcoded fetch