Inspectors
An inspector is the property panel shown in the sidebar's Properties section for the currently selected layer. That section is one of the sidebar's collapsible card panels — its header can be folded shut like the others, and its title names the selected layer's type (e.g. Properties — path). Each layer type has exactly one registered inspector component, discovered from packages/ (see Extension architecture). This page covers the inspector contract and the five built-in inspectors, one per layer type.
The inspector contract
components/ looks up the inspector registered for the selected layer's type and renders it with:
interface InspectorProps<L extends Layer = Layer> {
layer: L;
onChange(patch: Partial<L>, options?: { commit?: boolean }): void;
ctx: ToolContext;
}onChange patches the layer and pushes the result through the document history:
commit: true(the default) — a discrete edit becomes its own undo entry. Used for things like a number field's blur/Enter or a checkbox toggle.commit: false— the change is streamed (ctx.replace) rather than committed, for continuous input like a slider drag. The caller is expected to have opened one undo entry withctx.beginGesture()first; every subsequentcommit: falsecall coalesces into that same entry until the gesture ends.
When no inspector is registered for a layer's type, InspectorHost shows a plain "No inspector registered" message instead of crashing — a half-built extension wave still renders.
Shared building blocks
All five built-in inspectors are composed from components/:
Field/Row— a labeled row (a<label>vs. a plain<div>—Rowis used when the control is a button, since a<label>would otherwise steal the button's accessible name).NumberField— a numeric input that commits exactly one undo entry per discrete edit (blur, Enter, or a single arrow-key step). While focused it holds a local draft string so typing "250" doesn't collapse through 0/2/25 as commits, and a cleared/NaNdraft reverts to the last valid value rather than committing 0.MaterialField— a read-only swatch and label for the selected object's owning fixed container. Material changes happen by moving the object in Layers, never through a per-object color picker.ActionButton— a full-width button for an inspector's "open a dialog" affordance (e.g. Pattern's Browse…, Image's Convert to vector…).
Built-in inspectors
| Layer type | File | Fields |
|---|---|---|
shape | inspectors/ | Material (read-only), x, y, width, height, rotation (°) |
text | inspectors/ | Text content, font, material (read-only), size (mm), x, y, rotation (°) |
path | inspectors/ | Material (read-only), fill enabled, stroke enabled, stroke width (mm), closed |
image | inspectors/ | x, y, width, height, "Convert to vector…" |
pattern | inspectors/ | "Browse…" (pattern picker), material (read-only), x, y, size (mm), "Cover panel", one slider per pattern param |
Shape
Rect/ellipse layers show their owning material and expose position, size, and rotation fields. All geometry fields are plain NumberFields — the same fields Select's resize handles write to, so dragging a handle and typing a width produce identical patches.
Text
Adds a <textarea> for the multi-line content and a font <select> populated from CURATED_FONTS (see Fonts), with any starred favorite sorted first and marked with a ★. Below it, a Browse Google Fonts… button opens the Font Explorer for the full catalog. If the layer's current fontFamily isn't one of the curated options — a hand-edited JSON import, the demo document's generic sans-serif, or a family picked in the Explorer — it's kept as a selectable, synthesized option rather than silently swapped out. Picking a font (from either surface) calls ensureFont() and requests a repaint once the real face has actually loaded, so the canvas doesn't stay stuck on the fallback face.
Path
The owning material is read-only. Fill enabled and Stroke enabled preserve whether a path is fill-only, stroke-only, both, or neither; when enabled, the paint is forced to the owning material. Node/handle positions are not editable here — they're edited directly on the canvas by dragging with the Select tool (Alt breaks the mirrored-handle constraint), which the inspector notes in a caption.
Image
Only geometry (x/y/width/height) is editable — an image layer is a design-time raster reference, not a manufacturable layer. Its one real action is Convert to vector…, which opens the trace dialog (disabled, with an explanatory tooltip, if no trace dialog is registered — see Image tracing).
Pattern
Shows the current pattern's display name behind a Browse… button that opens the pattern picker (disabled if that dialog isn't registered), its read-only owning material, the square's geometry, and one <input type="range"> per PatternParamDef the pattern declares (see Patterns → Generator contract).
A pattern layer is a movable square — x, y, and size (mm), all plain NumberFields like the shape/image inspectors — since pattern squares became draggable on the canvas. size is clamped into [0.1, MAX_PATTERN_SIZE_MM] on commit (@zpd/core's MAX_PATTERN_SIZE_MM, a DoS guard against a generator looping over an absurd span — see Rendering & Camera), and a commit that would leave the value unchanged after clamping is skipped, so typing an out-of-range number and having it silently clamp back never writes a phantom undo entry. A Cover panel button beneath the geometry fields recenters and resizes the square back to patternCoverGeometry() — its original cover-the-panel-at-any-aspect-ratio placement — with the same no-op guard (a click that would leave x/y/size unchanged commits nothing).
Each slider follows the same lazy-gesture pattern as Select's drag: the first onChange of a scrub opens one undo entry (ctx.beginGesture()), every subsequent tick of the same drag streams onChange(patch, { commit: false }), and releasing the pointer/key just ends the scrub locally — there is no trailing commit, since that would double up the entry beginGesture already opened.
Note
Registering a new layer type's inspector is a one-file addition: create inspectors/ and call registerInspector('my-type', MyInspector). See Adding an inspector.