@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.
Files changed (100) hide show
  1. package/.claude/rules/box-kite-rules.md +16 -0
  2. package/.claude/skills/box-kite/SKILL.md +16 -0
  3. package/.claude/skills/box-kite/references/components.md +8 -3
  4. package/.claude/skills/box-kite/references/patterns.md +23 -0
  5. package/.cursor/rules/box-kite.mdc +16 -0
  6. package/AGENTS.md +19 -0
  7. package/BOX_AI_CONTEXT.md +562 -1
  8. package/BOX_KITE_AI_CONTEXT.md +562 -1
  9. package/README.md +39 -7
  10. package/catalog.cjs +1 -0
  11. package/catalog.d.ts +8 -0
  12. package/catalog.mjs +2333 -0
  13. package/client.cjs +1 -1
  14. package/client.mjs +40 -42
  15. package/components/agent.cjs +1 -0
  16. package/components/agent.d.ts +215 -0
  17. package/components/agent.mjs +405 -0
  18. package/components/chart.d.ts +1 -0
  19. package/components/dashboard.cjs +1 -0
  20. package/components/dashboard.d.ts +121 -0
  21. package/components/dashboard.mjs +690 -0
  22. package/components/dataGrid/components/dataGridBody.d.ts +2 -1
  23. package/components/dataGrid/components/dataGridDetailRow.d.ts +13 -2
  24. package/components/dataGrid/components/dataGridGroupRow.d.ts +13 -2
  25. package/components/dataGrid/components/dataGridRow.d.ts +13 -2
  26. package/components/dataGrid/contracts/dataGridContract.d.ts +66 -2
  27. package/components/dataGrid/models/cellModel.d.ts +3 -0
  28. package/components/dataGrid/models/editModel.d.ts +27 -0
  29. package/components/dataGrid/models/exportModel.d.ts +2 -1
  30. package/components/dataGrid/models/gridModel.d.ts +3 -0
  31. package/components/dataGrid/models/rangeModel.d.ts +119 -0
  32. package/components/dataGrid/models/viewportModel.d.ts +23 -2
  33. package/components/dataGrid/useGridNavigation.d.ts +10 -2
  34. package/components/dataGrid.cjs +4 -1
  35. package/components/dataGrid.d.ts +3 -0
  36. package/components/dataGrid.mjs +1056 -765
  37. package/components/markdown.cjs +1 -0
  38. package/components/markdown.d.ts +55 -0
  39. package/components/markdown.mjs +179 -0
  40. package/components/semantics.d.ts +29 -29
  41. package/components/skeleton.cjs +1 -0
  42. package/components/skeleton.d.ts +34 -0
  43. package/components/skeleton.mjs +25 -0
  44. package/dashboardSchema.cjs +1 -0
  45. package/dashboardSchema.mjs +78 -0
  46. package/docs/a11y.md +2 -3
  47. package/docs/catalog.md +451 -0
  48. package/docs/components.md +4 -0
  49. package/docs/index.md +3 -0
  50. package/docs/interop.md +237 -0
  51. package/docs/mcp.md +69 -0
  52. package/docs/props.md +3 -3
  53. package/interop.cjs +1 -0
  54. package/interop.d.ts +51 -0
  55. package/interop.mjs +454 -0
  56. package/mcp/src/data.d.ts +88 -0
  57. package/mcp/src/data.test.d.ts +1 -0
  58. package/mcp/src/engine.d.ts +23 -0
  59. package/mcp/src/index.d.ts +1 -0
  60. package/mcp/src/search.d.ts +26 -0
  61. package/mcp/src/search.test.d.ts +1 -0
  62. package/mcp/src/server.d.ts +18 -0
  63. package/mcp/src/server.test.d.ts +1 -0
  64. package/mcp/src/styles.d.ts +27 -0
  65. package/mcp/src/styles.test.d.ts +1 -0
  66. package/mcp/src/tools.d.ts +18 -0
  67. package/mcp/src/vocabulary.d.ts +41 -0
  68. package/package.json +124 -17
  69. package/react/spec/renderSpec.d.ts +51 -0
  70. package/react/spec/specBoundary.d.ts +25 -0
  71. package/react/spec/specRegistry.d.ts +42 -0
  72. package/react/spec/specRenderer.d.ts +10 -0
  73. package/registry/blocks/dashboard-shell/dashboard-shell.d.ts +22 -0
  74. package/registry/blocks/dashboard-shell/dashboard-stats.d.ts +13 -0
  75. package/registry/blocks/data-grid/invoice-rows.d.ts +15 -0
  76. package/registry/blocks/data-grid/invoices-grid.d.ts +1 -0
  77. package/registry/blocks/settings-form/settings-form.d.ts +17 -0
  78. package/spec.cjs +2 -0
  79. package/spec.d.ts +12 -0
  80. package/spec.mjs +382 -0
  81. package/specData.cjs +1 -0
  82. package/specData.mjs +69 -0
  83. package/specDefs.cjs +1 -0
  84. package/specDefs.mjs +33 -0
  85. package/specSchema.cjs +1 -0
  86. package/specSchema.mjs +88 -0
  87. package/utils/agent/agentUtils.d.ts +81 -0
  88. package/utils/catalog/catalogContracts.d.ts +19 -0
  89. package/utils/catalog/catalogManifest.d.ts +8 -0
  90. package/utils/dashboard/dashboardSchema.d.ts +18 -0
  91. package/utils/dashboard/dashboardUtils.d.ts +97 -0
  92. package/utils/interop/a2uiInterop.d.ts +82 -0
  93. package/utils/interop/assistantUiInterop.d.ts +34 -0
  94. package/utils/interop/toolInterop.d.ts +30 -0
  95. package/utils/spec/specData.d.ts +19 -0
  96. package/utils/spec/specDefs.d.ts +11 -0
  97. package/utils/spec/specProps.d.ts +15 -0
  98. package/utils/spec/specSchema.d.ts +23 -0
  99. package/utils/spec/specTypes.d.ts +71 -0
  100. package/utils/spec/specValidate.d.ts +10 -0
package/BOX_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`.