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
}
versionis required by push tooling — include"version": 1.themeand 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 realrow × colrectangle:colSpandivides the width,rowSpan × rowHeightis the height (rowHeightdefaults 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,colSpansets proportional width, heights are natural (rowSpanignored). 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.