Interface
The editor (Editor.tsx) is a single full-screen shell with four regions: a header, a left toolbar, a center canvas viewport, and a right sidebar. The shell owns only generic state — the document (with undo/redo history), the camera, the current selection, and which tool is active. Everything domain-specific is rendered from data the extension registries expose, so this page describes the chrome as it behaves once those registries are populated.
Header
components/ is the top bar. From left to right it shows:
The app name.
A zoom cluster: zoom-out / zoom-in buttons (25% steps), the current zoom percentage (relative to the "fit" scale, not to physical mm), and a Fit button that re-centers and re-scales the camera to frame the whole panel.
Undo / redo buttons, disabled when there is nothing to undo/redo.
A ? button that opens the keyboard shortcuts dialog.
A ⬇ JSON button that downloads the current document as an order-ready panel config JSON (see Download trigger below).
Left toolbar
components/ is entirely data-driven: it renders one button per entry in the tool registry, in registration order, followed by a divider and one button per entry in the add-action registry. Clicking a tool button calls ctx.setActiveTool(tool.id); clicking an add-action button calls action.run(ctx). Neither list is hand-maintained — a new file dropped into tools/ or add-actions/ appears here automatically. See Tools for the built-in tool set.
Canvas viewport & rulers
components/ is a purely presentational wrapper around a single <canvas>: it exposes a containerRef (measured with a ResizeObserver so the camera can re-fit on resize) and forwards pointer events to the Editor shell. The <canvas> itself has touch-action: none so drags aren't hijacked as scroll/pinch gestures on touch devices. All actual painting happens in the Editor's repaint effect — see Rendering & camera.
The viewport sits inside a fixed ruler frame (components/): a 20px millimeter ruler strip along the top, a matching strip down the left, and a small mm corner box where they meet. The strips redraw their ticks and labels whenever the camera pans or zooms, but they never move in layout — only the canvas content transforms underneath them. See Rendering & camera → The mm rulers for the tick math.
Right sidebar
components/ is a vertical stack of collapsible card panels. The top four share a scrolling inner column (overflow-y-auto overscroll-contain, so the list bottoming out never scrolls the canvas underneath); the Help panel is pinned below them as a non-scrolling footer that stays visible even when the stack above overflows.
Each panel is a CollapsibleSection (components/) — a bordered card whose header is a button that toggles the body open or closed (aria-expanded on the button, a ▾/▸ affordance). The open/closed state is per-session and deliberately not persisted; there is no reordering or animation.
Panel — a
<select>of the real Eurorack HP sizes (PANEL_SIZESfrom@zpd/core), each labeled{hp}HP — {widthMm}×{heightMm}mm. Changing it commits a newpanelHpon the document, which re-fits the camera.Palette (fixed) — a read-only legend of the three panel colors (black/soldermask, gold/exposed copper, white/silkscreen). This list is fixed because it mirrors a physical PCB finish, not a user preference.
Layers —
components/, the visual layer stack (top of the list = top of the stack = last array index). Each row supports select (click), rename (double-click the name), reorder (▲/▼), show/hide (👁), and delete (✕), all routed throughlayer- list. tsx @zpd/core's layer-ops so ordering and immutability rules live in one place.Properties —
components/, which renders the registered inspector for the selected layer's type, or a placeholder when nothing is selected. The card title also names the selected layer's type (e.g. Properties — shape).inspector- host. tsx Help (footer) —
components/, pinned at the very bottom of the sidebar and starting collapsed. It describes the currently active tool: its name, a keyboard-shortcut badge, and the tool'shelp- panel. tsx descriptiontext (see Tools and Extension architecture → Tool descriptions). It followsactiveToolId, so the transient Space-held pan override never changes what it shows.
App-wide text selection
The editor shell's root element carries select-none, so click-dragging anywhere in the chrome or on the canvas never starts a native text selection. This keeps drag gestures — moving a layer, panning, drawing a pen path — from accidentally highlighting UI labels mid-interaction. The editable fields opt back in explicitly with select-text (the layer-rename input, the inspector number/text fields, the text inspector's <textarea>), so you can still select and edit their contents normally.
Download trigger
The header's ⬇ JSON button calls downloadPanelConfig(ctx.doc) from download.ts. That function:
Serializes the document with
@zpd/core'sserializePanelConfig(the canonical shape the fabrication reader expects) andJSON.stringifys it — pulled out as the pure, DOM-freepanelConfigJson()helper so the exact output string is unit-testable without a realBlob/anchor.Wraps the string in a
Blob, creates an object URL, and triggers a browser download namedzpd-panel-{panelHp}hp.jsonvia a temporary<a download>click.Revokes the object URL on a deferred tick (
setTimeout(..., 0)) rather than immediately — revoking in the same tick as the click can abort the download in some browsers before it has read the blob.
This is the editor-side trigger only; the shape of the exported JSON and how it's consumed downstream is documented in the Export section.
Note
The document model, undo/redo history, and layer types referenced throughout this section (DocState, Layer, PATTERN_GENERATORS, serializePanelConfig, …) live in @zpd/core and @zpd/patterns and are covered in the Document Model section.