ConfiguratorPanel renders the customer-facing edit surface for a placed block: one control per parameter (slider, checkbox, color swatch, select, or text field), inferred from the parameter's valueType and constraints — no per-model UI code. It reads a PlacedBlock's parameter nodes straight off the engine graph and writes back through ParameterNode.setInput, so it needs no separate form-state layer; the graph IS the state.
Import
import { ConfiguratorPanel } from "@parametron/ui"Exported from the package root (packages/ui/src/configurator/index.ts → the root barrel src/index.ts); there is no /configurator subpath.
Signature
function ConfiguratorPanel(
host: BuilderHost,
selected: State<PlacedBlock | null>,
opts?: { accent?: string },
): DomphyElement<"div">host— aBuilderHost(see Builder panel). The configurator itself doesn't call any host method today; it's threaded through for parity with the rest of the panel cluster and future host-driven controls.selected— a DomphyState<PlacedBlock | null>.null(or no selection) renders an empty panel; aPlacedBlockrenders its parameter tree.opts.accent— optional CSS color override for control accents (checkbox/rangeaccent-color). Falls back tothemeColor(null, "shift-8", "primary")— use this for B2B embed theming, distinct from the reference theme.
What it reads
PlacedBlock.nodes (a NodeCollection, see Model & pull graph) is walked one level deep:
- A
ContainerNodewithlane === "parameters"(a header display-group) renders as aConfigGroup— an uppercase section label followed by its childParameterNodes. - A top-level
ParameterNoderenders as a bareConfigParamrow (no group header). - Anything else (operation/entity nodes) is skipped — the configurator only ever touches the
parametersheader, neveroperations.
Two kinds of parameters are filtered out:
- Builtin keys (
lengthX/lengthY/lengthZ, or whatever the model'sRegistry.builtinParametersdeclares) — these are dimension controls owned by the builder's own gizmo UI, not the configurator. This filter applies both to top-levelParameterNodes and to everyConfigGroup's children. - Load-method parameters (
isLoadMethod(entry.valueType), i.e.loadModel) — a sub-model reference has no scalar control; it's edited through the builder's nesting UI, not exposed here. This filter only runs on top-levelParameterNodes (not re-checked insideConfigGroup), since aloadModelparameter is never nested under aparametersheader group in practice.
Control selection
ConfigControl maps one ParameterNode to a Domphy input, in this priority order:
- Options present (
entry.options.elements.length > 0) — a<select>(@domphy/ui'sselect()), one<option>perOptionNode(label/expression), regardless ofvalueType. This is the same enum treatment the builder's basic-mode parameter view uses for the raw-expression editor, so a variant/finish/material picker looks the same in both places. valueType === "boolean"— a checkbox (inputCheckbox()).valueType === "color"— a native color input (inputColor()); the stored expression is unwrapped/rewrapped from its single-quoted string literal.valueTypein{ length, number, angle }with bothminandmaxset — a range slider (inputRange()) plus a live value readout (entry.display(l)+entry.symbol).stepdefaults tomax(1, (max - min) / 100)when not declared.- Same numeric types without a full
min/maxpair — a plain<input type="number">(inputNumber()), committed on blur or Enter. - Everything else — a plain text input (
inputText()), also committed on blur or Enter.
Every control writes back through the same closure: entry.setInput(expressionString) followed by node.requestEvaluate() — identical for all six control kinds. Text and number fields commit on blur/Enter rather than every keystroke — the configurator writes raw expression strings ('#ffffff', true/false as "true"/"false", a bare numeric literal), matching the engine's expression-string contract described in Expressions.
Labels
paramLabel(entry) prefers the node's own label expression (entry.label.evaluate()) and falls back to a titlized key (lengthX → Length X) via ??, so the fallback only fires when entry.label is missing (null/undefined — e.g. a ContainerNode with no label argument set) or .evaluate() throws or returns null/undefined. An explicit empty-string label (ParameterNode's default, unrenamed expression '') evaluates to "" and is not replaced by ?? — it renders as blank text. In practice this rarely surfaces: ParameterNode.setKey backfills the label expression to the titlized key the first time a parameter is renamed away from its default. This applies to both ConfigParam rows and ConfigGroup section headers — a ContainerNode's label is an ArgumentNode, a ParameterNode's is an ExpressionNode, but both expose .evaluate() so paramLabel treats them uniformly.
Usage
import { toState } from "@domphy/core"
import { ConfiguratorPanel } from "@parametron/ui"
import type { BuilderHost, PlacedBlock } from "@parametron/ui"
const selected = toState<PlacedBlock | null>(null)
const panel = ConfiguratorPanel(host, selected, { accent: "#2b7fff" })
// later, after the host places a block:
selected.set(await host.placeBlock("modelA"))host implements BuilderHost.placeBlock/updateBlock (see Builder panel for the full contract); the configurator never calls the host directly today, but takes the same BuilderHost type the rest of the panel cluster shares.
See also
- Builder panel — owns
BuilderHost/PlacedBlock, the gizmo controls forlengthX/lengthY/lengthZ, and sub-model (loadModel) nesting — everything this panel deliberately excludes. - Model & pull graph —
NodeCollection,ParameterNode,ContainerNode, and theparametersheader lane this panel walks. - Expressions — the expression-string contract every control writes back through
setInput. - Model authoring reference — parameter methods,
min/max/step/optionsconstraint fields, andvalueTypeper method.