@box-kite/react 2.0.1 → 2.1.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.
- package/.claude/rules/box-kite-rules.md +16 -0
- package/.claude/skills/box-kite/SKILL.md +16 -0
- package/.claude/skills/box-kite/references/components.md +8 -3
- package/.claude/skills/box-kite/references/patterns.md +23 -0
- package/.cursor/rules/box-kite.mdc +16 -0
- package/AGENTS.md +19 -0
- package/BOX_AI_CONTEXT.md +562 -1
- package/BOX_KITE_AI_CONTEXT.md +562 -1
- package/README.md +39 -7
- package/catalog.cjs +1 -0
- package/catalog.d.ts +8 -0
- package/catalog.mjs +2333 -0
- package/client.cjs +1 -1
- package/client.mjs +40 -42
- package/components/agent.cjs +1 -0
- package/components/agent.d.ts +215 -0
- package/components/agent.mjs +405 -0
- package/components/chart.d.ts +1 -0
- package/components/dashboard.cjs +1 -0
- package/components/dashboard.d.ts +121 -0
- package/components/dashboard.mjs +690 -0
- package/components/dataGrid/components/dataGridBody.d.ts +2 -1
- package/components/dataGrid/components/dataGridDetailRow.d.ts +13 -2
- package/components/dataGrid/components/dataGridGroupRow.d.ts +13 -2
- package/components/dataGrid/components/dataGridRow.d.ts +13 -2
- package/components/dataGrid/contracts/dataGridContract.d.ts +66 -2
- package/components/dataGrid/models/cellModel.d.ts +3 -0
- package/components/dataGrid/models/editModel.d.ts +27 -0
- package/components/dataGrid/models/exportModel.d.ts +2 -1
- package/components/dataGrid/models/gridModel.d.ts +3 -0
- package/components/dataGrid/models/rangeModel.d.ts +119 -0
- package/components/dataGrid/models/viewportModel.d.ts +23 -2
- package/components/dataGrid/useGridNavigation.d.ts +10 -2
- package/components/dataGrid.cjs +4 -1
- package/components/dataGrid.d.ts +3 -0
- package/components/dataGrid.mjs +1056 -765
- package/components/markdown.cjs +1 -0
- package/components/markdown.d.ts +55 -0
- package/components/markdown.mjs +179 -0
- package/components/semantics.d.ts +29 -29
- package/components/skeleton.cjs +1 -0
- package/components/skeleton.d.ts +34 -0
- package/components/skeleton.mjs +25 -0
- package/dashboardSchema.cjs +1 -0
- package/dashboardSchema.mjs +78 -0
- package/docs/a11y.md +2 -3
- package/docs/catalog.md +451 -0
- package/docs/components.md +4 -0
- package/docs/index.md +3 -0
- package/docs/interop.md +237 -0
- package/docs/mcp.md +69 -0
- package/docs/props.md +3 -3
- package/interop.cjs +1 -0
- package/interop.d.ts +51 -0
- package/interop.mjs +454 -0
- package/mcp/src/data.d.ts +88 -0
- package/mcp/src/data.test.d.ts +1 -0
- package/mcp/src/engine.d.ts +23 -0
- package/mcp/src/index.d.ts +1 -0
- package/mcp/src/search.d.ts +26 -0
- package/mcp/src/search.test.d.ts +1 -0
- package/mcp/src/server.d.ts +18 -0
- package/mcp/src/server.test.d.ts +1 -0
- package/mcp/src/styles.d.ts +27 -0
- package/mcp/src/styles.test.d.ts +1 -0
- package/mcp/src/tools.d.ts +18 -0
- package/mcp/src/vocabulary.d.ts +41 -0
- package/package.json +124 -17
- package/react/spec/renderSpec.d.ts +51 -0
- package/react/spec/specBoundary.d.ts +25 -0
- package/react/spec/specRegistry.d.ts +42 -0
- package/react/spec/specRenderer.d.ts +10 -0
- package/registry/blocks/dashboard-shell/dashboard-shell.d.ts +22 -0
- package/registry/blocks/dashboard-shell/dashboard-stats.d.ts +13 -0
- package/registry/blocks/data-grid/invoice-rows.d.ts +15 -0
- package/registry/blocks/data-grid/invoices-grid.d.ts +1 -0
- package/registry/blocks/settings-form/settings-form.d.ts +17 -0
- package/spec.cjs +2 -0
- package/spec.d.ts +12 -0
- package/spec.mjs +382 -0
- package/specData.cjs +1 -0
- package/specData.mjs +69 -0
- package/specDefs.cjs +1 -0
- package/specDefs.mjs +33 -0
- package/specSchema.cjs +1 -0
- package/specSchema.mjs +88 -0
- package/utils/agent/agentUtils.d.ts +81 -0
- package/utils/catalog/catalogContracts.d.ts +19 -0
- package/utils/catalog/catalogManifest.d.ts +8 -0
- package/utils/dashboard/dashboardSchema.d.ts +18 -0
- package/utils/dashboard/dashboardUtils.d.ts +97 -0
- package/utils/interop/a2uiInterop.d.ts +82 -0
- package/utils/interop/assistantUiInterop.d.ts +34 -0
- package/utils/interop/toolInterop.d.ts +30 -0
- package/utils/spec/specData.d.ts +19 -0
- package/utils/spec/specDefs.d.ts +11 -0
- package/utils/spec/specProps.d.ts +15 -0
- package/utils/spec/specSchema.d.ts +23 -0
- package/utils/spec/specTypes.d.ts +71 -0
- package/utils/spec/specValidate.d.ts +10 -0
package/BOX_KITE_AI_CONTEXT.md
CHANGED
|
@@ -868,6 +868,7 @@ setTheme(null); // reset to system auto-detection (clears localStorage)
|
|
|
868
868
|
|
|
869
869
|
**Props**: `theme?` (string — explicit theme name), `use?` (`'global'`|`'local'`, default `'local'`), `storageKey?` (string — localStorage key for persistence), `globalStyles?` (BoxStyleProps — app-wide styles on `<html>`, only with `use="global"`).
|
|
870
870
|
**DOM**: Sets `data-theme` attribute and theme class on wrapper (local) or `document.documentElement` (global). Cleaned up on unmount.
|
|
871
|
+
**Nesting**: a theme reaches the subtree it owns and stops at the next element declaring one, so `<Box.Theme use="local" theme="light">` inside a dark page really is light. Each theme rule is `@scope (.light) to ([data-theme]) { :scope .className … }`, which is why `data-theme` marks a theme root — write it beside any theme class you set by hand (the `<html>` one in a prerendered shell). Needs Chrome 118+, Safari 17.4+, Firefox 128+.
|
|
871
872
|
|
|
872
873
|
#### globalStyles — App-wide styles on `<html>`
|
|
873
874
|
|
|
@@ -2208,7 +2209,9 @@ the element wrapping the bars, the rows and the pager. Every callback below is a
|
|
|
2208
2209
|
| `expandedRowKeys` / `onExpandedRowKeysChange` | `Key[]` / `(keys, { reason })` | Controlled expanded rows; reason: `expand`/`collapse` |
|
|
2209
2210
|
| `globalFilterValue` / `onGlobalFilterChange` | `string` / `(value, { reason })` | Controlled global filter; reason: `filter`/`clear` |
|
|
2210
2211
|
| `columnFilters` / `onColumnFiltersChange` | `ColumnFilters` / `(filters, { reason })` | Controlled column filters; reason: `filter`/`clear` |
|
|
2211
|
-
| `onCellEditsChange` | `(edits, { reason })` | Every accepted edit, oldest first; reason: `edit`/`clear`
|
|
2212
|
+
| `onCellEditsChange` | `(edits, { reason })` | Every accepted edit, oldest first; reason: `edit`/`paste`/`clear` |
|
|
2213
|
+
| `onRangeChange` | `(range, { reason })` | The block of cells marked, or `undefined`; reason: `select`/`extend`/`clear` |
|
|
2214
|
+
| `onPaste` | `(paste)` | One Ctrl+V: `{ range, applied, rejected, skipped }`, once for the whole block |
|
|
2212
2215
|
|
|
2213
2216
|
`onSelectionChange(event)`, `onSortChange(key, dir)`, `onPageChange(page, size)` and
|
|
2214
2217
|
`onPageSizeChange(size)` are the pre-contract spellings of the four above. They still fire; the
|
|
@@ -2242,6 +2245,8 @@ named ones carry the reason.
|
|
|
2242
2245
|
| `noDataComponent` | `ReactNode` | `'empty'` | Custom empty state |
|
|
2243
2246
|
| `footer` | `boolean \| { label? }` | `false` | A pinned row of grand totals over every column with an `aggregate`. `label` replaces the default `Total` |
|
|
2244
2247
|
| `export` | `boolean \| ExportConfig` | `false` | Export buttons in the top bar: `{ csv?, xlsx?, fileName? }`. The writers load on the first press |
|
|
2248
|
+
| `tabNavigation` | `'none' \| 'cells'` | `'none'` | What Tab does. `'none'` is APG's single tab stop; `'cells'` is the spreadsheet reading |
|
|
2249
|
+
| `rangeSelection` | `boolean` | `false` | A drag or Shift+arrow marks a block of cells, and `Ctrl+C`/`Ctrl+V` copy and paste it as TSV (see below) |
|
|
2245
2250
|
|
|
2246
2251
|
### ColumnType
|
|
2247
2252
|
|
|
@@ -2471,6 +2476,95 @@ the same function — and an answer to an edit the user has since abandoned is d
|
|
|
2471
2476
|
value was refused — and `datagrid.body.cell.editor` (`isPending`) and `datagrid.body.cell.error` are
|
|
2472
2477
|
the editor and its message.
|
|
2473
2478
|
|
|
2479
|
+
### Range selection and the clipboard
|
|
2480
|
+
|
|
2481
|
+
Every grid marks the cell its arrows carry on from — whether the pointer or the keyboard put it there —
|
|
2482
|
+
and the mark **survives the grid losing focus**. `Ctrl+C` on it copies that cell, with no opt-in.
|
|
2483
|
+
`def.rangeSelection` is the rest: a drag, or Shift with the arrow keys, marks a rectangle, and `Ctrl+C`
|
|
2484
|
+
writes it as the tab-separated text a spreadsheet pastes as columns.
|
|
2485
|
+
|
|
2486
|
+
```tsx
|
|
2487
|
+
<DataGrid
|
|
2488
|
+
data={people}
|
|
2489
|
+
def={{
|
|
2490
|
+
rowKey: 'id',
|
|
2491
|
+
rangeSelection: true,
|
|
2492
|
+
columns: [
|
|
2493
|
+
{ key: 'name', header: 'Name' },
|
|
2494
|
+
{ key: 'country', header: 'Country' },
|
|
2495
|
+
{ key: 'salary', header: 'Salary', align: 'end' },
|
|
2496
|
+
],
|
|
2497
|
+
}}
|
|
2498
|
+
onRangeChange={(range, { reason }) => setSum(sumOf(range?.values()))}
|
|
2499
|
+
/>
|
|
2500
|
+
```
|
|
2501
|
+
|
|
2502
|
+
- **The mark is state, not a focus ring.** `:focus-visible` never matches a pointer, so a _clicked_ cell
|
|
2503
|
+
drew nothing (bug #64), and any ring made of focus goes out with it — leaving a copy with nothing to act
|
|
2504
|
+
on. It is the corner a Shift+arrow grows the block from, and after a drag it is the cell the drag ended
|
|
2505
|
+
on, so the ring, the tab stop and the next Shift+arrow are one cell.
|
|
2506
|
+
- **Extending:** drag across cells, hold Shift with the arrow keys (Home/End, Ctrl+Home/End and
|
|
2507
|
+
PageUp/PageDown extend too), or Shift+press to extend from wherever the block was anchored. A move with
|
|
2508
|
+
no modifier collapses the block onto the cell it landed on.
|
|
2509
|
+
- **A block is selected; one cell is a cursor.** The cells of a block carry `aria-selected` and the grid is
|
|
2510
|
+
`aria-multiselectable`; a lone current cell carries neither, because it has chosen nothing. The block is
|
|
2511
|
+
tinted _around_ the current cell, which keeps its ring — the way a sheet says where typing would land.
|
|
2512
|
+
- **What is copied is what a file would carry.** The clipboard is an export: a column's `exportValue` wins
|
|
2513
|
+
where it has one, an accepted edit wins over the row, and a group row copies its own value in the column
|
|
2514
|
+
that spans it. Fields are joined with tabs and rows with CRLF; a field carrying a tab, a newline or a
|
|
2515
|
+
quote is quoted with its quotes doubled. The copy rides the browser's own `copy` event, so text you have
|
|
2516
|
+
selected on the page still belongs to the page.
|
|
2517
|
+
- **Marking cells is a drag, so text selection is not.** A grid with `rangeSelection` on gives text
|
|
2518
|
+
selection up; an open editor hands it back for the value being typed. A press that lands on a widget
|
|
2519
|
+
belongs to the widget, and a touch is left alone entirely — that press is how the grid scrolls.
|
|
2520
|
+
- **`onRangeChange(range, { reason })`** reports `{ startRow, endRow, columns, values() }` or `undefined`,
|
|
2521
|
+
with `select` / `extend` / `clear`. Rows are **indices into the rows on screen** rather than keys,
|
|
2522
|
+
because a block may reach rows a datasource has not fetched — and `values()` is a call for the same
|
|
2523
|
+
reason, gathering the rectangle when it is asked for.
|
|
2524
|
+
- Style nodes: `datagrid.body.cell` gains `isCurrentCell` (the ring) and `isInRange` (the tint).
|
|
2525
|
+
|
|
2526
|
+
### Paste: Ctrl+V, one judgement per cell
|
|
2527
|
+
|
|
2528
|
+
The block goes back the other way. `Ctrl+V` fills from the current cell — or from the block that is marked,
|
|
2529
|
+
where one is — and every cell it covers goes through the same `def.onCellEdit` an editor would, so a paste
|
|
2530
|
+
needs no second validator and no second way of writing a value. It needs no opt-in either: `rangeSelection`
|
|
2531
|
+
only gives it a bigger target.
|
|
2532
|
+
|
|
2533
|
+
```tsx
|
|
2534
|
+
<DataGrid
|
|
2535
|
+
data={people}
|
|
2536
|
+
def={{
|
|
2537
|
+
rowKey: 'id',
|
|
2538
|
+
rangeSelection: true,
|
|
2539
|
+
columns: [
|
|
2540
|
+
{ key: 'name', header: 'Name', editable: true },
|
|
2541
|
+
{ key: 'salary', header: 'Salary', align: 'end', editable: true },
|
|
2542
|
+
],
|
|
2543
|
+
onCellEdit: ({ columnKey, value }) => (columnKey === 'salary' && Number(value) < 0 ? 'Salary cannot be negative' : undefined),
|
|
2544
|
+
}}
|
|
2545
|
+
onPaste={({ applied, rejected, skipped }) => setReport({ applied, rejected, skipped })}
|
|
2546
|
+
/>
|
|
2547
|
+
```
|
|
2548
|
+
|
|
2549
|
+
- **Each axis takes whichever is longer, the block or the clipboard.** A block bigger than the clipboard is
|
|
2550
|
+
tiled with it, a clipboard bigger than the block spills past it, and both stop at the edge of the grid.
|
|
2551
|
+
One rule, with the degenerate case falling out of it: a paste onto the current cell alone starts from a
|
|
2552
|
+
block of one.
|
|
2553
|
+
- **A refusal skips its own cell and nothing else.** A paste is many independent judgements, and stopping at
|
|
2554
|
+
the first bad value would leave the block half written with no way back. The refused cells wear the red
|
|
2555
|
+
ring (`datagrid.body.cell`'s `isInvalid`, with no editor open behind it) and report `aria-invalid`, so the
|
|
2556
|
+
cells say which ones and `onPaste` says why.
|
|
2557
|
+
- **The clipboard carries no types, so the cell says how to read the text.** A number column parses it and
|
|
2558
|
+
refuses what is not a number, a checkbox takes `true`/`false` (and `1`/`0`), and a `select` keeps to its
|
|
2559
|
+
own options and holds the option's value rather than its spelling. A value that did not change is not an
|
|
2560
|
+
edit at all, so pasting a column back over itself costs nothing.
|
|
2561
|
+
- **A paste that reaches an open editor belongs to the editor** — that is a value being typed, not a block
|
|
2562
|
+
being filled — and a cell that cannot be edited at all is skipped before anything is asked about it, so a
|
|
2563
|
+
validator only ever hears about cells that could have taken a value.
|
|
2564
|
+
- **`onPaste({ range, applied, rejected, skipped })`** fires once for the whole block, after the values have
|
|
2565
|
+
landed, so `range.values()` reads what the grid now shows. Everything it accepted lands in the same
|
|
2566
|
+
stream a typed value does: `onCellEditsChange(edits, { reason: 'paste' })`.
|
|
2567
|
+
|
|
2474
2568
|
### ContextMenuConfig
|
|
2475
2569
|
|
|
2476
2570
|
| Prop | Type | Default | Description |
|
|
@@ -2802,3 +2896,470 @@ Also accepts: `data` (TRow[]), `value`/`defaultValue`, `label`/`labelProps`, `mu
|
|
|
2802
2896
|
3. **CSS variables**: In `:root` rules
|
|
2803
2897
|
4. **Theme issues**: Ensure `<Box.Theme>` wraps your app
|
|
2804
2898
|
5. **Layer theming**: a tooltip or dropdown popup stays where it was declared and inherits the theme; only the no-Popover-API fallback uses the `#box-kite-portal` container
|
|
2899
|
+
|
|
2900
|
+
---
|
|
2901
|
+
|
|
2902
|
+
---
|
|
2903
|
+
|
|
2904
|
+
## Dashboard (`components/dashboard`)
|
|
2905
|
+
|
|
2906
|
+
A grid of widgets people rearrange, and a layout a model can write. Two components and one artifact:
|
|
2907
|
+
`<DashboardGrid>` places, `<Widget>` is the chrome, and the layout is plain JSON in cells.
|
|
2908
|
+
|
|
2909
|
+
```tsx
|
|
2910
|
+
import DashboardGrid, { DashboardUtils, Widget } from '@box-kite/react/components/dashboard';
|
|
2911
|
+
import { Sparkline } from '@box-kite/react/components/chart';
|
|
2912
|
+
|
|
2913
|
+
<DashboardGrid layout={layout} onLayoutChange={setLayout} onLayoutCommit={save} editable label="Sales">
|
|
2914
|
+
<Widget id="revenue" title="Revenue" description="Last 12 weeks">
|
|
2915
|
+
<Sparkline data={revenue} variant="area" width="100%" height="100%" />
|
|
2916
|
+
</Widget>
|
|
2917
|
+
<Widget id="orders" title="Orders" onRefresh={reload} loading={pending} />
|
|
2918
|
+
</DashboardGrid>;
|
|
2919
|
+
```
|
|
2920
|
+
|
|
2921
|
+
### The layout
|
|
2922
|
+
|
|
2923
|
+
```jsonc
|
|
2924
|
+
{
|
|
2925
|
+
"version": 1,
|
|
2926
|
+
"columns": 12,
|
|
2927
|
+
"items": [
|
|
2928
|
+
{ "id": "revenue", "x": 0, "y": 0, "w": 6, "h": 2 },
|
|
2929
|
+
{ "id": "orders", "x": 6, "y": 0, "w": 6, "h": 2, "minW": 3, "fixed": false },
|
|
2930
|
+
],
|
|
2931
|
+
}
|
|
2932
|
+
```
|
|
2933
|
+
|
|
2934
|
+
`x`/`y` are zero-based cells, `w`/`h` spans. `minW`/`minH`/`maxW`/`maxH` bound a resize, and `fixed`
|
|
2935
|
+
pins a widget: it is never moved or resized and has no handles at all. The layout is **compacted
|
|
2936
|
+
upward**, so a widget cannot be parked in mid-air and two dashboards of the same widgets in the same
|
|
2937
|
+
places compare equal. A widget dropped on a neighbour **takes the cell**, and the neighbour is handed one
|
|
2938
|
+
of its own — the row above where there is room for it, the row below otherwise. That is `moveTo`'s half
|
|
2939
|
+
of it; compaction is reading order and nothing else.
|
|
2940
|
+
|
|
2941
|
+
### DashboardGridProps
|
|
2942
|
+
|
|
2943
|
+
| Prop | Type | What it does |
|
|
2944
|
+
| -------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
2945
|
+
| `layout` / `defaultLayout` | `DashboardLayout` | Controlled and uncontrolled. Normalized on the way in, so a generated one need not be tidy. |
|
|
2946
|
+
| `onLayoutChange` | `ChangeHandler<DashboardLayout, DashboardReason>` | Every cell a drag crosses, and every arrow key. |
|
|
2947
|
+
| `onLayoutCommit` | `ChangeHandler<DashboardLayout, DashboardReason>` | Once when the interaction ends — the one to write to a server. |
|
|
2948
|
+
| `columns` | `number \| Partial<Record<DashboardSize, number>>` | One count, or one per container size. Default `{ xs: 1, md: 6, xxl: 12 }`. |
|
|
2949
|
+
| `rowHeight` | `number` | The height of one row, ÷4 scale. Default `24` (6rem). |
|
|
2950
|
+
| `editable` | `boolean` | The edit/view split: no handles and nothing in the tab order without it. |
|
|
2951
|
+
| `label` / `labelledBy` | `string` | Names the `role="list"` of widgets. |
|
|
2952
|
+
|
|
2953
|
+
`DashboardReason` is `'move' | 'resize' | 'imperative'` — which _device_ did it is in `details.event`.
|
|
2954
|
+
|
|
2955
|
+
### WidgetProps
|
|
2956
|
+
|
|
2957
|
+
| Prop | Type | What it does |
|
|
2958
|
+
| ----------------------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
|
|
2959
|
+
| `id` | `string` | The layout item it fills. Without one it is a card with the same chrome. |
|
|
2960
|
+
| `title` / `description` | `ReactNode` | The title bar. `title` is a real heading at `level` (default `3`). |
|
|
2961
|
+
| `name` | `string` | What the handles and the announcements call it. Defaults to a string `title`, then `id`. |
|
|
2962
|
+
| `actions` | `ReactNode` | The end of the title bar — a menu, a filter, a link out. |
|
|
2963
|
+
| `loading` / `error` / `empty` | `boolean` / `ReactNode` / … | The three states that replace the content; anything else renders the children. |
|
|
2964
|
+
| `onRefresh` / `refreshLabel` | `() => void` / `string` | A refresh button in the bar, and the retry an error state offers. |
|
|
2965
|
+
|
|
2966
|
+
### Placement is a class; only a drag is an inline style
|
|
2967
|
+
|
|
2968
|
+
A widget's cell is `gridColumnStart`/`gridColumnEnd`/`gridRowStart`/`gridRowEnd` — props, so a shared
|
|
2969
|
+
class — which is why a resting dashboard measures nothing and renders on a server. The one inline style
|
|
2970
|
+
in the component is the translate that keeps a dragged widget under the pointer.
|
|
2971
|
+
|
|
2972
|
+
`columns` as a record projects the one layout into each declared space by arithmetic, written as a
|
|
2973
|
+
container query. **A grid cannot container-query itself** (the query resolves against an ancestor
|
|
2974
|
+
container), so every projection is drawn on the widest arrangement's tracks. And an arrangement can only
|
|
2975
|
+
be edited in the space it is written in: where a projection is showing, the handles are not rendered.
|
|
2976
|
+
|
|
2977
|
+
### The keyboard, and what it says
|
|
2978
|
+
|
|
2979
|
+
Both handles are real buttons. Enter or Space picks the widget up (`aria-pressed`), the arrows move or
|
|
2980
|
+
resize it a cell at a time following the **reading order**, Enter drops it and Escape puts it back — the
|
|
2981
|
+
whole layout with it. A grab that loses focus is cancelled rather than dropped. Every step is announced
|
|
2982
|
+
in a polite live region that exists before there is anything to say.
|
|
2983
|
+
|
|
2984
|
+
### The model (`DashboardUtils`)
|
|
2985
|
+
|
|
2986
|
+
Framework-free, exported from the same module:
|
|
2987
|
+
|
|
2988
|
+
```ts
|
|
2989
|
+
DashboardUtils.SCHEMA; // the layout as JSON Schema — the subset catalog() emits
|
|
2990
|
+
DashboardUtils.parse(value); // { layout, issues } from a database, a file or a model
|
|
2991
|
+
DashboardUtils.add(layout, { id: 'latency', w: 3, h: 2 }); // placed under everything, then floated up
|
|
2992
|
+
DashboardUtils.remove(layout, 'latency');
|
|
2993
|
+
DashboardUtils.moveTo(layout, 'revenue', 3, 0); // what a drag does
|
|
2994
|
+
DashboardUtils.resizeTo(layout, 'revenue', 6, 3);
|
|
2995
|
+
DashboardUtils.project(layout, 6); // the same layout in a narrower space
|
|
2996
|
+
```
|
|
2997
|
+
|
|
2998
|
+
`parse` clamps what is out of range (a generated `w: 0`, a column count of 400), drops what it cannot
|
|
2999
|
+
read and reports it rather than throwing. Where a dashboard is **kept** is the app's decision.
|
|
3000
|
+
|
|
3001
|
+
### Style tree
|
|
3002
|
+
|
|
3003
|
+
`dashboard` (+ `placeholder`) and `widget` (+ `header`, `label`, `title`, `description`, `actions`,
|
|
3004
|
+
`handle`, `body`, `message`, `retry`, `skeleton`). Both handles are the one `handle` node, whose
|
|
3005
|
+
`corner` variant is the resize one; the row height is a variable (`vars={{ 'dashboard-row': '8rem' }}`),
|
|
3006
|
+
so it can be set per breakpoint or per theme.
|
|
3007
|
+
|
|
3008
|
+
## Agent chrome (`components/agent`)
|
|
3009
|
+
|
|
3010
|
+
The parts of an agent's turn that are not prose — what it ran, what it was thinking, what it wants
|
|
3011
|
+
permission to do — plus the text it answers with. One entry, four components, and a framework-free model
|
|
3012
|
+
behind them.
|
|
3013
|
+
|
|
3014
|
+
```tsx
|
|
3015
|
+
import { AgentUtils, ApprovalCard, Reasoning, StreamingText, ToolCallCard } from '@box-kite/react/components/agent';
|
|
3016
|
+
|
|
3017
|
+
<Reasoning duration={1400}>{reasoningText}</Reasoning>
|
|
3018
|
+
<ToolCallCard name="searchOrders" status="success" input={{ orderId: 4182 }} output={{ total: 6400 }} />
|
|
3019
|
+
<ApprovalCard
|
|
3020
|
+
title="Refund order 4182"
|
|
3021
|
+
description="6,400 MDL back to the customer. This cannot be undone."
|
|
3022
|
+
input={{ orderId: 4182, amount: 6400 }}
|
|
3023
|
+
onDecisionChange={(decision) => respond(decision === 'approved')}
|
|
3024
|
+
/>;
|
|
3025
|
+
```
|
|
3026
|
+
|
|
3027
|
+
### ToolCallCard
|
|
3028
|
+
|
|
3029
|
+
| Prop | Type | What it does |
|
|
3030
|
+
| --------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
3031
|
+
| `name` | `ReactNode` | The tool, as the model called it. Required, and what the header reads. |
|
|
3032
|
+
| `description` | `ReactNode` | A line under the name, in the app's own words. |
|
|
3033
|
+
| `status` | `'pending' \| 'running' \| 'success' \| 'error'` | Default `pending`. Every state carries its own **word** beside the dot. |
|
|
3034
|
+
| `input` / `output` / `error` | `unknown` | Shown under `inputLabel`/`outputLabel`/`errorLabel`, formatted rather than trusted. |
|
|
3035
|
+
| `open` / `defaultOpen` / `onOpenChange` | `boolean` / … / `ChangeHandler<boolean, 'trigger'>` | The body's state. Closed by default. |
|
|
3036
|
+
| `collapsible` | `boolean` | Default `true`. `false` leaves the header a plain row. |
|
|
3037
|
+
| `valueLimit` | `number` | Characters before a value is cut. Default 20,000. |
|
|
3038
|
+
|
|
3039
|
+
The four statuses map one for one onto what every runtime reports under its own spelling — AI SDK's
|
|
3040
|
+
`input-streaming` / `input-available` / `output-available` / `output-error` — so wiring a message part
|
|
3041
|
+
to a card is a lookup rather than a state machine. `aria-busy` while running.
|
|
3042
|
+
|
|
3043
|
+
**A card with nothing to disclose renders no control at all**: the header is a row of text rather than a
|
|
3044
|
+
button, because a control that opens nothing is a tab stop nobody wants to land on.
|
|
3045
|
+
|
|
3046
|
+
### ApprovalCard
|
|
3047
|
+
|
|
3048
|
+
| Prop | Type | What it does |
|
|
3049
|
+
| --------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
3050
|
+
| `title` | `ReactNode` | Required. Names the `role="group"`. |
|
|
3051
|
+
| `description` | `ReactNode` | The consequence, and whether it can be undone. |
|
|
3052
|
+
| `input` / `inputLabel` | `unknown` / `ReactNode` | The call being gated, shown as a tool card shows its input. |
|
|
3053
|
+
| `decision` / `defaultDecision` / `onDecisionChange` | `'approved' \| 'rejected' \| null` / … / `ChangeHandler<…, 'approve' \| 'reject'>` | The one channel. |
|
|
3054
|
+
| `busy` | `boolean` | The answer is on its way: both buttons disabled, `aria-busy` on the card. |
|
|
3055
|
+
| `approveLabel` / `rejectLabel` | `ReactNode` | Default `Approve` / `Reject`. |
|
|
3056
|
+
| `autoFocus` | `boolean` | Default `false`. Lands on **Reject** — the least destructive action. |
|
|
3057
|
+
|
|
3058
|
+
`onDecisionChange(decision, { reason })` maps straight onto AI SDK 6's `needsApproval`, CopilotKit's
|
|
3059
|
+
`renderAndWaitForResponse` and AG-UI's `INTERRUPT`. Once there is a decision the buttons are gone, and
|
|
3060
|
+
the answer lands in a `role="status"` **that was in the DOM before there was anything in it** — a live
|
|
3061
|
+
region inserted together with its text is not reliably announced.
|
|
3062
|
+
|
|
3063
|
+
### Reasoning
|
|
3064
|
+
|
|
3065
|
+
| Prop | Type | What it does |
|
|
3066
|
+
| --------------------------------------- | ----------- | --------------------------------------------------------------------------------- |
|
|
3067
|
+
| `label` | `ReactNode` | Default `Reasoning`, or `Thinking…` while `streaming`. |
|
|
3068
|
+
| `streaming` | `boolean` | Still arriving: the header says so, shimmers, and reports `aria-busy`. |
|
|
3069
|
+
| `duration` | `number` | Milliseconds. The header reads "Thought for 4.2s" once it is over. |
|
|
3070
|
+
| `open` / `defaultOpen` / `onOpenChange` | … | Closed by default. Auto-opening while streaming is the app's: `open={streaming}`. |
|
|
3071
|
+
|
|
3072
|
+
It opens in the same one-row grid an `Accordion` panel does — a track running `1fr` to `0fr`, so nothing
|
|
3073
|
+
is measured and the rules are shared with every other disclosure on the page.
|
|
3074
|
+
|
|
3075
|
+
### The model (`AgentUtils`)
|
|
3076
|
+
|
|
3077
|
+
Framework-free, exported from the same module — a tool's arguments are JSON a **model** invented, and
|
|
3078
|
+
`JSON.stringify` answers a circular structure, a `BigInt` and a four-megabyte result with a throw, a
|
|
3079
|
+
throw and a frozen frame:
|
|
3080
|
+
|
|
3081
|
+
```ts
|
|
3082
|
+
AgentUtils.formatValue(part.output, 4000); // { text, truncated } — capped, never throws
|
|
3083
|
+
AgentUtils.hasValue(part.input); // `0` and `''` are values; `null` and `undefined` are not
|
|
3084
|
+
AgentUtils.formatCount(3980); // '3,980' — grouped here rather than by locale, so a prerender matches
|
|
3085
|
+
AgentUtils.formatDuration(4200); // '4.2s'
|
|
3086
|
+
AgentUtils.statusLabel('success'); // 'Done'
|
|
3087
|
+
AgentUtils.decisionLabel('approved'); // 'Approved'
|
|
3088
|
+
```
|
|
3089
|
+
|
|
3090
|
+
### Style tree
|
|
3091
|
+
|
|
3092
|
+
`toolCall` (+ `header`, `summary`, `name`, `description`, `status` with `status.dot`, `arrow`, `track`,
|
|
3093
|
+
`clip`, `body`, `section`, `label`, `value`, `truncated`), `approval` (+ `title`, `description`,
|
|
3094
|
+
`section`, `label`, `value`, `truncated`, `footer`, `actions`, `button`, `decision`) and `reasoning`
|
|
3095
|
+
(+ `trigger`, `arrow`, `duration`, `track`, `clip`, `body`). The status is a variant on
|
|
3096
|
+
`toolCall.status`, the decision one on `approval` itself, and `toolCall.header`'s `interactive` variant
|
|
3097
|
+
is what makes it look pressable — so a card with nothing to open does not.
|
|
3098
|
+
|
|
3099
|
+
### StreamingText
|
|
3100
|
+
|
|
3101
|
+
What the agent _says_, as it arrives. Same entry.
|
|
3102
|
+
|
|
3103
|
+
| Prop | Type | What it does |
|
|
3104
|
+
| ----------- | --------- | ------------------------------------------------------------------------------------ |
|
|
3105
|
+
| `text` | `string` | The message **so far**, not the delta — knowing what is new means comparing the two. |
|
|
3106
|
+
| `streaming` | `boolean` | More is coming: the caret shows and the element reports `aria-busy`. |
|
|
3107
|
+
| `window` | `number` | How many arrived runs stay faded at once. Default 8; `0` turns the entrance off. |
|
|
3108
|
+
| `caret` | `boolean` | Default `true`. |
|
|
3109
|
+
|
|
3110
|
+
```tsx
|
|
3111
|
+
<StreamingText text={message} streaming={status === 'streaming'} />
|
|
3112
|
+
```
|
|
3113
|
+
|
|
3114
|
+
What is on the page is one settled string plus the last few runs to arrive, so a message that is already
|
|
3115
|
+
whole paints at once with nothing animating — a prerendered page, a transcript read back — and one still
|
|
3116
|
+
arriving costs the same at the ten-thousandth token as at the first. Anything but an append (a
|
|
3117
|
+
regenerate, an edit) settles at once rather than fading in the part that differs.
|
|
3118
|
+
|
|
3119
|
+
The entrance is `@starting-style`, not a keyframe, so it rides `--transitionTime` and disappears under
|
|
3120
|
+
`prefers-reduced-motion` with nothing declared for it; the caret is the `pulse` preset. It is **not** a
|
|
3121
|
+
live region — one announcing every token reads the message out a word at a time and again when it
|
|
3122
|
+
finishes — so what announces the turn is the transcript it lands in. Tree: `streamingText` (+ `segment`,
|
|
3123
|
+
`caret`).
|
|
3124
|
+
|
|
3125
|
+
`AgentUtils.initialStream`, `advanceStream` and `streamText` are the model behind it, framework-free and
|
|
3126
|
+
pure: advancing is keyed on the text rather than on a counter, so doing it twice is doing it once.
|
|
3127
|
+
|
|
3128
|
+
## Markdown (`components/markdown`)
|
|
3129
|
+
|
|
3130
|
+
`markdownComponents` is the `components` map `react-markdown`, Streamdown and everything built on that
|
|
3131
|
+
shape already take, with this engine's classes on it — **not** a renderer, because a parser is a choice
|
|
3132
|
+
an app has usually already made, and wrapping Streamdown would ask the project for a Tailwind `@source`
|
|
3133
|
+
line and shadcn's design tokens in a global stylesheet.
|
|
3134
|
+
|
|
3135
|
+
```tsx
|
|
3136
|
+
import Markdown from 'react-markdown';
|
|
3137
|
+
import { markdownComponents } from '@box-kite/react/components/markdown';
|
|
3138
|
+
|
|
3139
|
+
<Box component="markdown">
|
|
3140
|
+
<Markdown components={markdownComponents}>{message}</Markdown>
|
|
3141
|
+
</Box>;
|
|
3142
|
+
```
|
|
3143
|
+
|
|
3144
|
+
Three things to know. It is a **constant, not a factory**: a map built inside render is a new set of
|
|
3145
|
+
component _types_ every token, which React answers by unmounting the whole message and mounting it again
|
|
3146
|
+
— override by spreading at module scope (`{ ...markdownComponents, h1: MyHeading }`). Whether a URL is
|
|
3147
|
+
safe stays the **renderer's** (`urlTransform`, `defaultUrlTransform`), because the href is parsed before
|
|
3148
|
+
a component is called; the map sets `rel="noreferrer"`. And wrapping in `<Box component="markdown">` is
|
|
3149
|
+
what gives the block its size and colour, which the nodes inherit.
|
|
3150
|
+
|
|
3151
|
+
Covers `h1`–`h6`, `p`, `a`, `ul`, `ol`, `li`, `blockquote`, `code`, `pre`, `hr`, `img`, `table`,
|
|
3152
|
+
`thead`, `tbody`, `tr`, `th`, `td`, `strong`, `em`, `del` and a task list's `input`. Tree: `markdown`
|
|
3153
|
+
(+ `heading` with `level1`–`level6`, `paragraph`, `link`, `list`, `item`, `quote`, `code`, `codeBlock`
|
|
3154
|
+
with `language` and `code`, `rule`, `image`, `tableWrap`, `table`, `row`, `cell`, `inline`, `checkbox`).
|
|
3155
|
+
|
|
3156
|
+
## Skeleton (`components/skeleton`)
|
|
3157
|
+
|
|
3158
|
+
Where content will be, while it is being fetched.
|
|
3159
|
+
|
|
3160
|
+
| Prop | Type | What it does |
|
|
3161
|
+
| -------- | ----------- | ---------------------------------------------------------------------------- |
|
|
3162
|
+
| `lines` | `number` | How many bars. Default 1; past that the last one is short. |
|
|
3163
|
+
| `circle` | `boolean` | One round bar — an avatar. `width` sets its size and the height follows. |
|
|
3164
|
+
| `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`. |
|
|
3165
|
+
|
|
3166
|
+
```tsx
|
|
3167
|
+
<Skeleton lines={3} label="Loading orders" />
|
|
3168
|
+
<Skeleton circle width={10} />
|
|
3169
|
+
```
|
|
3170
|
+
|
|
3171
|
+
A `label` belongs on the one skeleton standing for a region and not on each bar — a screen of
|
|
3172
|
+
placeholders each announcing itself is a screen nobody can listen to, and a region inserted together
|
|
3173
|
+
with its text is not reliably announced anyway, so what it really buys is words for somebody who lands
|
|
3174
|
+
there. The gloss is a named duration in milliseconds, so it sits outside what `--transitionTime` zeroes
|
|
3175
|
+
and stops itself under `prefers-reduced-motion`. It renders on a server. Tree: `skeleton` (+ `bar` with
|
|
3176
|
+
`short` and `circle` variants, `gloss`).
|
|
3177
|
+
|
|
3178
|
+
## The catalog (`@box-kite/react/catalog`)
|
|
3179
|
+
|
|
3180
|
+
What a **generated** UI is allowed to build, as JSON Schema — the runtime counterpart to everything
|
|
3181
|
+
above, which is for whatever writes the code at dev time. A generative-UI runtime (json-render,
|
|
3182
|
+
assistant-ui, CopilotKit, A2UI) takes a component catalog; this is that catalog, read off the live prop
|
|
3183
|
+
registry rather than written down.
|
|
3184
|
+
|
|
3185
|
+
```ts
|
|
3186
|
+
import { catalog } from '@box-kite/react/catalog';
|
|
3187
|
+
|
|
3188
|
+
const allowed = catalog({ include: ['Flex', 'H2', 'P', 'Sparkline'], styleProps: ['d', 'gap', 'p', 'bgColor', 'fontSize'] });
|
|
3189
|
+
|
|
3190
|
+
allowed.components.Flex.props; // a strict JSON Schema: additionalProperties false, colours patterned to the palette
|
|
3191
|
+
allowed.rules; // the dividers — what a schema cannot state and a prompt must
|
|
3192
|
+
allowed.tokens; // the colours, the @keyframes names and the style-tree nodes a host may restyle
|
|
3193
|
+
```
|
|
3194
|
+
|
|
3195
|
+
- **The engine says what a prop accepts, the manifest says what it means.** Values come off the live
|
|
3196
|
+
registry, so a `Box.extend()` prop or colour is in the catalog with nothing regenerated — call
|
|
3197
|
+
`catalog()` after the `extend()`. Prose is generated from the same JSDoc as the prop reference.
|
|
3198
|
+
- **A colour prop is a `pattern` over the palette**, so a generated tree cannot invent `#ff00ff`. Every
|
|
3199
|
+
closed value list is an `enum`; a prop the registry leaves open is `{ type: 'string' }` with its
|
|
3200
|
+
values as `examples`, never a constraint the library does not enforce.
|
|
3201
|
+
- **Function props are `events`** (named, for the host to bind) and a `ReactNode` prop is a **slot**
|
|
3202
|
+
(`children` is `default`). A prop a JSON spec cannot express is left out rather than half-described.
|
|
3203
|
+
- **The props that are a _shape_ are described by hand**, since the other half of each is React:
|
|
3204
|
+
`DataGrid`'s `def` (required — the columns and the grid-wide flags) and `data`, `DashboardGrid`'s
|
|
3205
|
+
`layout`/`defaultLayout`/`columns`, `Widget`'s `empty`, `ChartContainer`'s `series`. A column's nested
|
|
3206
|
+
`columns`, and `dataSource`/`onCellEdit`/`rowDetail`/`treeData`, stay out — they are the app's to pass.
|
|
3207
|
+
- **The allow-list is the app's.** `catalog()` with no options is all 78 components and all 221 props —
|
|
3208
|
+
~4.6 MB serialized. `include`, `exclude` and `styleProps` are how an app narrows it.
|
|
3209
|
+
|
|
3210
|
+
Full reference: `docs/catalog.md`.
|
|
3211
|
+
|
|
3212
|
+
---
|
|
3213
|
+
|
|
3214
|
+
## The spec renderer (`@box-kite/react/spec`)
|
|
3215
|
+
|
|
3216
|
+
The other half of the catalog: the catalog says what a model may write, this renders what it wrote.
|
|
3217
|
+
Nothing in it imports Box — the components are the app's, handed over in a registry — so the entry
|
|
3218
|
+
carries no engine.
|
|
3219
|
+
|
|
3220
|
+
```tsx
|
|
3221
|
+
import { catalog } from '@box-kite/react/catalog';
|
|
3222
|
+
import Flex from '@box-kite/react/components/flex';
|
|
3223
|
+
import { H2, P } from '@box-kite/react/components/semantics';
|
|
3224
|
+
import SpecRenderer, { createSpecRegistry } from '@box-kite/react/spec';
|
|
3225
|
+
|
|
3226
|
+
const allowed = catalog({ include: ['Flex', 'H2', 'P'], styleProps: ['d', 'gap', 'p', 'bgColor', 'fontSize'] });
|
|
3227
|
+
const registry = createSpecRegistry({ catalog: allowed, components: { Flex, H2, P } });
|
|
3228
|
+
|
|
3229
|
+
<SpecRenderer spec={spec} registry={registry} data={data} onAction={(action, details) => run(action, details)} />;
|
|
3230
|
+
```
|
|
3231
|
+
|
|
3232
|
+
A node is JSON, and these are all the fields it has:
|
|
3233
|
+
|
|
3234
|
+
| Field | What it is |
|
|
3235
|
+
| ---------- | -------------------------------------------------------------------------------------------- |
|
|
3236
|
+
| `type` | The component. A name the registry does not hold renders nothing. |
|
|
3237
|
+
| `props` | Everything that component's own schema allows. A prop it refuses is dropped, not the node. |
|
|
3238
|
+
| `children` | The default slot: nodes, text, or `{ $data }` references to text. |
|
|
3239
|
+
| `slots` | A named slot (`Tooltip`'s `content`, a control's `label`). |
|
|
3240
|
+
| `on` | An event the catalog lists, bound to an action name: `{ onClick: 'refresh' }`. |
|
|
3241
|
+
| `repeat` | A reference to an array. The node renders once per item, with `$item` and `$index` in scope. |
|
|
3242
|
+
| `key` | React's key, where a node's identity has to survive a re-order. |
|
|
3243
|
+
|
|
3244
|
+
```jsonc
|
|
3245
|
+
{
|
|
3246
|
+
"type": "Flex",
|
|
3247
|
+
"props": { "d": "column", "gap": 4, "bgColor": "sky-500/10" },
|
|
3248
|
+
"children": [
|
|
3249
|
+
{ "type": "H2", "props": { "fontSize": 24 }, "children": ["Revenue"] },
|
|
3250
|
+
{ "type": "P", "children": [{ "$data": "summary" }] },
|
|
3251
|
+
],
|
|
3252
|
+
}
|
|
3253
|
+
```
|
|
3254
|
+
|
|
3255
|
+
- **The registry is the allow-list.** `createSpecRegistry({ catalog, components })` pairs an
|
|
3256
|
+
implementation with the catalog's rules for it; a component of your own takes `{ component, props,
|
|
3257
|
+
slots, events }` with its own JSON Schema, and one registered **without** a schema takes no props at
|
|
3258
|
+
all.
|
|
3259
|
+
- **Only an `event` can become a function.** `on` binds nothing else, and what an action does is the
|
|
3260
|
+
host's — `onAction(action, { payload, args, prop, component, path })`. There is no tag from the
|
|
3261
|
+
spec, no `eval` and no `dangerouslySetInnerHTML`.
|
|
3262
|
+
- **A reference resolves before it is validated.** `{ $data: 'stats.revenue' }` (a dot path or a JSON
|
|
3263
|
+
Pointer), `{ $item: 'label' }` and `{ $index: true }` read from `data`, own properties only, and the
|
|
3264
|
+
resolved value is what the schema judges.
|
|
3265
|
+
- **A partial spec is the normal case.** A node with no `type` yet renders nothing and reports nothing;
|
|
3266
|
+
a half-written value fails its schema and is dropped until it is whole.
|
|
3267
|
+
- **One node cannot take the tree down.** Every node has an error boundary; `fallback` is what stands
|
|
3268
|
+
in its place (nothing, by default), and `maxNodes` (1,000) and `maxDepth` (32) end a runaway tree.
|
|
3269
|
+
- **Everything refused is reported.** `onIssues` gets a `{ code, path, message, component, prop }` per
|
|
3270
|
+
render whose issues changed: `unknown-component`, `unknown-prop`, `invalid-prop`, `unknown-slot`,
|
|
3271
|
+
`unknown-event`, `unresolved-data`, `missing-prop`, `invalid-child`, `invalid-repeat`, `too-deep`,
|
|
3272
|
+
`too-many-nodes`, `render-error`.
|
|
3273
|
+
- **A node missing a prop it cannot do without is held back.** A required prop that has not arrived —
|
|
3274
|
+
or was refused — would be `undefined` inside the component, so the node renders nothing and reports
|
|
3275
|
+
`missing-prop` instead of throwing into its own boundary once a frame.
|
|
3276
|
+
- **A generated node writes the controlled prop, never the `default…` twin.** `layout` not
|
|
3277
|
+
`defaultLayout`, `value` not `defaultValue`: React reads an uncontrolled default once, so the frame
|
|
3278
|
+
it first arrived whole in is the one that sticks, and a second spec rendered in the same place keeps
|
|
3279
|
+
the first one's state.
|
|
3280
|
+
- **What a node has been shown with, it is not stripped of.** A stream is not monotone — a column's
|
|
3281
|
+
`align` passes through `"e"` on its way to `"end"`, and that takes the whole `def` with it — so
|
|
3282
|
+
`<SpecRenderer>` keeps the last value each node was given for a prop it cannot render without. It
|
|
3283
|
+
remembers while it is mounted; `key` it to forget. Gating a heavy component is the app's call:
|
|
3284
|
+
point its name at a placeholder in the registry while the spec is arriving.
|
|
3285
|
+
|
|
3286
|
+
`specSchema(source, { bindings, root })` is the other direction: one JSON Schema for a whole tree, for
|
|
3287
|
+
`streamObject`, a structured-output API or `z.fromJSONSchema`. **Import it from
|
|
3288
|
+
`@box-kite/react/catalog` in a route handler** — it is exported there too, and that entry renders
|
|
3289
|
+
nothing, where `/spec` carries a `use client` banner; it takes a `catalog()` as readily as a registry,
|
|
3290
|
+
so the server half of the loop needs no components. `renderSpec(spec, options)` is the same walk with
|
|
3291
|
+
no hook in it — it returns `{ element, issues }`, so a static spec renders on a server.
|
|
3292
|
+
|
|
3293
|
+
Full reference: `docs/catalog.md`.
|
|
3294
|
+
|
|
3295
|
+
## Ecosystem interop (`@box-kite/react/interop`)
|
|
3296
|
+
|
|
3297
|
+
The agentic runtimes' shapes and this library's, mapped onto each other. It **imports none of them**:
|
|
3298
|
+
an adapter that pulled in a runtime would be choosing one for the app. Framework-free and engine-free,
|
|
3299
|
+
so a route handler and a client both import from it.
|
|
3300
|
+
|
|
3301
|
+
```ts
|
|
3302
|
+
import { toolPart, applyToolEvent } from '@box-kite/react/interop';
|
|
3303
|
+
import { a2uiApply, a2uiEmpty, a2uiSurface, a2uiToSpec, a2uiCatalog, a2uiComponentSchema } from '@box-kite/react/interop';
|
|
3304
|
+
import { fromGenerativeUi, toGenerativeUi } from '@box-kite/react/interop';
|
|
3305
|
+
```
|
|
3306
|
+
|
|
3307
|
+
`toolPart(part)` reads a part from AI SDK, assistant-ui or CopilotKit and answers
|
|
3308
|
+
`{ kind, status, decision, input, output, error }` — `kind` is `'call'` or `'approval'`, `status` is
|
|
3309
|
+
the `pending`/`running`/`success`/`error` `<ToolCallCard>` takes, and `null` means it is not a tool part
|
|
3310
|
+
at all. **A decision is not a stage a call passes through, it is a question somebody answers**, which is
|
|
3311
|
+
why two components cover six states:
|
|
3312
|
+
|
|
3313
|
+
| Runtime | Call | Question |
|
|
3314
|
+
| ------------ | --------------------------------------------------------------------- | ----------------------------------------- |
|
|
3315
|
+
| AI SDK 6/7 | `input-streaming` `input-available` `output-available` `output-error` | `approval-requested` `approval-responded` |
|
|
3316
|
+
| assistant-ui | `status.type` `running` `complete` `incomplete` | `requires-action`, under either reason |
|
|
3317
|
+
| CopilotKit | `inProgress` `executing` `complete` | `executing` holding a `respond` |
|
|
3318
|
+
| AG-UI | `TOOL_CALL_START` `_ARGS` `_END` `_RESULT` | `INTERRUPT` |
|
|
3319
|
+
|
|
3320
|
+
**AG-UI reports events, not parts**, so its cards are a fold rather than a mapping:
|
|
3321
|
+
`applyToolEvent(parts, event)`, one call per event, oldest first, keyed by `toolCallId`.
|
|
3322
|
+
|
|
3323
|
+
### A2UI
|
|
3324
|
+
|
|
3325
|
+
The only genuinely different shape here: a **flat adjacency list** of components referring to each other
|
|
3326
|
+
by id, arriving one message at a time, with a data model of its own per surface.
|
|
3327
|
+
|
|
3328
|
+
```tsx
|
|
3329
|
+
const [state, setState] = useState(a2uiEmpty);
|
|
3330
|
+
// v0.8 and v0.9 both. Pure, and the same state back when a message changed nothing.
|
|
3331
|
+
const next = (message: unknown) => setState((current) => a2uiApply(current, message));
|
|
3332
|
+
const surface = a2uiSurface(state);
|
|
3333
|
+
|
|
3334
|
+
<SpecRenderer spec={a2uiToSpec(surface, { catalog })} registry={registry} data={surface?.data} onAction={run} />;
|
|
3335
|
+
```
|
|
3336
|
+
|
|
3337
|
+
- **Its data binding is already `$data`.** `{ "path": "/user/email" }` is a JSON Pointer, which `$data`
|
|
3338
|
+
has always taken — a rename, not a parse. The surface's own data model goes in `<SpecRenderer data>`.
|
|
3339
|
+
- **A template is a `repeat`.** `children: { componentId, path }` is one node per item of an array.
|
|
3340
|
+
- **A half-arrived surface is the ordinary case.** A component nothing reaches from the root is not
|
|
3341
|
+
rendered yet; a cycle in the id graph is cut rather than walked.
|
|
3342
|
+
- **`id`, `component`, `child`, `children` and `action` never reach the registry as props.** `action`
|
|
3343
|
+
becomes an `on` binding under that name, so an app registering Box components under A2UI's own names
|
|
3344
|
+
declares `events: ['action']` on them. Pass `{ catalog }` instead where the agent generated against
|
|
3345
|
+
this library, and a prop the catalog calls an event is bound as one.
|
|
3346
|
+
|
|
3347
|
+
`a2uiCatalog(catalog(), { catalogId })` is the other direction — `catalog()` as an A2UI catalog
|
|
3348
|
+
document, where a component carries its own `id`, its type is a property rather than the key above it,
|
|
3349
|
+
and its children are **ids**. Serve it at the `catalogId` you gave it.
|
|
3350
|
+
|
|
3351
|
+
### assistant-ui, CopilotKit, json-render
|
|
3352
|
+
|
|
3353
|
+
`fromGenerativeUi(spec)` reads their `GenerativeUISpec` (`component` where this says `type`, a bare
|
|
3354
|
+
string child that renders as text) and `toGenerativeUi(node)` goes back, returning `{ spec, losses }`:
|
|
3355
|
+
their nodes carry no data binding, no repeat and no action channel, so what could not come along is
|
|
3356
|
+
**reported** rather than dropped in silence. Tool UI is `makeAssistantToolUI` with the cards in it.
|
|
3357
|
+
|
|
3358
|
+
CopilotKit is two surfaces. Its A2UI renderer takes Zod, so `z.fromJSONSchema` over the catalog document
|
|
3359
|
+
is the bridge — and **a `$ref` resolves against the document, not against the piece you lifted out of
|
|
3360
|
+
it**, so `a2uiComponentSchema(document, name)` is what to convert (measured: zod 4.6 throws
|
|
3361
|
+
`Reference not found: #/$defs/color` otherwise). Its actions take a render function, where `render` is a
|
|
3362
|
+
call being shown and `renderAndWaitForResponse` is a question whose `respond` is what
|
|
3363
|
+
`onDecisionChange` calls. json-render is the same `z.fromJSONSchema` over `catalog()` itself.
|
|
3364
|
+
|
|
3365
|
+
Full reference: `docs/interop.md`.
|