Add-Actions & Dialogs
Two more self-registering extension kinds round out the toolbar: add-actions, the "Add …" buttons at the bottom of the left toolbar, and dialogs, the modal surfaces they (and inspectors) open. Both are discovered from packages/ and packages/ respectively — see Extension architecture.
Add-actions
An add-action is the simplest extension kind:
interface AddAction {
id: string;
label: string;
icon?: string;
run(ctx: ToolContext): void;
}components/ renders one button per registered action, below a divider under the tool buttons, in registration order. Clicking a button just calls action.run(ctx).
| Add-action | File | Behavior |
|---|---|---|
| Add rectangle | add- | Commits a new shape layer (rect) sized/positioned relative to the panel, selects it. |
| Add ellipse | add- | Same as above, shape: 'ellipse'. |
| Add pattern… | add- | Opens the pattern picker dialog with no props — the dialog itself adds the layer once a pattern is chosen. |
| Add image… | add- | Opens a transient <input type="file"> and sends the file through the shared classifier. Raster content is fitted to the panel and committed as an image; SVG within the vector-size cap opens the vector/material import dialog, while oversized SVG uses the raster fallback. |
Add rectangle/ellipse both snap their initial geometry to the 0.1mm grid and cap their default width at min(20mm, panelWidth/2) so a freshly added shape is never larger than the panel it lands on.
Dialogs
A dialog is a registered { id, component } pair rendered by components/, which is mounted once at the root of the Editor and subscribes to a tiny observable open/close store:
interface DialogModule<P = unknown> {
id: string;
component: ComponentType<DialogProps<P>>;
}
interface DialogProps<P = unknown> {
props: P;
close(): void;
ctx: ToolContext;
}Because the store lives outside React (openDialog/closeDialog in registry/), anything can open a dialog — including a tool's pointer handler, which is not a React component. DialogHost is the single owner of every dialog's modal chrome: it renders the open dialog's component inside a role="dialog" aria-modal="true" backdrop; clicking the backdrop (but not the dialog body itself, which stops propagation) closes it, as does pressing Escape anywhere on the page. It also traps focus inside the dialog while it's open, focuses the first focusable descendant on open (deferred to a dialog component that already moved focus itself, e.g. auto-focusing a search box or a Cancel button), and restores focus to whatever was focused before the dialog opened once it closes. Registered dialog components supply content only — none of this chrome is reimplemented per dialog.
Built-in dialogs
| Dialog id | File | Opened from |
|---|---|---|
shortcut-panel | dialogs/ | Header's ? button, or the bare ? key. |
command-palette | dialogs/ | ⌘/Ctrl+Shift+K. |
font-explorer | dialogs/ | Text inspector's Browse Google Fonts… button, or the command palette's Browse Google Fonts command. |
pattern-picker | dialogs/ | Pattern inspector's Browse…, or the Add pattern… add-action. |
svg-import | dialogs/ | SVG within the vector-size cap, selected through Add image…, drop import, or clipboard paste. Oversized SVG uses the raster fallback instead. |
trace | dialogs/ | Image inspector's Convert to vector…. |
confirm-dialog | components/ | Any code path needing a Confirm/Cancel gate before a destructive action — e.g. New panel and JSON import both replace the whole document. Core infra, not a Wave-5 extension: it self-registers via a direct import in dialog-host.tsx rather than the dialogs/* auto-discovery glob. |
Shortcuts overlay & command palette
The keyboard-shortcuts reference and the command palette are both generated live from the same command registry — the old hand-maintained static shortcuts table is gone. The ?-triggered overlay is now searchable and category-grouped; ⌘/Ctrl+Shift+K opens a fuzzy-searchable command palette that can run any chordless (palette-only) command too, such as Align, New Panel, or Import/Download JSON. See Shortcuts & Command Palette for the full contract.
The sidebar Help panel complements the shortcuts overlay rather than replacing it: the Help panel explains the one tool that is currently active (its per-tool description), while the shortcuts overlay stays the always-available, searchable reference for the app's keyboard shortcuts. Reach for the overlay when you want the shortcut table; glance at the Help footer when you want to know what the tool in your hand does.
Font Explorer dialog
Browses the full 1,942-family Google Fonts catalog with search, category filters (including a Japanese filter derived from font subsets), and starrable favorites — the second tier of the font picker, alongside the text inspector's curated dropdown. See Fonts → The Google Fonts Explorer.
Pattern picker dialog
A responsive grid of pattern thumbnails, one card per entry in PATTERN_GENERATORS (see Patterns → Built-in catalog), each rendered with renderPatternThumb. It opens two ways, both resolving to a single ctx.commit() (one undo entry) before closing:
With
{ layerId }(from the pattern inspector) — clicking a card swaps that existing pattern layer'spatternTypeand resets itsparamsto the new pattern's defaults.With no props (from the Add pattern… add-action) — clicking a card creates a brand-new pattern layer in the Copper container and selects it.
Each thumbnail is drawn with a useLayoutEffect (not useEffect) so it's sized and painted before the browser's first paint frame — otherwise the <canvas> would flash its default 300×150 box for one frame and the grid would visibly jump.
With the catalog grown to dozens of patterns, the dialog no longer renders every thumbnail up front. A search box (auto-focused on open, same convention as the Font Explorer's search) filters the grid by a case-insensitive substring match against each generator's name (the stable kebab id) or displayName. Below that, cards are rendered page by page: only the first ~24 matching cards mount initially, and a single IntersectionObserver watching a tail sentinel element loads the next page as the user scrolls near the bottom of the grid — the same paged/sentinel pattern the Font Explorer uses for its font cards, rather than per-card visibility tricks (CSS content-visibility would not stop each card's useLayoutEffect canvas draw, so it wouldn't actually defer the work). Typing in the search box resets paging back to the first page of the filtered results.
Trace dialog
The image-to-vector workflow — see Image tracing for the full pipeline it drives. In outline: it decodes the source image layer, downscales and traces it to an SVG (debounced 250ms after any option change), previews that SVG in an <img> (never dangerouslySetInnerHTML, so a hostile trace result can't execute embedded script), and on Apply converts the SVG into PathLayers. The source image is reinserted as a hidden design-time reference in Copper, while each traced path is routed to the material container mapped from its palette paint; the first traced layer is selected. All of that is one ctx.commit().
Trace options exposed in the dialog: a 3-color palette toggle (quantize straight to the fixed panel palette vs. a free 2–8 color count), min shape outline (drops small traced regions below this pixel threshold), and blur radius (pre-blur before tracing, to smooth noisy source rasters).
Tip
Both the pattern and image inspectors check for their dialog's registration (getDialog('pattern-picker') / getDialog('trace')) and disable their trigger button — with an explanatory tooltip — if it isn't registered yet. This lets an inspector ship before its dialog does without a broken button.