Layout Schema

Field reference for the layout JSON. Concepts are in Layouts; this is the lookup. (The app is the source of truth; treat unspecified fields as optional with sane defaults.)

LayoutConfig (top level)

{
  "name": "string (required) — display name",
  "headerTitle": "string — header bar title (defaults to name)",
  "version": 1,
  "accentColor": "#hex — app accent color (default #667eea)",
  "connection": { ConnectionConfig } | null,
  "appearance": { AppearanceConfig } | null,
  "theme": { ThemeConfig } | null,
  "tabs": [ TabDefinition, … ],
  "pollGroups": { "name": { "event": "string", "interval": number, "payload": any } },
  "dynamicTabs": [ { "event": "string" } ],
  "state": { StateConfig } | null
}

version is required by push tooling — include "version": 1. theme and per-control theming are premium (custom theming requires the Pro entitlement; otherwise the default glass theme is used). See Control Catalog.

ConnectionConfig

{
  "url": "wss://… or ws://… (required)",
  "token": "string | null",
  "e2eeKey": "base64 string | null",
  "identity": {
    "name": "string — device display name",
    "channel": "string — mesh channel",
    "role": "string — mesh role",
    "canBroadcast": false,
    "canRoute": false
  }
}

See Connection & Pairing.

StateConfig

Opt-in device-held state sync for shared rooms. Off by default (omit it for legacy behavior). The relay stores nothing — devices reconcile among themselves on join.

{
  "sync": false,
  "authority": "string | null"
}
Field Meaning
sync When true, a chat control in the layout backfills its history from peers on join/reconnect, and control values are pulled from the authority.
authority Who is the source of truth for control values: a role ("device"/"hub") matched against the connection's role, or an exact connection name. Omit for chat-only sync.

A joining device asks the room for the current state; the authority answers with a snapshot it adopts. Only the authority answers, and a node that is itself the authority never adopts an incoming snapshot. The hub side is enable_state_authority() / set_control_state() in carterkit. Chat history uses the same broadcast handshake (any peer that holds the log answers).

TabDefinition

{
  "title": "string (required)",
  "icon": "string — SF Symbol name (required)",
  "grid": { "columns": int, "rows": int, "mode": "grid|flow", "rowHeight": int },
  "children": [ ChildDefinition, … ]
}

ChildDefinition — control or group

ControlDefinition

{
  "type": "button|toggle|slider|stepper|segmented|picker|datePicker|textInput|colorPicker|label|image|gauge|sparkline|progressRing|statusLight|map|graph|chat|qrCode|webView|logConsole|joystick|list|cardList|divider|spacer|…",
  "id": "string (required, unique)",
  "position": [row, col],
  "span": [rowSpan, colSpan],         // default [1,1] — 2-D: colSpan=width, rowSpan=height
  "controlHeight": int,               // override the grid-derived height (rarely needed)
  "label": "string",
  "defaultValue": any,
  "icon": "string — SF Symbol",
  "tint": "#hex",
  "hideLabel": bool,
  "hideValue": bool,                  // ring/gauge: show just the visual, hide the number
  "hideBackground": bool,             // drop the card; control floats + fills its cell

  // type-specific (see control-catalog):
  "min": number, "max": number, "step": number,
  "minIcon": "string", "maxIcon": "string",
  "options": ["string", …],
  "placeholder": "string",
  "text": "string (label type)",
  "style": "string — per-control variant; label mono|large-mono|terminal renders an ANSI terminal",
  "scrollable": bool,                 // label: fixed-height scrolling terminal/log view (pair with controlHeight)
  "sendButton": bool, "minLines": int, "maxLines": int,   // textInput: growing composer (Send submits, Return = newline)
  "autocorrect": bool, "autocorrectToggle": bool,         // textInput: keyboard default + inline live "Aa" toggle
  "keyboard": "ascii|default|url|email|numbers",          // textInput: keyboard type
  "systemName": "string (image type — SF Symbol)",
  "gaugeStyle": "full|three_quarter|half",
  "segments": [{ "limit": number, "color": "#hex" }],
  "progressStyle": "ring|bar",
  "sparklinePoints": int, "sparklineFill": bool,
  "pickerStyle": "menu|wheel|inline",
  "datePickerStyle": "compact|wheel|graphical",
  "datePickerMode": "date|time|dateAndTime",
  "mapStyle": "standard|satellite|hybrid", "mapInteractive": bool,
  "graphConfig": { … see the in-app graph doc },
  "config": { "target": string|null, "showTypingIndicators": bool, "historyCount": int }, // chat

  // behavior (see data-flow):
  "action": ActionDefinition,
  "sync": [ SyncDefinition, … ],
  "visible": { "when": "control-id", "operator": "eq|neq|gt|lt|gte|lte", "value": any },
  "haptic": "light|medium|heavy|success|warning|error|selection",
  "animation": "snappy|smooth|bouncy|gentle|instant",
  "longPressGroup": GroupDefinition,
  "longPressAction": ActionDefinition
}

GroupDefinition

{
  "type": "group",
  "id": "string (required)",
  "label": "string",
  "position": [row, col],
  "span": [rowSpan, colSpan],
  "grid": { "columns": int, "rows": int, "mode": "grid|flow", "rowHeight": int },
  "children": [ ChildDefinition, … ],
  "dynamic": "string — event name for dynamic content",
  "visible": { … }
}

ActionDefinition (device → server)

{
  "method": "meshsocket",
  "mode": "request | broadcast",
  "event": "broadcast_request | route_msg | route_msg_noreply",
  "payload": { "key": "{{value}}" }
}

mode: "request" awaits a reply; otherwise fire-and-forget. {{value}} is substituted with the control's value (native type when the whole string is exactly "{{value}}"). See Data Flow — sync & actions.

SyncDefinition (server → device)

{
  "method": "meshsocket",
  "type": "listen",
  "event": "broadcast | <custom>",
  "filter": { "key": "value" },
  "valuePath": "dot.notation.path"
}

event: "broadcast" listens on the shared channel; filter selects frames (shallow equality on each key); valuePath extracts the value. See Data Flow — sync & actions.

Grid positioning

A grid is columns × rows. It has two modes:

  • "grid" (default) — true 2-D. Each child occupies a real row × col rectangle: colSpan divides the width, rowSpan × rowHeight is the height (rowHeight defaults to 56pt). A tall control can sit beside two stacked shorter ones. Best for dashboards and mixed screens.
  • "flow" — legacy row-banded: children group by row index, colSpan sets proportional width, heights are natural (rowSpan ignored). Best for a full-page single control (map/chat/cardList) or a plain form.

Field rules:

  • position: [row, col] — zero-indexed within the parent grid.
  • span: [rowSpan, colSpan] — cells occupied (default [1, 1]).
  • Children must fit their parent grid (tab or group).
  • A square control (ring, full gauge) reads best at ~3 rowSpan; inputs at 1.

Icons

SF Symbol names, e.g. slider.horizontal.3, bolt.fill, gauge.with.dots.needle.67percent, map.fill, chart.line.uptrend.xyaxis, house.fill, antenna.radiowaves.left.and.right.

See also Control Catalog and Message Reference.