> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voiceos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Glance block reference

> Every result-card block — props, caps, and rendering behavior.

Glance blocks are the vocabulary of [result cards](/integrations/result-cards),
attached at runtime via `glanceResult([...])`. This page is exhaustive; the
renderer silently drops anything off-schema, so treat every cap as hard.

## Global rules

* **1–3 blocks per card**; at most **one chart** (`bars`, `line`, `splitBar`) —
  extras are dropped, not truncated.
* Every string is trimmed and must be non-empty; every number must be finite.
* `tone` is always `"neutral" | "good" | "bad"` — a meaning, not a color. The
  renderer owns hues.
* Glance cards render on the notch's dark glass; there is no light variant on
  this surface.
* One failing block never sinks the card (per-block error boundaries), and one
  invalid block never invalidates its siblings — it's dropped and counted.

## `header`

| Prop       | Type       | Required | Cap                          |
| ---------- | ---------- | -------- | ---------------------------- |
| `icon`     | glyph name | —        | [allowlist](#glyphs)         |
| `appIcon`  | `string`   | —        | 1–40 chars; wins over `icon` |
| `title`    | `string`   | ✅        | 1–60 chars                   |
| `trailing` | `string`   | —        | 1–40 chars, right-aligned    |

`appIcon` renders a real app logo (e.g. `"notion"`) — but only when the
visible title starts with that app's name; otherwise the slot is dropped so
one brand can't wear another's icon.

## `stats`

| Prop            | Type     | Required | Cap                                             |
| --------------- | -------- | -------- | ----------------------------------------------- |
| `items`         | array    | ✅        | 1–3 items                                       |
| `items[].label` | `string` | ✅        | 1–48 chars                                      |
| `items[].value` | `string` | ✅        | 1–20 chars — pre-format numbers                 |
| `items[].delta` | `string` | —        | 1–12 chars; colors by sign (`-` red, `+` green) |
| `items[].tone`  | tone     | —        | colors the value                                |

Wraps to a second line before truncating.

## `bars` <sup>chart</sup>

| Prop     | Type       | Required | Cap                                        |
| -------- | ---------- | -------- | ------------------------------------------ |
| `title`  | `string`   | —        | 1–60 chars                                 |
| `labels` | `string[]` | ✅        | 2–7 items, each 1–12 chars                 |
| `values` | `number[]` | ✅        | same length as `labels`; each ≥ 0          |
| `unit`   | `string`   | —        | 1–8 chars; `$ ¥ € £` prefix, others suffix |

Every bar carries its value label; the series switches to compact form
(`16K`, `1.2M`) when exact numbers would collide.

## `line` <sup>chart</sup>

| Prop            | Type       | Required | Cap                                      |
| --------------- | ---------- | -------- | ---------------------------------------- |
| `title`         | `string`   | —        | 1–60 chars                               |
| `points`        | `number[]` | ✅        | 2–60 finite numbers (negatives allowed)  |
| `start` / `end` | `string`   | —        | 1–16 chars; x-axis edge labels           |
| `unit`          | `string`   | —        | 1–8 chars                                |
| `tone`          | tone       | —        | tints the trend green/red                |
| `baseline`      | `number`   | —        | dashed reference line, pulled into range |

The latest value is always called out.

## `splitBar` <sup>chart</sup>

| Prop       | Type               | Required | Cap                                     |
| ---------- | ------------------ | -------- | --------------------------------------- |
| `title`    | `string`           | —        | 1–60 chars                              |
| `unit`     | `string`           | —        | 1–8 chars                               |
| `segments` | `[label, value][]` | ✅        | 2–4 tuples; label 1–20 chars; value ≥ 0 |

Segment colors are assigned **by index** (blue → teal → purple → neutral) —
never by the author, and never green/red, so composition can't be confused
with good/bad. Sliver segments are floored at \~2% so they stay visible.

## `progress`

| Prop    | Type              | Required | Default   | Cap                                                     |
| ------- | ----------------- | -------- | --------- | ------------------------------------------------------- |
| `style` | `"bar" \| "ring"` | —        | `"bar"`   | —                                                       |
| `value` | `number`          | ✅        | —         | ≥ 0                                                     |
| `max`   | `number`          | —        | **`100`** | > 0                                                     |
| `label` | `string`          | —        | —         | 1–60 chars                                              |
| `icon`  | glyph             | —        | —         | allowlist; ring shows it in the center instead of the % |
| `tone`  | tone              | —        | neutral   | —                                                       |

<Warning>
  `max` defaults to **100**, not 1 — `{ "value": 0.4 }` renders as 0%. Send
  `{ "value": 40 }` or set `max` explicitly.
</Warning>

## `list`

| Prop              | Type              | Required | Cap                                                             |
| ----------------- | ----------------- | -------- | --------------------------------------------------------------- |
| `header`          | `string`          | —        | 1–60 chars                                                      |
| `rows`            | array             | ✅        | 1–6 rows                                                        |
| `rows[].icon`     | glyph             | —        | allowlist                                                       |
| `rows[].appIcon`  | `string`          | —        | 1–40 chars; wins over `icon`; same title-match gate as `header` |
| `rows[].title`    | `string`          | ✅        | 1–60 chars                                                      |
| `rows[].subtitle` | `string`          | —        | 1–72 chars                                                      |
| `rows[].trailing` | `string`          | —        | 1–24 chars                                                      |
| `rows[].badge`    | `{ text, tone? }` | —        | text 1–24 chars                                                 |

`badge` and `trailing` are mutually exclusive at render time — a badge wins.
A subtitle that repeats the badge text is dropped automatically.

## `keyValue`

| Prop    | Type               | Required | Cap                                      |
| ------- | ------------------ | -------- | ---------------------------------------- |
| `pairs` | `[label, value][]` | ✅        | 1–5 tuples; label 1–32, value 1–64 chars |

## `clock` <sup>live</sup>

| Prop    | Type                    | Required | Default                      |
| ------- | ----------------------- | -------- | ---------------------------- |
| `tz`    | IANA timezone           | —        | local time                   |
| `label` | `string` (1–60)         | —        | city from `tz`, else "Local" |
| `style` | `"analog" \| "digital"` | —        | `"analog"`                   |

Ticks every second. An unknown timezone falls back to local time **and**
relabels itself "Local" rather than showing a wrong city.

## `countdown` <sup>live</sup>

| Prop      | Type              | Required | Cap                        |
| --------- | ----------------- | -------- | -------------------------- |
| `until`   | ISO-8601 datetime | one of   | — (no cap)                 |
| `seconds` | `number`          | one of   | > 0, ≤ 2,592,000 (30 days) |
| `label`   | `string`          | —        | 1–60 chars                 |

Exactly one of `until` / `seconds` is required. Prefer `until` — an absolute
target survives remounts. The 30-day cap applies to `seconds` only; a
far-future `until` renders honestly (`168d 04:30`). Turns green at zero.

## `badges`

| Prop    | Type                | Required | Cap                  |
| ------- | ------------------- | -------- | -------------------- |
| `items` | `{ text, tone? }[]` | ✅        | 1–3; text 1–24 chars |

## `divider`

`{ "type": "divider" }` — no props.

## `row`

| Prop       | Type        | Required | Cap          |
| ---------- | ----------- | -------- | ------------ |
| `children` | leaf blocks | ✅        | 2–3 children |

Lays children side by side. Rules: rows never nest, `widget` can't be a child,
and at most one chart per row (it consumes the card's chart budget).

Children size themselves two ways:

| Behavior                      | Blocks                                                    |
| ----------------------------- | --------------------------------------------------------- |
| **Stretch** to fill the space | charts, `list`, `stats`, `keyValue`, bar-style `progress` |
| **Hug** their content         | rings, `clock`, `countdown`, `badges`                     |

## `widget`

The sandboxed escape hatch — see [Custom widgets](/integrations/widgets).

| Prop     | Type              | Required | Cap                                   |
| -------- | ----------------- | -------- | ------------------------------------- |
| `html`   | `string`          | ✅        | 1–131,072 chars                       |
| `height` | `number`          | —        | 60–420 (default 180)                  |
| `data`   | JSON-serializable | —        | ≤ 16,384 chars serialized             |
| `label`  | `string`          | —        | 1–60 chars (frame's accessible title) |

Not available to the model — only integration tool results can carry one.
Cannot sit inside a `row`.

## Glyphs

The 34-name allowlist for every `icon` prop:

```text theme={null}
calendar clock timer bed car mail message music sun moon cloud rain star
folder file globe pin phone person heart bolt check x mic note list
battery chart dollar home wifi coffee plane sparkle
```

Unknown names invalidate the block. Rendered as stroked template icons that
inherit the surrounding ink color.

## Parse & degradation semantics

* The wire format is `{ "blocks": [...] }` under the `_voiceos_glance` key of
  your tool's JSON result; VoiceOS strips it before the model reads the
  result.
* Invalid blocks are dropped individually; a card with zero valid blocks is
  dropped whole.
* After validation, the first 3 blocks survive and the first chart consumes
  the chart budget; the rest are dropped.
* Extraction caps the whole glance payload at 32,000 chars — raised to
  96,000 when a widget block is present.
