Layouts

A layout is the JSON document the app renders. As an integrator you don't usually write full layouts by hand (the editor/MCP do that), but you must understand their shape — your server's event names and msg_types have to match what the layout's controls declare. Full field-by-field detail is in Layout Schema; this page is the model.

The shape

flowchart TD L["LayoutConfig"] L --> meta["name · headerTitle · version · accentColor<br/><i>identity and theming</i>"] L --> conn["connection { url, token, identity }<br/><i>where / how to connect</i>"] L --> look["appearance · theme<br/><i>look and feel — theming is premium</i>"] L --> poll["pollGroups { … }<br/><i>timed outbound requests</i>"] L --> dtabs["dynamicTabs [ … ]<br/><i>tabs generated from server data</i>"] L --> tabs["tabs [ TabDefinition … ]"] tabs --> tab["Tab { title, icon, grid }"] tab --> children["children [ ChildDefinition … ]"] children --> ctrl["ControlDefinition<br/><i>a single control</i>"] children --> grp["GroupDefinition<br/><i>a sub-grid of children</i>"]

A layout is tabs → a grid → children. Children are either controls (a gauge, button, …) or groups (a container with its own grid). Groups nest, so you can build cards, sections, and dashboards.

Controls

Every control has at minimum:

{ "type": "gauge", "id": "cpu", "position": [0, 0], "span": [2, 2] }
  • type — what it is (see the Control Catalog).
  • id — unique within the layout; the key your data and actions reference.
  • position[row, col], zero-indexed, within the parent grid.
  • span[rowSpan, colSpan], default [1, 1].

Plus type-specific fields (min/max for a gauge, options for a picker, …), and the two behavior blocks that matter to you: sync (receive) and action (send), both detailed in Data Flow — sync & actions.

Groups

A group is a control-shaped container:

{ "type": "group", "id": "card", "position": [0,0], "span": [3,4],
  "grid": { "columns": 2, "rows": 3 },
  "children": [ /* controls positioned within the group's own grid */ ] }

Groups can be dynamic — populated at runtime from a broadcast event — which is how you render a variable number of items (see Recipe: Server-Driven (Dynamic) UI).

Dynamic structure

Two layout-level features let the server shape the UI, not just fill it:

  • pollGroups — named timers that periodically send an event with a payload. Use them to ask your server for fresh data on an interval without any user action. Each group declares { event, interval, payload }.
  • dynamicTabs — tabs whose content is generated from a broadcast event, so your server can add/replace tabs on the fly.

Both are covered in Recipe: Server-Driven (Dynamic) UI.

The connection block

"connection": {
  "url": "ws://192.168.1.50:8765",
  "token": null,
  "identity": { "name": "Dashboard", "channel": "home", "role": "viewer",
                "canBroadcast": false, "canRoute": false }
}

This is how the device finds your relay and channel. A layout with no connection runs fully offline (useful for demos). See Connection & Pairing for transports, tokens, and pairing.

What you actually need to agree on

For your server and a layout to work together, three things must line up:

  1. Channel — server and layout join the same channel.
  2. Receive contract — for each display control, the event (usually "broadcast"), the filter (msg_type), and the valuePath it reads.
  3. Send contract — for each input control, the event (verb) and the msg_type it tags its action payload with.

Keep a short table of your msg_types and valuePaths; it is your API.

Next: Connection & Pairing.