@terreno/mcp 57.12.0 → 57.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/docs/upgrades/57.13.0.md +75 -0
  2. package/dist/docs/versioned/next/explanation/README.md +1 -1
  3. package/dist/docs/versioned/next/explanation/agent-ui-blocks.md +69 -9
  4. package/dist/docs/versioned/next/explanation/example-coverage.md +3 -3
  5. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +12 -0
  6. package/dist/docs/versioned/next/how-to/README.md +2 -0
  7. package/dist/docs/versioned/next/how-to/agent-ui-blocks.md +187 -3
  8. package/dist/docs/versioned/next/how-to/circleci.md +2 -0
  9. package/dist/docs/versioned/next/how-to/preview-demo.md +5 -5
  10. package/dist/docs/versioned/next/how-to/rate-limiting.md +17 -0
  11. package/dist/docs/versioned/next/how-to/test-ui-accessibility.md +35 -0
  12. package/dist/docs/versioned/next/implementationPlans/agent-ui-rich-replies.md +359 -0
  13. package/dist/docs/versioned/next/reference/README.md +1 -1
  14. package/dist/docs/versioned/next/reference/admin-backend.md +26 -0
  15. package/dist/docs/versioned/next/reference/admin-frontend.md +15 -0
  16. package/dist/docs/versioned/next/reference/ai.md +55 -3
  17. package/dist/docs/versioned/next/reference/api.md +4 -0
  18. package/dist/docs/versioned/next/reference/blocks.md +243 -11
  19. package/dist/docs/versioned/next/reference/environment-variables.md +3 -0
  20. package/dist/docs/versioned/next/reference/ui.md +212 -10
  21. package/dist/docs/versioned/next/tasks/agent-ui-rich-replies.md +188 -0
  22. package/package.json +3 -3
  23. package/src/docs/upgrades/57.13.0.md +75 -0
  24. package/src/docs/versioned/next/explanation/README.md +1 -1
  25. package/src/docs/versioned/next/explanation/agent-ui-blocks.md +69 -9
  26. package/src/docs/versioned/next/explanation/example-coverage.md +3 -3
  27. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +12 -0
  28. package/src/docs/versioned/next/how-to/README.md +2 -0
  29. package/src/docs/versioned/next/how-to/agent-ui-blocks.md +187 -3
  30. package/src/docs/versioned/next/how-to/circleci.md +2 -0
  31. package/src/docs/versioned/next/how-to/preview-demo.md +5 -5
  32. package/src/docs/versioned/next/how-to/rate-limiting.md +17 -0
  33. package/src/docs/versioned/next/how-to/test-ui-accessibility.md +35 -0
  34. package/src/docs/versioned/next/implementationPlans/agent-ui-rich-replies.md +359 -0
  35. package/src/docs/versioned/next/reference/README.md +1 -1
  36. package/src/docs/versioned/next/reference/admin-backend.md +26 -0
  37. package/src/docs/versioned/next/reference/admin-frontend.md +15 -0
  38. package/src/docs/versioned/next/reference/ai.md +55 -3
  39. package/src/docs/versioned/next/reference/api.md +4 -0
  40. package/src/docs/versioned/next/reference/blocks.md +243 -11
  41. package/src/docs/versioned/next/reference/environment-variables.md +3 -0
  42. package/src/docs/versioned/next/reference/ui.md +212 -10
  43. package/src/docs/versioned/next/tasks/agent-ui-rich-replies.md +188 -0
@@ -54,7 +54,7 @@ A dataset name matches `^[a-z][a-z0-9_]{0,63}$`. A document has at most 8 datase
54
54
  | `image` | `alt`, `src` | `id` |
55
55
  | `details` | `title`, `text` | `id` |
56
56
 
