โŒ˜K

UI widgets

The containers, controls, displays, and theming available in ui.ts.

These come from @msh/device-sdk/device/ui and are used in ui.ts. ui.ts is pure data: no closures, no runtime logic. It runs at build time and serializes to a tree the renderer paints. For the how-to, see Design a custom look.

Containers

Sizing is relative. Use Theme.sizing(n), "fill", or { grow: n }, never raw pixels. gap and pad are whole sizing units.

WidgetPurpose
Box({ fill, pad, anchor, inset, width, height }, kids)A frame. With anchor it positions children absolutely (overlays); inset floats them in from the anchored edge.
Stack({ dir, gap, pad, align, justify, fill, grow, width, height }, kids)A flex row (dir: "row") or column (dir: "col").
Grid({ cols, rows, gap, pad }, kids)A CSS-grid container.
Spacer({ grow, size })Flexible empty space that pushes siblings apart.

anchor is one of "top-left", "top-center", "top-right", "center", "bottom-left", "bottom-center", "bottom-right". Use it to stack a toolbar and a centred logo over a fill Canvas.

Controls

Each binds to a manifest param (or setting) so modulation, automation, and undo work for free. Bind with param("id").

WidgetNotes
Knob({ bind, label, size, color, showValue, bipolar })A dial. Msh owns the disk, arc and mod wheels. color overrides the value-arc colour. Takes a center child slot.
Slider({ bind, label, orient })A linear slider (orient: "h" | "v").
Toggle({ bind, label, icon })A binary on/off button.
ToggleGroup({ bind, options, size })A segmented bar; the value is the selected option index.
Select({ bind, options, width })A dropdown; the value is the selected option index.
DragNumber({ bind, label, width })A drag-to-change numeric readout.
Button({ label, icon, action })A momentary button firing a constrained action. icon is an asset name or short text.

Set width (Theme.sizing(n) or "fill") on Select / DragNumber to stop them reflowing the row as their content changes width.

Button actions

A button's action is a constrained data object, never a closure:

ActionDoes
{ kind: "setParam", id, value }Set a param to a value.
{ kind: "setSetting", id, value }Set a setting to a value.
{ kind: "toggleSetting", id }Flip a setting.
{ kind: "cycleSetting", id }Advance a setting, wrapping.
{ kind: "trigger", id }Fire a momentary trigger param.
{ kind: "resetParams", prefix }Reset every param whose id starts with prefix to its default.
{ kind: "setParams", prefix, value }Set every param starting with prefix to value.
{ kind: "resetCurve", id }Reset a curveParam to its declared default shape (or the diagonal).
{ kind: "openEditor" }Open the full-window editor (also always available from the tile header's icon).
{ kind: "command", id, value? }Dispatch to Device.onCommand. See Control surface.

The prefix actions let one button reset a whole bank, e.g. a 16-point curve's pt_* params, instead of editing each.

Displays

Read-only live data, bound to a channel. The node fills each channel per block with ctx.writeChannel.

WidgetStatus
Spectrum({ input, output?, responseCurve?, color?, axis? })Wired.
TransferCurve({ input, color? })Wired.
CurveEditor({ bind, overlay?, background?, color?, snapToGrid?, bipolar? })Wired. Interactive editor for a curveParam. bipolar draws a center line at 0.5 for signed curves.
Scope, Meter, Waveform, XYPadDeclared, render a placeholder for now. Draw these with a canvas.

CurveEditor binds to a curve param with curve("id"). overlay: true floats it transparently over the element below. background: draws a live widget behind the now-transparent editor:

CurveEditor({
  bind: curve("shape"),
  background: Spectrum({ input: channel("spectrum") }), // heatmap under the curve
});

A custom Canvas can also read a curve param's points. Declare curve("id") as an input and sample it with p.curve(i, x). See the Canvas API.

Chrome and bindings

  • Chrome: Label({ text, size, color }), Image({ asset, width, height, color }), Group({ title }, kids).
  • Bindings: param(id), setting(id), channel(id), curve(id), modulation(id).
  • Show or hide any widget with visibleWhen: param("mode").is(2) (a select's option index, a number, or a bool). param and setting carry .is(value).

Theming

TokenUse
Theme.sizing(units)Sizes and spacing. 1 unit = 4px at 100% UI scale.
Theme.colors.accentThe node's declared palette.accent.
Theme.colors.signal.{audio, midi, modulation, feedback}Signal colours.
Theme.colors.status.{success, warning, destructive}Status colours.
Theme.colors.grey[400 | 600 | 800 | 900 | 925 | 950], grey.white, grey.blackThe greyscale ramp. Only these shades exist; a missing one falls back to the accent.
Theme.fontSizes.{2xs, xs, sm, base, lg, xl, 2xl}The type scale.

Prefer theme tokens so the device follows the user's theme; for a deliberately fixed colour, pass Theme.raw("#ff3300") or any CSS colour string. Never hardcode a pixel size. There are no radius or shadow tokens by design.

Palette

The ui(config, root) config takes a palette:

FieldSets
accentThe node's accent (a theme token or raw hex).
backgroundTile background colour.
foregroundTile text colour.
backgroundImageAn assets/ SVG/PNG drawn to fill the tile behind everything.

Full-window editor

Export a second createEditorUI(node): UI for a larger editor opened from the tile, like a plugin's big window. The graph tile shows createUI; the editor reuses every widget and the same bound params, so edits round-trip.

export function createEditorUI(node: NodeContext): UI {
  return ui(
    { size: { graph: { w: 120, h: 80 } }, palette: { accent } } /* big root */,
  );
}

The editor always opens from the tile header's "open editor" icon (shown whenever an editor exists) and from any { kind: "openEditor" } button. Set editorTrigger on the graph ui() config to widen that:

ValueOpens the editor on
"none"Header icon and openEditor button only (default).
"doubleClick"Also double-clicking anywhere on the tile body. Off by default because it collides with the double-click-to-reset gesture on knobs, hit regions, and XY pads.