Parametron

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 — a BuilderHost (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 Domphy State<PlacedBlock | null>. null (or no selection) renders an empty panel; a PlacedBlock renders its parameter tree.
  • opts.accent — optional CSS color override for control accents (checkbox/range accent-color). Falls back to themeColor(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 ContainerNode with lane === "parameters" (a header display-group) renders as a ConfigGroup — an uppercase section label followed by its child ParameterNodes.
  • A top-level ParameterNode renders as a bare ConfigParam row (no group header).
  • Anything else (operation/entity nodes) is skipped — the configurator only ever touches the parameters header, never operations.

Two kinds of parameters are filtered out:

  • Builtin keys (lengthX/lengthY/lengthZ, or whatever the model's Registry.builtinParameters declares) — these are dimension controls owned by the builder's own gizmo UI, not the configurator. This filter applies both to top-level ParameterNodes and to every ConfigGroup'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-level ParameterNodes (not re-checked inside ConfigGroup), since a loadModel parameter is never nested under a parameters header group in practice.

Control selection

ConfigControl maps one ParameterNode to a Domphy input, in this priority order:

  1. Options present (entry.options.elements.length > 0) — a <select> (@domphy/ui's select()), one <option> per OptionNode (label/expression), regardless of valueType. 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.
  2. valueType === "boolean" — a checkbox (inputCheckbox()).
  3. valueType === "color" — a native color input (inputColor()); the stored expression is unwrapped/rewrapped from its single-quoted string literal.
  4. valueType in { length, number, angle } with both min and max set — a range slider (inputRange()) plus a live value readout (entry.display(l) + entry.symbol). step defaults to max(1, (max - min) / 100) when not declared.
  5. Same numeric types without a full min/max pair — a plain <input type="number"> (inputNumber()), committed on blur or Enter.
  6. 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 (lengthXLength 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 for lengthX/lengthY/lengthZ, and sub-model (loadModel) nesting — everything this panel deliberately excludes.
  • Model & pull graphNodeCollection, ParameterNode, ContainerNode, and the parameters header lane this panel walks.
  • Expressions — the expression-string contract every control writes back through setInput.
  • Model authoring reference — parameter methods, min/max/step/options constraint fields, and valueType per method.
📖 4 min read