57
- `src` is a `data:image` URL, a `file:` ref (`file:` plus an id), or an `https` URL. `https` is allowed only when the hostname is in `validateBlocks` `imageHosts` (the server option is `uiBlocks.imageHosts`). Anything else is `IMAGE_HOST_NOT_ALLOWED`. An empty list rejects every `https` image. A ref dataset `id` uses 1–80 letters, digits, underscores, or hyphens. A block or action-element `id` stays lowercase and starts with a letter. `limit` of `0` is `INVALID_TYPE` (an integer of at least 1), not `TOO_SHORT`.
57
+ `src` (on an `image` block, on each `gallery` tile, and on a `list` item's `image`) is a `data:image` URL, a `file:` ref (`file:` plus an id), or an `https` URL. `https` is allowed only when the hostname is in `validateBlocks` `imageHosts` (the server option is `uiBlocks.imageHosts`). Anything else is `IMAGE_HOST_NOT_ALLOWED`. An empty list rejects every `https` image. A ref dataset `id` uses 1–80 letters, digits, underscores, or hyphens. A block or action-element `id` stays lowercase and starts with a letter. `limit` of `0` is `INVALID_TYPE` (an integer of at least 1), not `TOO_SHORT`.
58
58
 
59
59
  Warnings do not block rendering:
60
60
 
@@ -74,15 +74,153 @@ Warnings do not block rendering:
74
74
  | `badge` | `text` (1–80) | `status`: `info`, `error`, `warning`, `success`, `neutral`, `active`; `id` | `Badge` |
75
75
  | `divider` | — | `id` | `SectionDivider` |
76
76
  | `context` | `text` (1–280) | `id` | `Text` |
77
+ | `stepper` | see [Stepper](#stepper) | | `IconButton` − and +, `Text`, `Heading` |
78
+ | `checklist` | see [Checklist](#checklist) | | `CheckBox` rows, `Text`, `Heading` |
79
+ | `gallery` | see [Gallery](#gallery) | | `Image` tiles in a row or grid, `Text` captions |
80
+ | `list` | see [List](#list) | | `Image` thumbnails beside `Text` title, text, and meta |
77
81
  | `columns` | `children`: 2–4 blocks | `id` | `Box` row |
78
- | `card` | `children`: at least 1 block | `title` (1–120), `id` | `Card` |
82
+ | `card` | `children`: at least 1 block | `title` (1–120), `eyebrow` (1–60), `id` | `Card` |
79
83
 
80
84
  `columns` and `card` sit at the top level. Their children are leaf blocks. A layout block
81
85
  inside another layout block is `DEPTH_EXCEEDED`. Every block counts toward the 50-block
82
86
  cap, including the `card` or `columns` block itself, so a card cannot hold 50 children.
83
87
 
88
+ A card's `eyebrow` is a short label that sits small and muted above the `title` (or above the
89
+ children when there is no title). `BlocksView` draws it as a `Text` in the same small, muted style
90
+ as the stepper label, with test ID `<card path>-eyebrow`.
91
+
84
92
  `id` matches `^[a-z][a-z0-9_]{0,63}$`.
85
93
 
94
+ ## Stepper
95
+
96
+ A `stepper` is a − value + control. Each tap calls a host callback, and the host returns the
97
+ replacement stepper (see `scaleStepperHostAction` in [the AI reference](ai.md)).
98
+
99
+ | Field | Required | Rule |
100
+ | --- | --- | --- |
101
+ | `id` | yes | Block id pattern, at most 54 characters |
102
+ | `label` | yes | 1–80 |
103
+ | `value`, `min`, `max` | yes | Numbers. `min` < `max` and `min` ≤ `value` ≤ `max`, or `OUT_OF_RANGE` |
104
+ | `step` | no | Above 0 (`OUT_OF_RANGE`). Default 1 |
105
+ | `unit` | no | 1–40, shown under the value ("People") |
106
+ | `callback` | yes | `{name, payload?}`. `name` is checked by `UNKNOWN_HOST_ACTION` against `stepperActions` when that is passed, otherwise against `hostActions` |
107
+ | `itemsTitle` | no | 1–80, a small heading above the items |
108
+ | `items` | no | 0–12 of `{label (1–80), amount, unit? (1–20), decimals? (0–3, default 0), round?: nearest or up (default nearest)}`. `decimals` below 0 or above 3 is `OUT_OF_RANGE` |
109
+ | `note` | no | 1–280, muted under the items |
110
+
111
+ ```yaml
112
+ - type: stepper
113
+ id: guests
114
+ label: Number of people
115
+ unit: People
116
+ value: 5
117
+ min: 1
118
+ max: 20
119
+ callback: {name: scaleStepper}
120
+ items:
121
+ - {label: Bone-in leg of lamb, amount: 2, unit: kg, decimals: 1}
122
+ - {label: Carrots, amount: 8, round: up}
123
+ note: Generous portions, with a little extra.
124
+ ```
125
+
126
+ The renderer uses the element ids `<id>_decrease` and `<id>_increase` for the − and + buttons
127
+ (`stepperElementIds(id)`). Another block or action element with either id is `DUPLICATE_ID`.
128
+ A tap sends `{kind: callback, name, payload: {...payload, value: value ± step}}` with
129
+ `blockId: <id>`.
130
+
131
+ ## Checklist
132
+
133
+ A `checklist` is a list of tickable items with an "n of m" counter. `BlocksView` draws the title
134
+ and counter, then one `CheckBox` row per item: the bold `text`, then the small muted `meta` (such
135
+ as a time), then the muted `detail`. With a `callback` whose name is in `hostActions` (or with `hostActions` omitted), a
136
+ tick calls `onAction` with `blockId: <id>`, `elementId: <id>_<item id>`, and
137
+ `{kind: "callback", name, payload: {...payload, itemId, checked, state}}`. `state` maps every
138
+ item id to its tick after this one. `BlocksView` disables the checklist while a tick is
139
+ pending, and the tick shows only when the host's returned checklist arrives as an override.
140
+ Without a `callback`, or when its name is not in `hostActions`, ticks stay on the device. On the
141
+ server, `toggleChecklistHostAction` from `@terreno/ai` handles the callback and returns the stored
142
+ checklist with `checked` from `state` (see [Host actions](ai.md#host-actions)).
143
+
144
+ | Field | Required | Rule |
145
+ | --- | --- | --- |
146
+ | `id` | yes | Block id pattern, at most 31 characters |
147
+ | `title` | no | 1–120 |
148
+ | `callback` | no | `{name, payload?}`. `name` is checked by `UNKNOWN_HOST_ACTION` against `checklistActions` when that is passed, otherwise against `hostActions` |
149
+ | `items` | yes | 1–30 of `{id, text (1–120), detail? (1–280), meta? (1–40), checked?: boolean}`. An item `id` uses the block id pattern, at most 32 characters, and is unique in the checklist (`DUPLICATE_ID`) |
150
+
151
+ ```yaml
152
+ - type: checklist
153
+ id: cooking
154
+ title: Cooking checklist
155
+ callback: {name: toggleChecklist}
156
+ items:
157
+ - {id: oven, meta: "1:00 pm", text: Preheat the oven, detail: "220 C, fan off.", checked: true}
158
+ - {id: lamb_in, meta: "1:30 pm", text: Put the lamb in}
159
+ ```
160
+
161
+ Each item's tick has the reserved element id `<id>_<item id>` (`checklistElementId(id, itemId)`).
162
+ Another block or action element with that id, or a stepper or checklist that derives the same
163
+ element id, is `DUPLICATE_ID`.
164
+
165
+ ## Gallery
166
+
167
+ A `gallery` is a set of 2–6 photos shown together. `BlocksView` draws up to three in one row of
168
+ equal 4:3 tiles and wraps more into a three-column grid. On a narrow screen the tiles keep a
169
+ minimum width and the row scrolls sideways (see [BlocksView](ui.md#blocksview)).
170
+
171
+ | Field | Required | Rule |
172
+ | --- | --- | --- |
173
+ | `id` | no | Block id pattern |
174
+ | `images` | yes | 2–6 of `{src, alt (1–200), caption? (1–120)}`. Fewer is `TOO_FEW`, more is `TOO_MANY` |
175
+
176
+ Each tile's `src` follows the `image` rules above. A tile that breaks them is
177
+ `IMAGE_HOST_NOT_ALLOWED` at that tile's path, such as `blocks[0].images[2].src`.
178
+
179
+ ```yaml
180
+ - type: gallery
181
+ id: roast_photos
182
+ images:
183
+ - {src: "file:roast-lamb", alt: Roast leg of lamb on a carving board, caption: Roast lamb}
184
+ - {src: "file:roast-potatoes", alt: Crisp roast potatoes in a tray}
185
+ - {src: "https://images.example.com/carrots.jpg", alt: Glazed carrots, caption: "Honey, thyme, and butter"}
186
+ ```
187
+
188
+ The third tile validates only when `imageHosts` includes `images.example.com`.
189
+
190
+ ## List
191
+
192
+ A `list` is a stack of 1–12 entries, each with a title and an optional thumbnail. `BlocksView`
193
+ draws each item as a row: a 3:4 portrait thumbnail on the left, then the small muted `meta`, the
194
+ bold `title`, and the muted `text` (see [BlocksView](ui.md#blocksview)).
195
+
196
+ | Field | Required | Rule |
197
+ | --- | --- | --- |
198
+ | `id` | no | Block id pattern |
199
+ | `items` | yes | 1–12 items. Fewer is `TOO_FEW`, more is `TOO_MANY` |
200
+ | `items[].title` | yes | 1–120 characters |
201
+ | `items[].text` | no | 1–500 characters of plain text, not markdown |
202
+ | `items[].meta` | no | 1–40 characters, a short label such as a time or a price |
203
+ | `items[].image` | no | `{src, alt}`. `alt` is 1–200 characters, the same cap as an `image` block's `alt` (`BLOCK_LIMITS.headingTextMaxLength`) |
204
+
205
+ An item image's `src` follows the `image` rules above. One that breaks them is
206
+ `IMAGE_HOST_NOT_ALLOWED` at that item's path, such as `blocks[0].items[1].image.src`.
207
+
208
+ ```yaml
209
+ - type: list
210
+ id: menu
211
+ items:
212
+ - title: Roast leg of lamb
213
+ text: Rubbed with garlic and rosemary, then rested for 20 minutes.
214
+ meta: Main
215
+ image: {src: "file:roast-lamb", alt: Roast leg of lamb on a carving board}
216
+ - title: Honey carrots
217
+ text: "Glazed with honey, thyme, and butter."
218
+ image: {src: "https://images.example.com/carrots.jpg", alt: Glazed carrots}
219
+ - title: Mint sauce
220
+ ```
221
+
222
+ The second item validates only when `imageHosts` includes `images.example.com`.
223
+
86
224
  ## Actions
87
225
 
88
226
  An `actions` block requires `id` and `elements` (1–25). An element is a `button` or a
@@ -94,10 +232,76 @@ An `actions` block requires `id` and `elements` (1–25). An element is a `butto
94
232
  | `segmented` | `id`, `target` (a chart or table `id`), `options` (2–8 of `{label, data}`) | — |
95
233
 
96
234
  `action.kind` is `reply` (`text`), `open` (`url` or `route`, exactly one), `select`
97
- (`target` plus `data`), or `callback` (`name` plus optional `payload`). A `select` target
98
- that is not a chart or table `id` is `SELECT_TARGET_INVALID`. When `validateBlocks` is
235
+ (`target` plus `data`), `callback` (`name` plus optional `payload`), or `copy` (`text` or
236
+ `target`, exactly one; see [Copy](#copy)). A `select` target that is not a chart or table `id`
237
+ is `SELECT_TARGET_INVALID`. When `validateBlocks` is
99
238
  called with `hostActions`, a callback `name` outside that list is `UNKNOWN_HOST_ACTION`.
100
- Omitting `hostActions` skips that check.
239
+ Omitting `hostActions` skips that check. A stepper `callback.name` is checked against
240
+ `stepperActions` instead when that list is passed, so a stepper that names a registered action
241
+ that does not handle steppers is `UNKNOWN_HOST_ACTION`. `checklistActions` does the same for
242
+ checklist callbacks.
243
+
244
+ ## Copy
245
+
246
+ A `copy` action puts text on the device clipboard. It never reaches the host, so `hostActions`
247
+ does not apply to it. It is a `button` element's `action`:
248
+
249
+ | Field | Rule |
250
+ | --- | --- |
251
+ | `text` | 1–4,000 characters (`BLOCK_LIMITS.copyTextMaxLength`), copied as written |
252
+ | `target` | The `id` of a `stepper`, `checklist`, `list`, `table`, or `text` block in this document (`COPY_TARGET_TYPES`). Any other block, or an id no block has, is `COPY_TARGET_INVALID` |
253
+
254
+ Set exactly one of the two. Neither or both is `MISSING_REQUIRED` at the action's path, as for
255
+ `open`. The target may sit anywhere in the document, before or after the button, inside a
256
+ `card` or `columns`.
257
+
258
+ ```yaml
259
+ v: 1
260
+ blocks:
261
+ - type: stepper
262
+ id: guests
263
+ label: Number of people
264
+ unit: People
265
+ value: 5
266
+ min: 1
267
+ max: 20
268
+ callback: {name: scaleStepper}
269
+ items:
270
+ - {label: Bone-in leg of lamb, amount: 2, unit: kg, decimals: 1}
271
+ - {label: Carrots, amount: 8}
272
+ - type: actions
273
+ id: shopping
274
+ elements:
275
+ - type: button
276
+ id: copy_list
277
+ text: Copy shopping list
278
+ variant: outline
279
+ action: {kind: copy, target: guests}
280
+ - type: button
281
+ id: copy_note
282
+ text: Copy note
283
+ action: {kind: copy, text: "Lamb 2 kg, carrots 8, mint sauce."}
284
+ ```
285
+
286
+ `blockPlainText(block, {datasets?, checked?})` turns a target block into the copied text. Lines
287
+ are joined with `\n`, with no trailing newline.
288
+
289
+ | Block | Text |
290
+ | --- | --- |
291
+ | `stepper` | `<label>: <value> <unit>`, then one `<item label>: <amount> <unit>` line per item. Amounts use the item's `decimals` (2 at 1 decimal is `2.0`). A missing unit leaves no trailing space. For the example above: `Number of people: 5 People`, `Bone-in leg of lamb: 2.0 kg`, `Carrots: 8` |
292
+ | `checklist` | One `[x] <text>` or `[ ] <text>` line per item. `checked` maps item ids to the current ticks and overrides each item's `checked`; ids the checklist lacks are ignored |
293
+ | `list` | One `- <title>: <text>` line per item, or `- <title>` when the item has no text |
294
+ | `table` | A header row of column names (the table's `columns` in order, or every dataset column) with each `_` shown as a space, as the table on screen shows them (`lamb_kg` is `lamb kg`), then one row per dataset row, tab-separated. A `null` cell is empty. The table's dataset must be inline in `datasets`; a missing dataset or an unresolved `ref` returns an empty string, so resolve refs first |
295
+ | `text` | The `markdown` as written |
296
+
297
+ Any other block returns an empty string. Pass the block as it is shown, with a host's
298
+ replacement block applied, so a stepper at 6 copies the scaled amounts.
299
+
300
+ `BlocksView` performs the copy on the device. A press builds the text with `blockPlainText`
301
+ from the target as it is shown now (the host's override, the device's checklist ticks, and the
302
+ table's selected dataset once it has rows), writes it with `expo-clipboard`, and shows "Copied"
303
+ next to the button for 2 seconds. It never calls `onAction`. See
304
+ [BlocksView](./ui.md#blocksview).
101
305
 
102
306
  ## Partial parsing
103
307
 
@@ -122,6 +326,21 @@ Unknown fields fail with `UNKNOWN_KEY`. `v` must be `1` (`UNSUPPORTED_VERSION`).
122
326
  | `columns` children | 2–4 |
123
327
  | Elements in one `actions` block | 25 |
124
328
  | Text in one `text` block | 4,000 characters |
329
+ | Items in one `stepper` | 12 |
330
+ | `stepper` id | 54 characters |
331
+ | Items in one `checklist` | 1–30 |
332
+ | `checklist` id | 31 characters |
333
+ | `checklist` item id | 32 characters |
334
+ | Images in one `gallery` | 2–6 |
335
+ | `gallery` image `alt` | 200 characters |
336
+ | `gallery` image `caption` | 120 characters |
337
+ | Items in one `list` | 1–12 |
338
+ | `list` item `title` | 120 characters |
339
+ | `list` item `text` | 500 characters |
340
+ | `list` item `meta` | 40 characters |
341
+ | `list` item image `alt` | 200 characters |
342
+ | `card` `eyebrow` | 60 characters |
343
+ | `copy` action `text` | 4,000 characters |
125
344
  | Document version | 1 |
126
345
 
127
346
  ## Errors
@@ -134,19 +353,21 @@ heuristics and does not fail `ok`.
134
353
  | --- | --- |
135
354
  | `COLUMN_NOT_FOUND` | A chart or table names a column the dataset does not have. |
136
355
  | `COLUMN_TYPE_MISMATCH` | A column value, or a chart axis, does not match the column type. |
356
+ | `COPY_TARGET_INVALID` | A copy action's `target` names a block that is not a `stepper`, `checklist`, `list`, `table`, or `text` block, or an id no block in the document has. |
137
357
  | `DATASET_NOT_FOUND` | A chart or table names a dataset the document does not define. |
138
358
  | `DATASET_TOO_LARGE` | A dataset has more than 500 rows or 12 columns. |
139
359
  | `DEPTH_EXCEEDED` | A `columns` or `card` block is nested inside another layout block. |
140
- | `DUPLICATE_ID` | A block id, an action-element id, or a column name is used more than once. |
360
+ | `DUPLICATE_ID` | A block id, an action-element id, or a column name is used more than once, an id takes a stepper's `<id>_decrease` or `<id>_increase` or a checklist's `<id>_<item id>`, or a checklist repeats an item id. |
141
361
  | `HTML_DISABLED` | An html block is present and this host has not turned HTML on. |
142
362
  | `HTML_TOO_LARGE` | An html block is larger than 100,000 bytes. |
143
- | `IMAGE_HOST_NOT_ALLOWED` | An image URL is not a `data:image` URL, a `file:` ref, or an `https` URL on an allowed host. |
363
+ | `IMAGE_HOST_NOT_ALLOWED` | An `image`, `gallery` tile, or `list` item image URL is not a `data:image` URL, a `file:` ref, or an `https` URL on an allowed host. A gallery error names the tile (`blocks[0].images[2].src`); a list error names the item (`blocks[0].items[1].image.src`). |
144
364
  | `INVALID_ENUM` | A value is not one of the allowed values. |
145
365
  | `INVALID_FORMAT` | A string does not match its required format. |
146
- | `INVALID_TYPE` | A value has the wrong type, including a numeric field below its minimum. |
366
+ | `INVALID_TYPE` | A value has the wrong type, including a ref `limit` below 1. |
147
367
  | `KEY_ORDER` | Top-level keys are not in the order `v`, `datasets`, `blocks`. |
148
- | `MISSING_REQUIRED` | A required field is missing. |
368
+ | `MISSING_REQUIRED` | A required field is missing, or an `open` or `copy` action sets neither or both of its two fields. |
149
369
  | `NOT_A_DOCUMENT` | The reply is not one YAML or JSON mapping with a `v` field. |
370
+ | `OUT_OF_RANGE` | A number is outside its allowed range: a stepper `value` outside `min` and `max`, `min` not below `max`, `step` at or below 0, or `decimals` below 0 or above 3. |
150
371
  | `SELECT_TARGET_INVALID` | A select action names a block that is not a chart or table. |
151
372
  | `ROW_ARITY_MISMATCH` | A dataset row does not have one value per column. |
152
373
  | `TABLE_TOO_WIDE` | A table lists more than 12 columns. |
@@ -167,12 +388,23 @@ heuristics and does not fail `ok`.
167
388
  | --- | --- |
168
389
  | `parseBlocks(text)` | Fence strip, YAML or JSON parse |
169
390
  | `parseBlocksPartial(text)` | Completed top-level blocks while a reply is still streaming |
170
- | `validateBlocks(doc, options?)` | Structure, then dataset, chart, table, action, and html lint. `options.knownDatasets` checks `ref` columns. `options.hostActions` checks callback names. `options.allowHtml` allows `html` blocks. |
391
+ | `validateBlocks(doc, options?)` | Structure, then dataset, chart, table, action, stepper, checklist, image, gallery, and list image src, and html lint. `options.knownDatasets` checks `ref` columns. `options.hostActions` checks callback names. `options.stepperActions` and `options.checklistActions`, when set, check stepper and checklist `callback.name` instead (`UNKNOWN_HOST_ACTION`). `options.allowHtml` allows `html` blocks. |
171
392
  | `wrapAsTextDocument(text)` | Display fallback for a non-document |
172
393
  | `blocksSchema` | Zod schema |
173
394
  | `blocksJsonSchema` | JSON Schema for the same structure |
174
- | `blocksPromptSection({hostActions, allowHtml})` | System-prompt section. Limits come from `BLOCK_LIMITS`. `allowHtml` adds the `html` block. |
395
+ | `blocksPromptSection({hostActions, allowHtml, imageHosts, richBlocks, stepperActions, checklistActions})` | System-prompt section. Limits come from `BLOCK_LIMITS`. `allowHtml` adds the `html` block. `richBlocks` (default `true`) offers the rich blocks; `false` returns the prompt as it was before them, whatever the other lists hold. With `richBlocks` on and a non-empty `stepperActions`, the prompt adds `stepper`, its rules and limits, and names those actions as its callbacks; without one it never mentions `stepper`. With `richBlocks` on, the prompt adds `checklist`, `gallery`, and `list` with their limits, plus the `card` `eyebrow` and its limit; gallery tile and list item image srcs follow the same image src line as `image`, so `https` is offered only with `imageHosts`. The word `list` appears in the prompt only as this block's name. A non-empty `checklistActions` is named as the callback to set; without one the prompt says to leave `callback` out, so ticks stay local. With `richBlocks` on, one line offers the `copy` action with its text limit and its target block types, naming `stepper` only when the stepper is offered. |
175
396
  | `BLOCK_LIMITS` | The numbers in the table above |
176
397
  | `BLOCK_ERROR_CODES` | The codes in the table above |
177
398
  | `BLOCK_WARNING_CODES` | `BAR_TOO_MANY_CATEGORIES`, `DONUT_TOO_MANY_SLICES`, `LINE_SINGLE_POINT` |
399
+ | `stepperElementIds(id)` | `{decrease, increase}`: the element ids of a stepper's buttons |
400
+ | `checklistElementId(id, itemId)` | `<id>_<item id>`: the element id of a checklist item's tick. `ChecklistBlock`, `ChecklistItem`, and `ChecklistCallback` are type exports. |
401
+ | `scaleStepperBlock(original, value)` | The stepper at `value` with each item's `amount` scaled linearly from `original` (`amount × value / original value`), rounded to its `decimals` (`round: up` rounds up). An `original` with `value: 0` keeps its amounts. Always pass the block the agent wrote, so rounding does not drift. `scaleStepperHostAction` and the demo playground use it |
402
+ | `isStepperValueAllowed(stepper, value)` | `true` when `value` is finite, within `min` and `max`, and a whole number of `step`s from the stepper's `value` |
403
+ | `applyChecklistState(checklist, {itemId, checked, state})` | The checklist with each item's `checked` from `state`; an item missing from `state` is unchecked and `itemId` takes `checked`. `toggleChecklistHostAction` and the demo playground use it. `ChecklistTick` is a type export |
404
+ | `unknownChecklistItemIds(checklist, tick)` | The ids in a tick's `itemId` and `state` that the checklist has no item for, each once |
405
+ | `blockPlainText(block, {datasets?, checked?})` | The text a copy action copies for its target block (see [Copy](#copy)). `BlockPlainTextOptions` is a type export |
406
+ | `COPY_TARGET_TYPES` | `stepper`, `checklist`, `list`, `table`, `text`: the blocks a copy `target` may name. `CopyAction` is a type export |
407
+ | `GalleryBlock`, `GalleryImage` | Type exports for the `gallery` block and its tiles |
408
+ | `ListBlock`, `ListItem`, `ListItemImage` | Type exports for the `list` block, its items, and their thumbnails |
409
+ | `STEPPER_ROUNDING` | `nearest`, `up`. `StepperBlock`, `StepperItem`, and `StepperCallback` are type exports. |
178
410
  | `HTML_HEIGHTS`, `CALLOUT_STATUSES` | Allowed `html` heights and `callout` statuses. `HtmlBlock`, `CalloutBlock`, `ImageBlock`, and `DetailsBlock` are type exports. |
@@ -136,9 +136,12 @@ Resolution order for API base URL (`rtk/src/constants.ts`):
136
136
  | Variable | Read by | Required | Default | Secret | Scope |
137
137
  |----------|---------|----------|---------|--------|-------|
138
138
  | `GCS_BUCKET` | example-backend, admin | ❌ | — | No | server |
139
+ | `GCS_FOLDER_PREFIX` | example-backend `DocumentStorageApp` (`folderPrefix`) | ❌ | — | No | server |
139
140
  | `GCS_PROJECT_ID` | GCS clients | ❌ | — | No | server |
140
141
  | `GCS_SERVICE_ACCOUNT_KEY` | GCS clients | ❌ | — | Yes | server |
141
142
 
143
+ Example-backend Cloud Run deploys set `GCS_BUCKET` (default `flourish-terreno-example-documents`). Preview deploys also set `GCS_FOLDER_PREFIX=pr-<number>/`.
144
+
142
145
  ## Feature flags
143
146
 
144
147
  | Variable | Read by | Required | Default | Secret | Scope |
@@ -61,6 +61,55 @@ const customStyle: StyleProp<ViewStyle> = {
61
61
  - Ensures type compatibility when passing styles to @terreno/ui components
62
62
  - Simplifies imports (one package instead of two)
63
63
 
64
+ ## Theme modes
65
+
66
+ `TerrenoProvider` defaults to the Flourish Health light mode exported from
67
+ Figma. Select the matching Figma dark mode explicitly or follow the operating
68
+ system:
69
+
70
+ ``````tsx
71
+ import {TerrenoProvider} from "@terreno/ui";
72
+
73
+ <TerrenoProvider colorScheme="system">
74
+ <App />
75
+ </TerrenoProvider>
76
+ ``````
77
+
78
+ `colorScheme` accepts `"light"`, `"dark"`, or `"system"` and defaults to
79
+ `"light"` for deterministic server rendering. `"system"` follows the operating
80
+ system. Change the choice after startup with `setColorScheme`:
81
+
82
+ ``````tsx
83
+ import {SelectField, themeColorSchemeOptions, useTheme, type ThemeColorScheme} from "@terreno/ui";
84
+
85
+ const AppearanceSetting = () => {
86
+ const {colorSchemeSetting, setColorScheme} = useTheme();
87
+ return (
88
+ <SelectField
89
+ onChange={(value) => {
90
+ setColorScheme(value as ThemeColorScheme);
91
+ }}
92
+ options={themeColorSchemeOptions}
93
+ title="Appearance"
94
+ value={colorSchemeSetting}
95
+ />
96
+ );
97
+ };
98
+ ``````
99
+
100
+ Dark `surface.warningLight` uses `warning200`. The lighter warning orange, `warning100`, is 2.52:1 against warning text and misses WCAG AA. Highlighted `DataTable` cells switch to `text.inverted` when `text.primary` is below 4.5:1 on that fill. Inverted `MarkdownView` code uses `surface.neutralDark` when `surface.neutralLight` is below 4.5:1 against that text, and blockquotes do the same with `surface.baseAlternate`. Primary `Button` labels use `text.secondaryDark` when that pair meets 4.5:1 on `surface.primary`, and `text.inverted` otherwise, so `setTheme(darkThemeConfig)` stays readable without changing `colorScheme`. The demo palette generator emits status `050` steps so custom palettes update those dark error, success, and warning surfaces.
101
+
102
+ `themeColorSchemeOptions` labels those values Light, Dark, and Follow system.
103
+ `useTheme()` also returns the resolved `colorScheme` (`"light"` or `"dark"`)
104
+ with `theme`, `setTheme`, `setPrimitives`, and `resetTheme`. An explicit
105
+ `colorScheme` prop replaces the current choice whenever the parent changes it.
106
+
107
+ The public `lightThemeConfig`, `darkThemeConfig`, and
108
+ `defaultThemePrimitives` exports are the canonical token maps. They include the
109
+ Figma AI surface/border roles and the dark status ramps. Custom overrides set
110
+ through `useTheme()` are layered over the active mode; `resetTheme()` removes
111
+ those overrides without changing the provider's selected mode.
112
+
64
113
  ### DashboardGrid and DashboardGridItem
65
114
 
66
115
  Eager layout-only wrapping grid. Default columns `{sm: 1, md: 2, lg: 3}`. Children stay caller-supplied `Card`s. Cell width is `(rowWidth - gap × (columns - 1)) / columns` so flex `gap` does not wrap extra columns.
@@ -473,10 +522,17 @@ rows, answers, and errors. Steps: [Add agent asks to a chat](../how-to/agent-ui-
473
522
  `BlocksView` paints a whole-reply document from `@terreno/blocks`. Pass the assistant text
474
523
  as `document`. Leaf blocks in this slice are `heading`, `text`, `metric`, `badge`,
475
524
  `divider`, and `context`. `columns` is a row from the `md` breakpoint and a stack on `sm`.
476
- `card` groups children under an optional title. A `callout` draws `Banner` and is not dismissible. An `image` draws `Image` and requires `alt`; pass `imageHosts` to allow `https` hosts. A `details` block draws `Accordion`. A `chart` draws `LineChart`, `BarChart`,
525
+ `card` groups children under an optional title, with an optional small muted `eyebrow` above it (test ID `<card path>-eyebrow`). A `callout` draws `Banner` and is not dismissible. An `image` draws `Image` and requires `alt`; pass `imageHosts` to allow `https` hosts. A `details` block draws `Accordion`. A `chart` draws `LineChart`, `BarChart`,
477
526
  `AreaChart`, or `DonutChart` from an inline dataset or from `points`. The series is only
478
527
  `{label, value}` — a point color in the document is not passed through. A `table` draws
479
- `DataTable`. A `ref` dataset stays empty until `resolveDataset` returns its rows, and the
528
+ `DataTable` and maps each dataset column type to a cell: `number` to a right-aligned
529
+ `number` cell, `date` to a `date` cell (Luxon `DATE_MED`, such as "Mar 14, 2026"), and
530
+ `string` to `text`. A listed column that the dataset does not have yet is `text`. Each header
531
+ is the column name with underscores shown as spaces (`bone_in_lamb` reads "bone in lamb"). The table
532
+ measures its container with `onLayout` and splits the width evenly across its columns, with
533
+ at least 96 per column; past that it scrolls sideways. Before the first layout each column
534
+ is 120 wide. Its height fits the header plus its rows (54 each), up to 10 rows; a longer table
535
+ scrolls inside that height, so it never spills over the next block. A `ref` dataset stays empty until `resolveDataset` returns its rows, and the
480
536
  chart shows `loading` while that promise is in flight. A rejected fetch clears that
481
537
  loading state. A segmented control highlights the option whose `data` matches the target
482
538
  chart or table. A `select` action switches a table the same way it switches a chart.
@@ -494,6 +550,83 @@ action also stores the chosen dataset on the target chart for this view. Pass
494
550
  `hostActions` and every callback stays enabled. `pendingElementIds` shows those
495
551
  buttons as loading. `overrides` replaces a block by id.
496
552
 
553
+ A `stepper` draws its `label` as a small muted line, then `IconButton` − and + around the
554
+ value and its `unit`, then `itemsTitle`, a two-column grid of item labels and amounts
555
+ (formatted with `decimals`, so `{amount: 2, unit: kg, decimals: 1}` reads "2.0 kg"), and the
556
+ muted `note`. − is disabled when `value - step` would fall below `min`, and + when `value + step`
557
+ would pass `max`, so with `max: 20` and `step: 2` + is disabled at 19. Both are disabled while either button's
558
+ element id is in `pendingElementIds`, and when `hostActions` is passed without the stepper's
559
+ `callback.name`. The buttons are labelled "Decrease <label>" and "Increase <label>". A tap
560
+ calls `onAction` with `blockId: <id>`, `elementId: <id>_decrease` or `<id>_increase`, and
561
+ `{kind: "callback", name, payload: {...payload, value: value ± step}}`. A host returns the
562
+ new stepper, and `overrides[<id>]` paints it.
563
+
564
+ A `checklist` draws its `title` on the left and an "n of m" counter on the right, then one
565
+ `CheckBox` row per item: the bold `text` first, then the small muted `meta` (such as a time),
566
+ then the muted `detail`. Each row is a pressable `Box` with the `checkbox` role, labelled with the item
567
+ text, that reports `accessibilityState` `{checked, disabled}` (`aria-checked` and
568
+ `aria-disabled` on web); Space toggles a focused row on web. A locked row keeps its role and
569
+ label and reports `disabled: true`. Test ids are stable across states: the item wrapper is
570
+ `<path>-<id>_<item id>`, the pressable row `<path>-<id>_<item id>-row-clickable`, its
571
+ `CheckBox` `<path>-<id>_<item id>-checkbox`, its lines `<path>-<id>_<item id>-text`, `-meta`,
572
+ and `-detail`, and the counter `<path>-counter`. A tick goes to the host when the checklist has a `callback`,
573
+ `onAction` is passed, and `hostActions` is omitted (as for the stepper) or lists
574
+ `callback.name`. Then a tap calls `onAction` with `blockId: <id>`,
575
+ `elementId: <id>_<item id>`, and `{kind: "callback", name, payload: {...payload, itemId,
576
+ checked, state}}`, where `state` maps every item id to its tick after this one. The tick is
577
+ not drawn ahead of the reply. `BlocksView` disables the checklist while a tick is pending:
578
+ while any of its `<id>_<item id>` element ids is in `pendingElementIds`, no row is pressable,
579
+ so a second tick cannot send a `state` that misses the first. The counter changes when
580
+ `overrides[<id>]` brings the returned checklist. Otherwise a tick stays in this `BlocksView` only and updates the counter at once.
581
+ When an override for the checklist arrives or changes, earlier local ticks on it are dropped
582
+ and the override's `checked` values show.
583
+ On the server, `toggleChecklistHostAction` from `@terreno/ai` handles the callback and returns
584
+ the stored checklist with the ticks from `state` (see [Host actions](ai.md#host-actions)). A host
585
+ that records progress registers its own `handles: "checklist"` action.
586
+
587
+ A `gallery` draws each image as a 4:3 `Image` tile (`fit` cover), with `alt` as the tile's
588
+ accessible label and the optional `caption` as small muted text under it. Tiles are 8 apart
589
+ (the `gap={2}` spacing step). The gallery measures its container with `onLayout`: up to three
590
+ images share one row at equal widths, and more wrap into a three-column grid whose last row
591
+ keeps the same tile width. When that width would fall under 160, every tile stays 160 wide in
592
+ one row that scrolls sideways. A measured width decides this rather than the `sm` breakpoint, so
593
+ a gallery inside a narrow `columns` child or card also scrolls. Before the first layout each tile
594
+ is 160 wide. A `file:` tile loads through `resolveImage` like an `image` block. Until it has a
595
+ URL, or when the lookup fails, the tile is a muted 4:3 placeholder that shows the alt text and
596
+ keeps it as the label. Test ids: the gallery `<path>`, each grid row `<path>-row-<n>`, the
597
+ scrolling row `<path>-scroll`, each tile `<path>-image-<index>`, the frame around a loaded
598
+ image `<path>-image-<index>-image`, its caption `<path>-image-<index>-caption`, and a
599
+ placeholder `<path>-image-<index>-placeholder`.
600
+ `Image` passes `alt` to the native image as its `accessibilityLabel`.
601
+
602
+ A `list` draws one row per item, 12 apart (`gap={3}`). The row starts with a 3:4 portrait
603
+ `Image` thumbnail (`fit` cover), always 112 wide and 149 tall, with `alt` as its accessible label.
604
+ Beside it, the optional `meta` sits small and muted above the bold `title`, and the optional
605
+ `text` follows in muted body text. `text` is plain text, not markdown. When any item in the list
606
+ has a thumbnail, an item without one keeps an empty 112-wide gutter so every title starts at the
607
+ same place; a list with no thumbnails draws text only, with no gutter. A `file:` thumbnail loads
608
+ through `resolveImage`, including in a list inside a card. Until it has a URL, or when the lookup
609
+ fails, it is a muted 112 by 149 placeholder that shows the alt text and keeps it as the label.
610
+ Test ids: the list `<path>`, each row `<path>-item-<index>`, its thumbnail
611
+ `<path>-item-<index>-image`, placeholder `<path>-item-<index>-placeholder`, gutter
612
+ `<path>-item-<index>-gutter`, text column `<path>-item-<index>-body`, and the
613
+ `<path>-item-<index>-meta`, `-title`, and `-text` lines.
614
+
615
+ A `copy` button writes the clipboard on the device and never calls `onAction`, so
616
+ `hostActions` does not disable it. A press builds the text with `blockPlainText` from
617
+ `@terreno/blocks`: a literal `text` as written, or the `target` block as it is shown now. That
618
+ means the `overrides` replacement (a stepper at 6 copies the scaled amounts), the checklist's
619
+ ticks made on the device, and a table's selected dataset, inline or a `ref` that
620
+ `resolveDataset` has returned. `BlocksView` writes it with `expo-clipboard` `setStringAsync`
621
+ (web and native), then shows "Copied" in small muted text beside the button for 2 seconds. A
622
+ failed write, or a target with no text yet (such as a `ref` table still loading), shows
623
+ "Couldn't copy" in error text for 2 seconds instead, logs `console.warn`, and does not throw.
624
+ The status sits in a `Box` with `accessibilityLiveRegion="polite"`, which stays mounted while
625
+ empty, so screen readers on web and Android announce the status when it appears (`Text` has
626
+ no live-region prop, and VoiceOver on iOS does not announce React Native live regions).
627
+ Test ids: the button `<path>-<element id>`, the live region `<path>-<element id>-status`, and
628
+ the status text `<path>-<element id>-status-text`.
629
+
497
630
  ```tsx
498
631
  <BlocksView
499
632
  document={reply}
@@ -505,6 +638,23 @@ buttons as loading. `overrides` replaces a block by id.
505
638
 
506
639
  Demo story: `BlocksView`.
507
640
 
641
+ ### MarkdownView
642
+
643
+ `MarkdownView` renders markdown with `react-native-markdown-display` and the current theme.
644
+ GPTChat and the `text` block render through it.
645
+
646
+ GFM tables use theme colors, never fixed hex values:
647
+
648
+ - Cell borders draw a grid in `theme.border.default`.
649
+ - Header cells sit on `theme.surface.secondaryLight` and use the bold text font (`text-bold`).
650
+ Body cells use `text-regular`.
651
+ - Every column gets an equal share of the width, at least 96. A narrow table fills its
652
+ container. A table wider than its container scrolls sideways in a horizontal `ScrollView`.
653
+
654
+ Test ids: `markdown-table-scroll` (the scroll view), `markdown-table`,
655
+ `markdown-table-header-<column>`, and `markdown-table-cell-<row>-<column>` (body rows count
656
+ from 0).
657
+
508
658
  ### HtmlFrame
509
659
 
510
660
  `HtmlFrame` paints one `html` block. On web it is an `<iframe sandbox="" referrerpolicy="no-referrer">` whose `srcdoc` starts with a Content-Security-Policy meta tag (`default-src 'none'`). On native it is a WebView with JavaScript off, and navigation after the first load is rejected. `BlocksView` renders that frame only when `allowHtml` is true and `streaming` is false. Otherwise the block is a card that says the preview is off or still arriving. `GPTChat` passes `allowHtml` through.
@@ -738,7 +888,8 @@ top spacing, the navigation row, bottom spacing, and `bottomNavBarHeight`.
738
888
  opts into a labeled full-width pager on the narrow viewport. `narrowBelowWidth` uses that
739
889
  viewport when the window is at or below the given pixel width; when omitted, the narrow
740
890
  viewport follows `isNarrowViewport()`. These props are web only; the native `SplitPage`
741
- ignores them. See `SplitPageProps` for when each prop applies and what is ignored.
891
+ ignores them. See `SplitPageProps` for when each prop applies and what is ignored. An unset
892
+ `color` is `baseAlternate` on web and native.
742
893
  `IconButton`'s `backgroundOpacity` tints only that button's background.
743
894
 
744
895
  ```typescript
@@ -772,12 +923,25 @@ Buttons use a scale animation by default. Set `pressAnimation="opacity"` for an
772
923
 
773
924
  Disabled and loading buttons use a non-interactive pressable regardless of the selected animation.
774
925
 
926
+ ### Tooltip
927
+
928
+ `Tooltip` (and `tooltipText` on `Button` / `IconButton`) renders its bubble outside the trigger's clipping and stacking context:
929
+
930
+ | Platform | Mount point | Positioning |
931
+ | --- | --- | --- |
932
+ | Web | `document.body` | `position: fixed`, `z-index: 9999`, so it shows above an open `Modal` and nothing else covers the page |
933
+ | iOS / Android | `TerrenoProvider` portal host | `position: absolute`, `z-index: 999` |
934
+
935
+ The bubble stays off screen at `opacity: 0` until the trigger is measured, then `getTooltipPosition` places it at `idealPosition` or the first side that fits on screen.
936
+
775
937
  ### Toast
776
938
 
777
939
  `TerrenoProvider` mounts the toast container. Call `useToast()` for `success`, `info`, `warn`, `error`, `show`, `hide`, and `catch`.
778
940
 
779
941
  On web, that container is portaled to `document.body` (`position: fixed`, `z-index: 999999`, `pointerEvents: "box-none"`), so a toast stays above an open `Modal` and its backdrop. The web container is sized with `100%` rather than the measured window, so statically exported pages (where `Dimensions` reports 0×0 at build time) still center the toast on screen. Native iOS and Android keep the in-tree absolute container sized to the window.
780
942
 
943
+ Only the visible toast takes clicks. Every full-width layer around it (the container, its safe area, and the `Toast` layout wrapper) uses `pointerEvents: "box-none"`, so a button at the same height as an open toast stays clickable. Declare that style through `StyleSheet.create`; react-native-web drops `pointerEvents` from inline styles.
944
+
781
945
  ## Authentication Components
782
946
 
783
947
  ### SocialLoginButton
@@ -1114,6 +1278,32 @@ Responsive `Box` direction props update automatically when the window resizes or
1114
1278
  All responsive Boxes share one dimension listener; non-responsive Boxes do not subscribe.
1115
1279
  When multiple direction props match, the largest active breakpoint wins (`xl` over `lg` over `md` over `sm`).
1116
1280
 
1281
+ `Box` takes an optional `accessibilityState` of `{checked?, disabled?}`. It is sent to screen
1282
+ readers as `accessibilityState` and as `aria-checked` / `aria-disabled`, on both a plain and a
1283
+ clickable Box. On a clickable Box, `disabled: true` also stops presses while the role and
1284
+ label stay. A clickable Box with `accessibilityRole` `checkbox` or `switch` also toggles on
1285
+ Space on web, as ARIA expects.
1286
+
1287
+ ```tsx
1288
+ <Box
1289
+ accessibilityHint="Marks this item as done"
1290
+ accessibilityLabel="Preheat the oven"
1291
+ accessibilityRole="checkbox"
1292
+ accessibilityState={{checked: false, disabled: isSaving}}
1293
+ onClick={toggle}
1294
+ />
1295
+ ```
1296
+
1297
+ `Box` also takes `accessibilityLiveRegion` (`polite` or `assertive`) on a Box without
1298
+ `onClick`. It is sent as `aria-live`, so screen readers announce changes to the Box's contents
1299
+ on web and Android. VoiceOver on iOS does not announce React Native live regions.
1300
+
1301
+ ```tsx
1302
+ <Box accessibilityLiveRegion="polite" testID="copy-status">
1303
+ {status ? <Text size="sm">{status}</Text> : null}
1304
+ </Box>
1305
+ ```
1306
+
1117
1307
  ## Icons
1118
1308
 
1119
1309
  Terreno uses **FontAwesome 6** by default. Pass icon names via `iconName` on `Icon`, `Button`, `IconButton`, form fields, `Badge`, and other icon-aware components.
@@ -1233,8 +1423,8 @@ const buttonStyles = toggle(isPressed, pressedStyles, defaultStyles);
1233
1423
 
1234
1424
  @terreno/ui components do not require environment variables. All configuration is done at runtime via:
1235
1425
 
1236
- - **TerrenoProvider props** — Theme customization, custom icon registry (`icons`), OpenAPI spec URL
1237
- - **Theme hooks** — `useTheme()`, `setTheme()`, `setPrimitives()`
1426
+ - **TerrenoProvider props** — `colorScheme`, `initialPrimitives`, custom icons, and OpenAPI spec URL
1427
+ - **Theme hooks** — `useTheme()`, `setColorScheme()`, `setTheme()`, `setPrimitives()`
1238
1428
  - **Component props** — Direct prop overrides for individual components
1239
1429
 
1240
1430
  **Example configuration:**
@@ -1243,16 +1433,28 @@ const buttonStyles = toggle(isPressed, pressedStyles, defaultStyles);
1243
1433
  import {TerrenoProvider} from "@terreno/ui";
1244
1434
 
1245
1435
  <TerrenoProvider
1246
- baseUrl="https://api.example.com"
1247
- theme={{
1248
- surface: {primary: "secondary500"},
1249
- }}
1250
- onError={(error) => console.error(error)}
1436
+ colorScheme="system"
1437
+ openAPISpecUrl="https://api.example.com/openapi.json"
1251
1438
  >
1252
1439
  {children}
1253
1440
  </TerrenoProvider>
1254
1441
  ``````
1255
1442
 
1443
+ ## DataTable column types
1444
+
1445
+ `columnType` picks the cell a column draws, unless `customColumnComponentMap` has an entry
1446
+ for that type:
1447
+
1448
+ | `columnType` | Cell |
1449
+ | --- | --- |
1450
+ | `text` (and any unmapped type) | The value as a string |
1451
+ | `number` | The value as a string, right-aligned, with the header right-aligned to match. A number or a numeric string both work |
1452
+ | `date` | An ISO string or a `Date`, formatted with Luxon `DATE_MED` ("Mar 14, 2026") in the local zone. A value Luxon cannot parse is shown as written, and an empty value is blank |
1453
+ | `boolean` | A check or a cross icon |
1454
+
1455
+ A value that is already formatted, such as "Oct 8, 2026, 3:00 PM", is not ISO, so a `date`
1456
+ column shows it unchanged.
1457
+
1256
1458
  ## DataTable server-side filtering
1257
1459
 
1258
1460
  `DataTable` is data-layer agnostic. Optional filter and search props emit a