zudo-panel-designer docs
GitHub repository

Type to search...

to open search from anywhere

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/app/src/editor/inspectors/ (see Extension architecture). This page covers the inspector contract and the five built-in inspectors, one per layer type.

The inspector contract

components/inspector-host.tsx 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 with ctx.beginGesture() first; every subsequent commit: false call 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/inspector-ui.tsx:

  • Field / Row — a labeled row (a <label> vs. a plain <div>Row is 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/NaN draft 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 typeFileFields
shapeinspectors/shape.tsxMaterial (read-only), x, y, width, height, rotation (°)
textinspectors/text.tsxText content, font, material (read-only), size (mm), x, y, rotation (°)
pathinspectors/path.tsxMaterial (read-only), fill enabled, stroke enabled, stroke width (mm), closed
imageinspectors/image.tsxx, y, width, height, "Convert to vector…"
patterninspectors/pattern.tsx"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 squarex, 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/my-type.tsx and call registerInspector('my-type', MyInspector). See Adding an inspector.