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.
| Widget | Purpose |
|---|---|
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").
| Widget | Notes |
|---|---|
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:
| Action | Does |
|---|---|
{ 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.
| Widget | Status |
|---|---|
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, XYPad | Declared, 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).paramandsettingcarry.is(value).
Theming
| Token | Use |
|---|---|
Theme.sizing(units) | Sizes and spacing. 1 unit = 4px at 100% UI scale. |
Theme.colors.accent | The 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.black | The 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:
| Field | Sets |
|---|---|
accent | The node's accent (a theme token or raw hex). |
background | Tile background colour. |
foreground | Tile text colour. |
backgroundImage | An 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:
| Value | Opens 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. |