@box-kite/react 2.0.1 → 2.2.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 (145) hide show
  1. package/.claude/rules/box-kite-rules.md +24 -2
  2. package/.claude/skills/box-kite/SKILL.md +27 -5
  3. package/.claude/skills/box-kite/references/components.md +8 -3
  4. package/.claude/skills/box-kite/references/patterns.md +24 -1
  5. package/.claude/skills/box-kite/references/styling.md +23 -0
  6. package/.cursor/rules/box-kite.mdc +25 -3
  7. package/AGENTS.md +28 -3
  8. package/BOX_AI_CONTEXT.md +732 -12
  9. package/BOX_KITE_AI_CONTEXT.md +732 -12
  10. package/README.md +41 -9
  11. package/box.cjs +1 -1
  12. package/box.d.ts +13 -2
  13. package/box.mjs +25 -18
  14. package/catalog.cjs +1 -0
  15. package/catalog.d.ts +8 -0
  16. package/catalog.mjs +2347 -0
  17. package/client.cjs +1 -1
  18. package/client.mjs +59 -53
  19. package/components/agent.cjs +1 -0
  20. package/components/agent.d.ts +215 -0
  21. package/components/agent.mjs +405 -0
  22. package/components/button.cjs +1 -1
  23. package/components/button.mjs +10 -14
  24. package/components/chart.d.ts +1 -0
  25. package/components/dashboard.cjs +1 -0
  26. package/components/dashboard.d.ts +121 -0
  27. package/components/dashboard.mjs +690 -0
  28. package/components/dataGrid/components/dataGridBody.d.ts +2 -1
  29. package/components/dataGrid/components/dataGridDetailRow.d.ts +13 -2
  30. package/components/dataGrid/components/dataGridGroupRow.d.ts +13 -2
  31. package/components/dataGrid/components/dataGridRow.d.ts +13 -2
  32. package/components/dataGrid/contracts/dataGridContract.d.ts +66 -2
  33. package/components/dataGrid/models/cellModel.d.ts +3 -0
  34. package/components/dataGrid/models/editModel.d.ts +27 -0
  35. package/components/dataGrid/models/exportModel.d.ts +2 -1
  36. package/components/dataGrid/models/gridModel.d.ts +3 -0
  37. package/components/dataGrid/models/rangeModel.d.ts +119 -0
  38. package/components/dataGrid/models/viewportModel.d.ts +23 -2
  39. package/components/dataGrid/useGridNavigation.d.ts +10 -2
  40. package/components/dataGrid.cjs +4 -1
  41. package/components/dataGrid.d.ts +3 -0
  42. package/components/dataGrid.mjs +1056 -765
  43. package/components/flex.cjs +1 -1
  44. package/components/flex.mjs +10 -13
  45. package/components/form.cjs +1 -1
  46. package/components/form.mjs +7 -7
  47. package/components/grid.cjs +1 -1
  48. package/components/grid.mjs +10 -13
  49. package/components/markdown.cjs +1 -0
  50. package/components/markdown.d.ts +55 -0
  51. package/components/markdown.mjs +179 -0
  52. package/components/menu.cjs +1 -1
  53. package/components/menu.mjs +16 -16
  54. package/components/overlay.cjs +1 -1
  55. package/components/overlay.mjs +35 -34
  56. package/components/popover.cjs +1 -1
  57. package/components/popover.mjs +4 -4
  58. package/components/semantics.cjs +1 -1
  59. package/components/semantics.d.ts +29 -29
  60. package/components/semantics.mjs +9 -13
  61. package/components/skeleton.cjs +1 -0
  62. package/components/skeleton.d.ts +34 -0
  63. package/components/skeleton.mjs +25 -0
  64. package/components/svg.cjs +1 -1
  65. package/components/svg.mjs +38 -44
  66. package/components/textarea.cjs +1 -1
  67. package/components/textarea.mjs +12 -16
  68. package/components/textbox.cjs +1 -1
  69. package/components/textbox.mjs +12 -16
  70. package/components/toaster.cjs +1 -1
  71. package/components/toaster.mjs +42 -43
  72. package/components/visuallyHidden.cjs +1 -1
  73. package/components/visuallyHidden.mjs +10 -13
  74. package/dashboardSchema.cjs +1 -0
  75. package/dashboardSchema.mjs +78 -0
  76. package/docs/a11y.md +2 -3
  77. package/docs/catalog.md +451 -0
  78. package/docs/components.md +4 -0
  79. package/docs/index.md +3 -0
  80. package/docs/interop.md +237 -0
  81. package/docs/mcp.md +70 -0
  82. package/docs/props.md +135 -23
  83. package/interop.cjs +1 -0
  84. package/interop.d.ts +51 -0
  85. package/interop.mjs +454 -0
  86. package/layer.cjs +1 -1
  87. package/layer.mjs +61 -59
  88. package/mcp/src/data.d.ts +88 -0
  89. package/mcp/src/data.test.d.ts +1 -0
  90. package/mcp/src/engine.d.ts +23 -0
  91. package/mcp/src/index.d.ts +1 -0
  92. package/mcp/src/search.d.ts +26 -0
  93. package/mcp/src/search.test.d.ts +1 -0
  94. package/mcp/src/server.d.ts +18 -0
  95. package/mcp/src/server.test.d.ts +1 -0
  96. package/mcp/src/styles.d.ts +27 -0
  97. package/mcp/src/styles.test.d.ts +1 -0
  98. package/mcp/src/tools.d.ts +18 -0
  99. package/mcp/src/vocabulary.d.ts +41 -0
  100. package/package.json +170 -37
  101. package/react/boxComponent.d.ts +10 -0
  102. package/react/popover/useTopLayer.d.ts +17 -0
  103. package/react/spec/renderSpec.d.ts +51 -0
  104. package/react/spec/specBoundary.d.ts +25 -0
  105. package/react/spec/specRegistry.d.ts +42 -0
  106. package/react/spec/specRenderer.d.ts +10 -0
  107. package/react/theme/theme.d.ts +10 -0
  108. package/react/useStyles.d.ts +2 -0
  109. package/react-shared.cjs +1 -1
  110. package/react-shared.mjs +44 -37
  111. package/registry/blocks/dashboard-shell/dashboard-shell.d.ts +22 -0
  112. package/registry/blocks/dashboard-shell/dashboard-stats.d.ts +13 -0
  113. package/registry/blocks/data-grid/invoice-rows.d.ts +15 -0
  114. package/registry/blocks/data-grid/invoices-grid.d.ts +1 -0
  115. package/registry/blocks/settings-form/settings-form.d.ts +17 -0
  116. package/rsc.cjs +1 -1
  117. package/rsc.d.ts +11 -1
  118. package/rsc.mjs +15 -12
  119. package/spec.cjs +2 -0
  120. package/spec.d.ts +12 -0
  121. package/spec.mjs +382 -0
  122. package/specData.cjs +1 -0
  123. package/specData.mjs +69 -0
  124. package/specDefs.cjs +1 -0
  125. package/specDefs.mjs +33 -0
  126. package/specSchema.cjs +1 -0
  127. package/specSchema.mjs +88 -0
  128. package/ssg.cjs +1 -1
  129. package/ssg.d.ts +8 -2
  130. package/ssg.mjs +13 -9
  131. package/utils/agent/agentUtils.d.ts +81 -0
  132. package/utils/catalog/catalogContracts.d.ts +19 -0
  133. package/utils/catalog/catalogManifest.d.ts +8 -0
  134. package/utils/dashboard/dashboardSchema.d.ts +18 -0
  135. package/utils/dashboard/dashboardUtils.d.ts +97 -0
  136. package/utils/environment/environmentUtils.d.ts +5 -0
  137. package/utils/interop/a2uiInterop.d.ts +82 -0
  138. package/utils/interop/assistantUiInterop.d.ts +34 -0
  139. package/utils/interop/toolInterop.d.ts +30 -0
  140. package/utils/spec/specData.d.ts +19 -0
  141. package/utils/spec/specDefs.d.ts +11 -0
  142. package/utils/spec/specProps.d.ts +15 -0
  143. package/utils/spec/specSchema.d.ts +23 -0
  144. package/utils/spec/specTypes.d.ts +71 -0
  145. package/utils/spec/specValidate.d.ts +10 -0
@@ -1,6 +1,6 @@
1
1
  # @box-kite/react - AI Assistant Context
2
2
 
3
- Runtime CSS-in-JS library. `Box` component accepts 221 CSS props and generates CSS classes at runtime. Same prop values share a single class.
3
+ Runtime CSS-in-JS library. `Box` component accepts 235 CSS props and generates CSS classes at runtime. Same prop values share a single class.
4
4
 
5
5
  ---
6
6
 
@@ -282,9 +282,13 @@ const { css, anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', ali
282
282
  The palette is **Tailwind 4.3's, in OKLCH**: twenty-six families of eleven steps (`50`–`950`) — the five
283
283
  neutrals (`slate`, `gray`, `zinc`, `neutral`, `stone`), the four Tailwind 4.3 added (`mauve`, `mist`,
284
284
  `olive`, `taupe`) and seventeen hues (`red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`,
285
- `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`). Each token is a
286
- CSS variable declared in `:root` the first time something uses it, so a colour you never write costs
287
- nothing.
285
+ `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`). A token is a
286
+ CSS variable written **with its own value as the fallback** — `bgColor="sky-500"` is
287
+ `background-color: var(--sky-500,oklch(68.5% .169 237.3))` — so nothing is declared in `:root` (a late
288
+ `:root` insert recalculates every element on the page) and declaring `--sky-500` yourself still overrides
289
+ it. `var(--sky-500)` elsewhere in this file is that, shortened. Your own CSS cannot count on `--sky-500`
290
+ existing: hand it a variable of yours with `vars={{ brand: 'violet-500' }}` on an ancestor. A hand-written
291
+ `var(--sky-500)` _inside_ a Box value — `fill`, `vars`, `css`, a `Box.extend()` variable — gets the fallback too.
288
292
 
289
293
  Any colour value takes a slash and a percentage:
290
294
 
@@ -642,6 +646,131 @@ const wobble = Box.spring({ stiffness: 120, damping: 8 });
642
646
 
643
647
  ---
644
648
 
649
+ ### Scroll-driven animations
650
+
651
+ An animation's progress can come from a scroll position rather than a clock. There is no listener, no
652
+ `requestAnimationFrame` and no state: the browser runs it off the compositor, and the whole thing is props.
653
+
654
+ | Prop | CSS Property | Notes |
655
+ | ------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
656
+ | `animationTimeline` | animation-timeline | `'none'`, `'auto'`, an anonymous timeline (`'scroll()'`, `'scroll(root block)'`, `'view()'`, `'view(inline 20%)'`) or a name a declaration below gave |
657
+ | `animationRange` | animation-range | One end or both: `'entry'`, `'cover 20%'`, `'entry 0% entry 100%'`, `'normal'` |
658
+ | `animationRangeStart` / `animationRangeEnd` | animation-range-start / -end | One end at a time. A named part of the pass with an optional offset, a bare length, or `'normal'` |
659
+ | `scrollTimeline` | scroll-timeline | Declares a named timeline on a **scroller**: `'page'` or `'page block'`. The `--` is optional and added for you |
660
+ | `scrollTimelineName` / `scrollTimelineAxis` | scroll-timeline-name / -axis | The two halves of that shorthand. Axis: `'block'`, `'inline'`, `'x'`, `'y'` |
661
+ | `viewTimeline` | view-timeline | Declares a named timeline on a **subject** — its own pass across the scrollport: `'card'` or `'card block'` |
662
+ | `viewTimelineName` / `viewTimelineAxis` | view-timeline-name / -axis | The two halves of that one |
663
+ | `viewTimelineInset` | view-timeline-inset | `'auto'`, or one or two lengths, shrinking the scrollport the pass is measured against |
664
+ | `timelineScope` | timeline-scope | Makes named timelines below this element reachable from anywhere else below it. Comma-separated |
665
+
666
+ The two anonymous timelines cover most of it and need nothing declared elsewhere:
667
+
668
+ ```tsx
669
+ // A reading-progress bar: the page's own scroll, driving a scaleX from 0 to 1.
670
+ Box.keyframes({ progress: { from: { scale: 0 }, to: { scale: 1 } } });
671
+
672
+ <Box
673
+ position="fixed"
674
+ top={0}
675
+ insetX={0}
676
+ height={1}
677
+ bgColor="sky-500"
678
+ css={{ transformOrigin: 'left' }}
679
+ animationName="progress"
680
+ animationTimeline="scroll(root)"
681
+ animationFillMode="both"
682
+ motionReduce={{ animation: 'none' }}
683
+ />;
684
+
685
+ // Reveal on scroll: this element's own pass, over the first 60% of its entry.
686
+ Box.keyframes({ 'fade-up': { from: { opacity: 0, translateY: 4 }, to: { opacity: 1, translateY: 0 } } });
687
+
688
+ <Box animationName="fade-up" animationTimeline="view()" animationRange="entry 0% entry 60%" motionReduce={{ animation: 'none' }}>
689
+ …
690
+ </Box>;
691
+ ```
692
+
693
+ A **named** timeline is the two-sided form, for when the animated element is not inside the scroller:
694
+
695
+ ```tsx
696
+ <Box timelineScope="page">
697
+ <Box height="fit-screen" overflow="auto" scrollTimeline="page block">
698
+ …
699
+ </Box>
700
+ <Box animationName="progress" animationTimeline="page" animationFillMode="both" />
701
+ </Box>
702
+ ```
703
+
704
+ **Three things to know, and all three are traps:**
705
+
706
+ - **The CSS `animation` shorthand resets `animation-timeline`.** A timeline declared before it is
707
+ silently undone. The registry declares `animationTimeline` after `animation`, so writing both props is
708
+ safe in either order — but an `animation` written inside `css` sorts last and will undo it.
709
+ - **A scroll-driven animation has no duration**, so `--transitionTime` cannot zero it. It is the one kind
710
+ of motion in this library that does **not** stop itself under `prefers-reduced-motion`: pair it with
711
+ `motionReduce={{ animation: 'none' }}`, every time.
712
+ - **Where the browser has none, the declaration is dropped rather than the animation.** It then runs on the
713
+ document timeline over whatever duration it was given. Make the end state the resting state — with
714
+ `animationFillMode="both"` and a sequence that ends where the element belongs, a missing timeline degrades
715
+ to "already arrived" rather than to a loop. Chrome/Edge 115+, Safari 26+; Firefox is behind a flag.
716
+
717
+ ---
718
+
719
+ ### View transitions
720
+
721
+ The browser screenshots the page, runs the update, screenshots again, and cross-fades between the two —
722
+ so a change that was a jump becomes a transition with nothing animated by hand.
723
+
724
+ | Prop / API | What it does |
725
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
726
+ | `viewTransitionName` | Names this element, so it animates _from where it was to where it is_ instead of being cross-faded with the page. A `<custom-ident>`, **not** dashed |
727
+ | `viewTransitionClass` | A name shared by several elements, so `::view-transition-group(.card)` styles them together. Space-separated for more than one |
728
+ | `Box.viewTransition(update, options)` | Runs `update` inside `document.startViewTransition` where the browser has one, and plainly where it has not |
729
+ | `<Box.Theme viewTransition>` | That call around a theme change, `flushSync` included |
730
+
731
+ ```tsx
732
+ const handle = Box.viewTransition(() => flushSync(() => setTab(next)), { types: ['forward'] });
733
+ await handle.finished;
734
+ ```
735
+
736
+ ```ts
737
+ interface ViewTransitionOptions {
738
+ /** `'skip'` (default) runs the update with no transition under reduced motion; `'play'` transitions anyway. */
739
+ reducedMotion?: 'skip' | 'play';
740
+ /** Types `:active-view-transition-type(name)` selects on, so forward and back can differ. */
741
+ types?: string[];
742
+ }
743
+
744
+ interface ViewTransitionHandle {
745
+ ready: Promise<void>;
746
+ finished: Promise<void>;
747
+ updateCallbackDone: Promise<void>;
748
+ skip(): void;
749
+ /** False when the browser had no API, or reduced motion skipped it. */
750
+ transitioned: boolean;
751
+ }
752
+ ```
753
+
754
+ **Four things to know:**
755
+
756
+ - **In React the update has to be flushed.** The browser takes its second screenshot the moment the
757
+ callback returns, and a `setState` has not rendered by then — so a hand-rolled version captures the old
758
+ state twice and nothing appears to move. `flushSync` inside the callback is the fix, and
759
+ `<Box.Theme viewTransition>` is it already done for the one change every app has.
760
+ - **A name has to be unique in the document while the transition runs.** Two elements sharing one is how a
761
+ transition silently does nothing. The prop is for the handful of names a layout has (`header`, `main`);
762
+ a name **per list item** is a rule per item that is never freed, so that one goes in
763
+ `props={{ style: { viewTransitionName: id } }}` — the exception `useAnchorPosition`'s anchor name and a
764
+ `Slider` thumb already take.
765
+ - **Reduced motion skips the transition and keeps the update.** A whole-page cross-fade is exactly the
766
+ motion the preference is about. `{ reducedMotion: 'play' }` is the opt-out, for a change that is
767
+ unreadable without one.
768
+ - **The handle is the same shape either way**, so there is one code path: with no API, `finished` is an
769
+ already-resolved promise, `skip()` a no-op and `transitioned` false. `ready` rejects when a transition is
770
+ skipped, which is not an error — branch on `finished`.
771
+
772
+ ---
773
+
645
774
  ## Pseudo-Classes, Breakpoints & Themes
646
775
 
647
776
  ```tsx
@@ -868,6 +997,7 @@ setTheme(null); // reset to system auto-detection (clears localStorage)
868
997
 
869
998
  **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
999
  **DOM**: Sets `data-theme` attribute and theme class on wrapper (local) or `document.documentElement` (global). Cleaned up on unmount.
1000
+ **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
1001
 
872
1002
  #### globalStyles — App-wide styles on `<html>`
873
1003
 
@@ -1118,24 +1248,41 @@ second argument for an element inside an `<svg>`. `<Icon>` is this hook plus tha
1118
1248
  ### Server-Side Rendering
1119
1249
 
1120
1250
  ```tsx
1121
- import { getStyles, resetStyles } from '@box-kite/react/ssg';
1251
+ import Box from '@box-kite/react';
1252
+ import { getStyleTag, resetStyles } from '@box-kite/react/ssg';
1253
+
1254
+ Box.configure({ classNames: 'stable' }); // in the server AND in the browser entry
1122
1255
 
1123
1256
  const html = renderToString(<App />); // any React server renderer
1124
- const cssString = getStyles(); // the CSS for what was just rendered
1125
- // <style id="box-kite-styles">{cssString}</style> in the document head
1257
+ const styleTag = getStyleTag(); // <style id="box-kite-styles" data-box-kite="…">…</style>, for the head
1126
1258
  resetStyles(); // reset before the next request
1127
1259
  ```
1128
1260
 
1129
- Or in one call, which injects the styles into the rendered `<head>` and resets afterwards:
1261
+ Or in one call, which injects the tag into the rendered `<head>` and resets afterwards:
1130
1262
 
1131
1263
  ```tsx
1132
1264
  import { renderToStaticMarkup } from '@box-kite/react/ssg';
1133
1265
 
1134
- const { html, styles } = renderToStaticMarkup(<App />);
1266
+ const { html, styles, styleTag } = renderToStaticMarkup(<App />);
1135
1267
  ```
1136
1268
 
1137
1269
  No DOM is needed: with no `document` in the process the engine collects CSS in memory. Sequential
1138
- requests are independent and identical markup gets identical class names.
1270
+ requests are independent and identical markup gets identical class names. `getStyles()` is the bare CSS.
1271
+
1272
+ **Adoption.** The `data-box-kite` attribute is a manifest — each rule's cascade position and a checksum
1273
+ of the classes it styles. On the first render the browser engine checks it against the sheet it parsed
1274
+ and takes the sheet over: every rule in it counts as generated, so hydration builds and inserts nothing
1275
+ for what the HTML already styled, and only rules the client adds afterwards are inserted, each in its
1276
+ cascade position. Two conditions:
1277
+
1278
+ | Condition | Why | If it does not hold |
1279
+ | --------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
1280
+ | `classNames: 'stable'` on both sides | the content-hashed names are how the browser recognises the server's rules | the sheet is left in place under no id and the engine writes its own after it |
1281
+ | the browser parsed every generated rule | an index is only trustworthy if nothing in between was dropped | same fallback |
1282
+
1283
+ Do **not** remove the server's `<style>` after hydration — it is the engine's own sheet now. Register
1284
+ the same `Box.extend()` props and `Box.keyframes()` sequences on both sides. `engine.getStyleTag()` is
1285
+ the same thing for `@box-kite/core`.
1139
1286
 
1140
1287
  ---
1141
1288
 
@@ -2138,7 +2285,7 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
2138
2285
  9. **Theme styles nest**: `theme={{ dark: { hover: { ... } } }}`
2139
2286
  10. **HTML attributes go in `props` prop**: `<Link props={{ href: '/about' }}>` not `<Link href>`. `data-*` and `aria-*` go there too — a `data-state` written at the top level typechecks and is then dropped
2140
2287
  11. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
2141
- 12. **Box is memoized** with `React.memo`
2288
+ 12. **Box is memoized** with `React.memo`, and so are `Flex`, `H1`, `Button` and the rest. A component of your own that is a Box with defaults calls `useBoxElement({ p: 4, ...props }, ref)` from its own `forwardRef` rather than returning `<Box>` — one fiber instead of two
2142
2289
  13. **`style` is top-level only** — never inside breakpoints (`sm={{ style: ... }}`), pseudo-classes, or theme objects. `css` is the one that nests: `sm={{ css: { objectPosition: 'top' } }}` is a rule
2143
2290
  14. **A CSS variable is a prop**: `vars={{ 'color-x': 'sky-500' }}` declares `--color-x` for everything inside — and a third-party chart goes in `<ChartContainer series={['revenue']}>` so it names no colour at all
2144
2291
  15. **All sizing props use divider 4** — `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight` are NOT direct pixels. `height={10}` = 2.5rem = 40px
@@ -2208,7 +2355,9 @@ the element wrapping the bars, the rows and the pager. Every callback below is a
2208
2355
  | `expandedRowKeys` / `onExpandedRowKeysChange` | `Key[]` / `(keys, { reason })` | Controlled expanded rows; reason: `expand`/`collapse` |
2209
2356
  | `globalFilterValue` / `onGlobalFilterChange` | `string` / `(value, { reason })` | Controlled global filter; reason: `filter`/`clear` |
2210
2357
  | `columnFilters` / `onColumnFiltersChange` | `ColumnFilters` / `(filters, { reason })` | Controlled column filters; reason: `filter`/`clear` |
2211
- | `onCellEditsChange` | `(edits, { reason })` | Every accepted edit, oldest first; reason: `edit`/`clear` |
2358
+ | `onCellEditsChange` | `(edits, { reason })` | Every accepted edit, oldest first; reason: `edit`/`paste`/`clear` |
2359
+ | `onRangeChange` | `(range, { reason })` | The block of cells marked, or `undefined`; reason: `select`/`extend`/`clear` |
2360
+ | `onPaste` | `(paste)` | One Ctrl+V: `{ range, applied, rejected, skipped }`, once for the whole block |
2212
2361
 
2213
2362
  `onSelectionChange(event)`, `onSortChange(key, dir)`, `onPageChange(page, size)` and
2214
2363
  `onPageSizeChange(size)` are the pre-contract spellings of the four above. They still fire; the
@@ -2242,6 +2391,8 @@ named ones carry the reason.
2242
2391
  | `noDataComponent` | `ReactNode` | `'empty'` | Custom empty state |
2243
2392
  | `footer` | `boolean \| { label? }` | `false` | A pinned row of grand totals over every column with an `aggregate`. `label` replaces the default `Total` |
2244
2393
  | `export` | `boolean \| ExportConfig` | `false` | Export buttons in the top bar: `{ csv?, xlsx?, fileName? }`. The writers load on the first press |
2394
+ | `tabNavigation` | `'none' \| 'cells'` | `'none'` | What Tab does. `'none'` is APG's single tab stop; `'cells'` is the spreadsheet reading |
2395
+ | `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
2396
 
2246
2397
  ### ColumnType
2247
2398
 
@@ -2471,6 +2622,95 @@ the same function — and an answer to an edit the user has since abandoned is d
2471
2622
  value was refused — and `datagrid.body.cell.editor` (`isPending`) and `datagrid.body.cell.error` are
2472
2623
  the editor and its message.
2473
2624
 
2625
+ ### Range selection and the clipboard
2626
+
2627
+ Every grid marks the cell its arrows carry on from — whether the pointer or the keyboard put it there —
2628
+ and the mark **survives the grid losing focus**. `Ctrl+C` on it copies that cell, with no opt-in.
2629
+ `def.rangeSelection` is the rest: a drag, or Shift with the arrow keys, marks a rectangle, and `Ctrl+C`
2630
+ writes it as the tab-separated text a spreadsheet pastes as columns.
2631
+
2632
+ ```tsx
2633
+ <DataGrid
2634
+ data={people}
2635
+ def={{
2636
+ rowKey: 'id',
2637
+ rangeSelection: true,
2638
+ columns: [
2639
+ { key: 'name', header: 'Name' },
2640
+ { key: 'country', header: 'Country' },
2641
+ { key: 'salary', header: 'Salary', align: 'end' },
2642
+ ],
2643
+ }}
2644
+ onRangeChange={(range, { reason }) => setSum(sumOf(range?.values()))}
2645
+ />
2646
+ ```
2647
+
2648
+ - **The mark is state, not a focus ring.** `:focus-visible` never matches a pointer, so a _clicked_ cell
2649
+ drew nothing (bug #64), and any ring made of focus goes out with it — leaving a copy with nothing to act
2650
+ on. It is the corner a Shift+arrow grows the block from, and after a drag it is the cell the drag ended
2651
+ on, so the ring, the tab stop and the next Shift+arrow are one cell.
2652
+ - **Extending:** drag across cells, hold Shift with the arrow keys (Home/End, Ctrl+Home/End and
2653
+ PageUp/PageDown extend too), or Shift+press to extend from wherever the block was anchored. A move with
2654
+ no modifier collapses the block onto the cell it landed on.
2655
+ - **A block is selected; one cell is a cursor.** The cells of a block carry `aria-selected` and the grid is
2656
+ `aria-multiselectable`; a lone current cell carries neither, because it has chosen nothing. The block is
2657
+ tinted _around_ the current cell, which keeps its ring — the way a sheet says where typing would land.
2658
+ - **What is copied is what a file would carry.** The clipboard is an export: a column's `exportValue` wins
2659
+ where it has one, an accepted edit wins over the row, and a group row copies its own value in the column
2660
+ that spans it. Fields are joined with tabs and rows with CRLF; a field carrying a tab, a newline or a
2661
+ quote is quoted with its quotes doubled. The copy rides the browser's own `copy` event, so text you have
2662
+ selected on the page still belongs to the page.
2663
+ - **Marking cells is a drag, so text selection is not.** A grid with `rangeSelection` on gives text
2664
+ selection up; an open editor hands it back for the value being typed. A press that lands on a widget
2665
+ belongs to the widget, and a touch is left alone entirely — that press is how the grid scrolls.
2666
+ - **`onRangeChange(range, { reason })`** reports `{ startRow, endRow, columns, values() }` or `undefined`,
2667
+ with `select` / `extend` / `clear`. Rows are **indices into the rows on screen** rather than keys,
2668
+ because a block may reach rows a datasource has not fetched — and `values()` is a call for the same
2669
+ reason, gathering the rectangle when it is asked for.
2670
+ - Style nodes: `datagrid.body.cell` gains `isCurrentCell` (the ring) and `isInRange` (the tint).
2671
+
2672
+ ### Paste: Ctrl+V, one judgement per cell
2673
+
2674
+ The block goes back the other way. `Ctrl+V` fills from the current cell — or from the block that is marked,
2675
+ where one is — and every cell it covers goes through the same `def.onCellEdit` an editor would, so a paste
2676
+ needs no second validator and no second way of writing a value. It needs no opt-in either: `rangeSelection`
2677
+ only gives it a bigger target.
2678
+
2679
+ ```tsx
2680
+ <DataGrid
2681
+ data={people}
2682
+ def={{
2683
+ rowKey: 'id',
2684
+ rangeSelection: true,
2685
+ columns: [
2686
+ { key: 'name', header: 'Name', editable: true },
2687
+ { key: 'salary', header: 'Salary', align: 'end', editable: true },
2688
+ ],
2689
+ onCellEdit: ({ columnKey, value }) => (columnKey === 'salary' && Number(value) < 0 ? 'Salary cannot be negative' : undefined),
2690
+ }}
2691
+ onPaste={({ applied, rejected, skipped }) => setReport({ applied, rejected, skipped })}
2692
+ />
2693
+ ```
2694
+
2695
+ - **Each axis takes whichever is longer, the block or the clipboard.** A block bigger than the clipboard is
2696
+ tiled with it, a clipboard bigger than the block spills past it, and both stop at the edge of the grid.
2697
+ One rule, with the degenerate case falling out of it: a paste onto the current cell alone starts from a
2698
+ block of one.
2699
+ - **A refusal skips its own cell and nothing else.** A paste is many independent judgements, and stopping at
2700
+ the first bad value would leave the block half written with no way back. The refused cells wear the red
2701
+ ring (`datagrid.body.cell`'s `isInvalid`, with no editor open behind it) and report `aria-invalid`, so the
2702
+ cells say which ones and `onPaste` says why.
2703
+ - **The clipboard carries no types, so the cell says how to read the text.** A number column parses it and
2704
+ refuses what is not a number, a checkbox takes `true`/`false` (and `1`/`0`), and a `select` keeps to its
2705
+ own options and holds the option's value rather than its spelling. A value that did not change is not an
2706
+ edit at all, so pasting a column back over itself costs nothing.
2707
+ - **A paste that reaches an open editor belongs to the editor** — that is a value being typed, not a block
2708
+ being filled — and a cell that cannot be edited at all is skipped before anything is asked about it, so a
2709
+ validator only ever hears about cells that could have taken a value.
2710
+ - **`onPaste({ range, applied, rejected, skipped })`** fires once for the whole block, after the values have
2711
+ landed, so `range.values()` reads what the grid now shows. Everything it accepted lands in the same
2712
+ stream a typed value does: `onCellEditsChange(edits, { reason: 'paste' })`.
2713
+
2474
2714
  ### ContextMenuConfig
2475
2715
 
2476
2716
  | Prop | Type | Default | Description |
@@ -2802,3 +3042,483 @@ Also accepts: `data` (TRow[]), `value`/`defaultValue`, `label`/`labelProps`, `mu
2802
3042
  3. **CSS variables**: In `:root` rules
2803
3043
  4. **Theme issues**: Ensure `<Box.Theme>` wraps your app
2804
3044
  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
3045
+ 6. **Read the console first**: outside a production build the engine prints one `[box-kite]` warning per mistake that would otherwise be silent, each naming the fix:
3046
+
3047
+ | You wrote | The warning says |
3048
+ | -------------------------- | -------------------------------------------------------------------------- |
3049
+ | `bgColor="bleu-500"` | wrote no CSS … Did you mean `"blue-500"`? |
3050
+ | `opacity={50}` | wrote no CSS … It takes one of 0, 0.1, … 1 |
3051
+ | `<Link href="/about">` | `href` is an HTML attribute … write `props={{ href: ... }}` |
3052
+ | `fontSize={4}` | is 4px text … For 16px write `fontSize={16}` |
3053
+ | `lineHeight={1.5}` | is a 1.5px line … `lineHeight="font-size"`, or `css={{ lineHeight: 1.5 }}` |
3054
+ | `transitionDuration={0.3}` | is 0.3ms … For 0.3s write `transitionDuration={300}` |
3055
+ | `brightness={1.1}` | is 1.1% … For 110% write `brightness={110}` |
3056
+
3057
+ Once per value, and `Box.configure({ warnings: false })` silences them. In production a rejected value is dropped with nothing said, which is why `check_styles` (`@box-kite/mcp`) exists
3058
+
3059
+ ---
3060
+
3061
+ ---
3062
+
3063
+ ## Dashboard (`components/dashboard`)
3064
+
3065
+ A grid of widgets people rearrange, and a layout a model can write. Two components and one artifact:
3066
+ `<DashboardGrid>` places, `<Widget>` is the chrome, and the layout is plain JSON in cells.
3067
+
3068
+ ```tsx
3069
+ import DashboardGrid, { DashboardUtils, Widget } from '@box-kite/react/components/dashboard';
3070
+ import { Sparkline } from '@box-kite/react/components/chart';
3071
+
3072
+ <DashboardGrid layout={layout} onLayoutChange={setLayout} onLayoutCommit={save} editable label="Sales">
3073
+ <Widget id="revenue" title="Revenue" description="Last 12 weeks">
3074
+ <Sparkline data={revenue} variant="area" width="100%" height="100%" />
3075
+ </Widget>
3076
+ <Widget id="orders" title="Orders" onRefresh={reload} loading={pending} />
3077
+ </DashboardGrid>;
3078
+ ```
3079
+
3080
+ ### The layout
3081
+
3082
+ ```jsonc
3083
+ {
3084
+ "version": 1,
3085
+ "columns": 12,
3086
+ "items": [
3087
+ { "id": "revenue", "x": 0, "y": 0, "w": 6, "h": 2 },
3088
+ { "id": "orders", "x": 6, "y": 0, "w": 6, "h": 2, "minW": 3, "fixed": false },
3089
+ ],
3090
+ }
3091
+ ```
3092
+
3093
+ `x`/`y` are zero-based cells, `w`/`h` spans. `minW`/`minH`/`maxW`/`maxH` bound a resize, and `fixed`
3094
+ pins a widget: it is never moved or resized and has no handles at all. The layout is **compacted
3095
+ upward**, so a widget cannot be parked in mid-air and two dashboards of the same widgets in the same
3096
+ places compare equal. A widget dropped on a neighbour **takes the cell**, and the neighbour is handed one
3097
+ of its own — the row above where there is room for it, the row below otherwise. That is `moveTo`'s half
3098
+ of it; compaction is reading order and nothing else.
3099
+
3100
+ ### DashboardGridProps
3101
+
3102
+ | Prop | Type | What it does |
3103
+ | -------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------- |
3104
+ | `layout` / `defaultLayout` | `DashboardLayout` | Controlled and uncontrolled. Normalized on the way in, so a generated one need not be tidy. |
3105
+ | `onLayoutChange` | `ChangeHandler<DashboardLayout, DashboardReason>` | Every cell a drag crosses, and every arrow key. |
3106
+ | `onLayoutCommit` | `ChangeHandler<DashboardLayout, DashboardReason>` | Once when the interaction ends — the one to write to a server. |
3107
+ | `columns` | `number \| Partial<Record<DashboardSize, number>>` | One count, or one per container size. Default `{ xs: 1, md: 6, xxl: 12 }`. |
3108
+ | `rowHeight` | `number` | The height of one row, ÷4 scale. Default `24` (6rem). |
3109
+ | `editable` | `boolean` | The edit/view split: no handles and nothing in the tab order without it. |
3110
+ | `label` / `labelledBy` | `string` | Names the `role="list"` of widgets. |
3111
+
3112
+ `DashboardReason` is `'move' | 'resize' | 'imperative'` — which _device_ did it is in `details.event`.
3113
+
3114
+ ### WidgetProps
3115
+
3116
+ | Prop | Type | What it does |
3117
+ | ----------------------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
3118
+ | `id` | `string` | The layout item it fills. Without one it is a card with the same chrome. |
3119
+ | `title` / `description` | `ReactNode` | The title bar. `title` is a real heading at `level` (default `3`). |
3120
+ | `name` | `string` | What the handles and the announcements call it. Defaults to a string `title`, then `id`. |
3121
+ | `actions` | `ReactNode` | The end of the title bar — a menu, a filter, a link out. |
3122
+ | `loading` / `error` / `empty` | `boolean` / `ReactNode` / … | The three states that replace the content; anything else renders the children. |
3123
+ | `onRefresh` / `refreshLabel` | `() => void` / `string` | A refresh button in the bar, and the retry an error state offers. |
3124
+
3125
+ ### Placement is a class; only a drag is an inline style
3126
+
3127
+ A widget's cell is `gridColumnStart`/`gridColumnEnd`/`gridRowStart`/`gridRowEnd` — props, so a shared
3128
+ class — which is why a resting dashboard measures nothing and renders on a server. The one inline style
3129
+ in the component is the translate that keeps a dragged widget under the pointer.
3130
+
3131
+ `columns` as a record projects the one layout into each declared space by arithmetic, written as a
3132
+ container query. **A grid cannot container-query itself** (the query resolves against an ancestor
3133
+ container), so every projection is drawn on the widest arrangement's tracks. And an arrangement can only
3134
+ be edited in the space it is written in: where a projection is showing, the handles are not rendered.
3135
+
3136
+ ### The keyboard, and what it says
3137
+
3138
+ Both handles are real buttons. Enter or Space picks the widget up (`aria-pressed`), the arrows move or
3139
+ resize it a cell at a time following the **reading order**, Enter drops it and Escape puts it back — the
3140
+ whole layout with it. A grab that loses focus is cancelled rather than dropped. Every step is announced
3141
+ in a polite live region that exists before there is anything to say.
3142
+
3143
+ ### The model (`DashboardUtils`)
3144
+
3145
+ Framework-free, exported from the same module:
3146
+
3147
+ ```ts
3148
+ DashboardUtils.SCHEMA; // the layout as JSON Schema — the subset catalog() emits
3149
+ DashboardUtils.parse(value); // { layout, issues } from a database, a file or a model
3150
+ DashboardUtils.add(layout, { id: 'latency', w: 3, h: 2 }); // placed under everything, then floated up
3151
+ DashboardUtils.remove(layout, 'latency');
3152
+ DashboardUtils.moveTo(layout, 'revenue', 3, 0); // what a drag does
3153
+ DashboardUtils.resizeTo(layout, 'revenue', 6, 3);
3154
+ DashboardUtils.project(layout, 6); // the same layout in a narrower space
3155
+ ```
3156
+
3157
+ `parse` clamps what is out of range (a generated `w: 0`, a column count of 400), drops what it cannot
3158
+ read and reports it rather than throwing. Where a dashboard is **kept** is the app's decision.
3159
+
3160
+ ### Style tree
3161
+
3162
+ `dashboard` (+ `placeholder`) and `widget` (+ `header`, `label`, `title`, `description`, `actions`,
3163
+ `handle`, `body`, `message`, `retry`, `skeleton`). Both handles are the one `handle` node, whose
3164
+ `corner` variant is the resize one; the row height is a variable (`vars={{ 'dashboard-row': '8rem' }}`),
3165
+ so it can be set per breakpoint or per theme.
3166
+
3167
+ ## Agent chrome (`components/agent`)
3168
+
3169
+ The parts of an agent's turn that are not prose — what it ran, what it was thinking, what it wants
3170
+ permission to do — plus the text it answers with. One entry, four components, and a framework-free model
3171
+ behind them.
3172
+
3173
+ ```tsx
3174
+ import { AgentUtils, ApprovalCard, Reasoning, StreamingText, ToolCallCard } from '@box-kite/react/components/agent';
3175
+
3176
+ <Reasoning duration={1400}>{reasoningText}</Reasoning>
3177
+ <ToolCallCard name="searchOrders" status="success" input={{ orderId: 4182 }} output={{ total: 6400 }} />
3178
+ <ApprovalCard
3179
+ title="Refund order 4182"
3180
+ description="6,400 MDL back to the customer. This cannot be undone."
3181
+ input={{ orderId: 4182, amount: 6400 }}
3182
+ onDecisionChange={(decision) => respond(decision === 'approved')}
3183
+ />;
3184
+ ```
3185
+
3186
+ ### ToolCallCard
3187
+
3188
+ | Prop | Type | What it does |
3189
+ | --------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------- |
3190
+ | `name` | `ReactNode` | The tool, as the model called it. Required, and what the header reads. |
3191
+ | `description` | `ReactNode` | A line under the name, in the app's own words. |
3192
+ | `status` | `'pending' \| 'running' \| 'success' \| 'error'` | Default `pending`. Every state carries its own **word** beside the dot. |
3193
+ | `input` / `output` / `error` | `unknown` | Shown under `inputLabel`/`outputLabel`/`errorLabel`, formatted rather than trusted. |
3194
+ | `open` / `defaultOpen` / `onOpenChange` | `boolean` / … / `ChangeHandler<boolean, 'trigger'>` | The body's state. Closed by default. |
3195
+ | `collapsible` | `boolean` | Default `true`. `false` leaves the header a plain row. |
3196
+ | `valueLimit` | `number` | Characters before a value is cut. Default 20,000. |
3197
+
3198
+ The four statuses map one for one onto what every runtime reports under its own spelling — AI SDK's
3199
+ `input-streaming` / `input-available` / `output-available` / `output-error` — so wiring a message part
3200
+ to a card is a lookup rather than a state machine. `aria-busy` while running.
3201
+
3202
+ **A card with nothing to disclose renders no control at all**: the header is a row of text rather than a
3203
+ button, because a control that opens nothing is a tab stop nobody wants to land on.
3204
+
3205
+ ### ApprovalCard
3206
+
3207
+ | Prop | Type | What it does |
3208
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
3209
+ | `title` | `ReactNode` | Required. Names the `role="group"`. |
3210
+ | `description` | `ReactNode` | The consequence, and whether it can be undone. |
3211
+ | `input` / `inputLabel` | `unknown` / `ReactNode` | The call being gated, shown as a tool card shows its input. |
3212
+ | `decision` / `defaultDecision` / `onDecisionChange` | `'approved' \| 'rejected' \| null` / … / `ChangeHandler<…, 'approve' \| 'reject'>` | The one channel. |
3213
+ | `busy` | `boolean` | The answer is on its way: both buttons disabled, `aria-busy` on the card. |
3214
+ | `approveLabel` / `rejectLabel` | `ReactNode` | Default `Approve` / `Reject`. |
3215
+ | `autoFocus` | `boolean` | Default `false`. Lands on **Reject** — the least destructive action. |
3216
+
3217
+ `onDecisionChange(decision, { reason })` maps straight onto AI SDK 6's `needsApproval`, CopilotKit's
3218
+ `renderAndWaitForResponse` and AG-UI's `INTERRUPT`. Once there is a decision the buttons are gone, and
3219
+ the answer lands in a `role="status"` **that was in the DOM before there was anything in it** — a live
3220
+ region inserted together with its text is not reliably announced.
3221
+
3222
+ ### Reasoning
3223
+
3224
+ | Prop | Type | What it does |
3225
+ | --------------------------------------- | ----------- | --------------------------------------------------------------------------------- |
3226
+ | `label` | `ReactNode` | Default `Reasoning`, or `Thinking…` while `streaming`. |
3227
+ | `streaming` | `boolean` | Still arriving: the header says so, shimmers, and reports `aria-busy`. |
3228
+ | `duration` | `number` | Milliseconds. The header reads "Thought for 4.2s" once it is over. |
3229
+ | `open` / `defaultOpen` / `onOpenChange` | … | Closed by default. Auto-opening while streaming is the app's: `open={streaming}`. |
3230
+
3231
+ It opens in the same one-row grid an `Accordion` panel does — a track running `1fr` to `0fr`, so nothing
3232
+ is measured and the rules are shared with every other disclosure on the page.
3233
+
3234
+ ### The model (`AgentUtils`)
3235
+
3236
+ Framework-free, exported from the same module — a tool's arguments are JSON a **model** invented, and
3237
+ `JSON.stringify` answers a circular structure, a `BigInt` and a four-megabyte result with a throw, a
3238
+ throw and a frozen frame:
3239
+
3240
+ ```ts
3241
+ AgentUtils.formatValue(part.output, 4000); // { text, truncated } — capped, never throws
3242
+ AgentUtils.hasValue(part.input); // `0` and `''` are values; `null` and `undefined` are not
3243
+ AgentUtils.formatCount(3980); // '3,980' — grouped here rather than by locale, so a prerender matches
3244
+ AgentUtils.formatDuration(4200); // '4.2s'
3245
+ AgentUtils.statusLabel('success'); // 'Done'
3246
+ AgentUtils.decisionLabel('approved'); // 'Approved'
3247
+ ```
3248
+
3249
+ ### Style tree
3250
+
3251
+ `toolCall` (+ `header`, `summary`, `name`, `description`, `status` with `status.dot`, `arrow`, `track`,
3252
+ `clip`, `body`, `section`, `label`, `value`, `truncated`), `approval` (+ `title`, `description`,
3253
+ `section`, `label`, `value`, `truncated`, `footer`, `actions`, `button`, `decision`) and `reasoning`
3254
+ (+ `trigger`, `arrow`, `duration`, `track`, `clip`, `body`). The status is a variant on
3255
+ `toolCall.status`, the decision one on `approval` itself, and `toolCall.header`'s `interactive` variant
3256
+ is what makes it look pressable — so a card with nothing to open does not.
3257
+
3258
+ ### StreamingText
3259
+
3260
+ What the agent _says_, as it arrives. Same entry.
3261
+
3262
+ | Prop | Type | What it does |
3263
+ | ----------- | --------- | ------------------------------------------------------------------------------------ |
3264
+ | `text` | `string` | The message **so far**, not the delta — knowing what is new means comparing the two. |
3265
+ | `streaming` | `boolean` | More is coming: the caret shows and the element reports `aria-busy`. |
3266
+ | `window` | `number` | How many arrived runs stay faded at once. Default 8; `0` turns the entrance off. |
3267
+ | `caret` | `boolean` | Default `true`. |
3268
+
3269
+ ```tsx
3270
+ <StreamingText text={message} streaming={status === 'streaming'} />
3271
+ ```
3272
+
3273
+ What is on the page is one settled string plus the last few runs to arrive, so a message that is already
3274
+ whole paints at once with nothing animating — a prerendered page, a transcript read back — and one still
3275
+ arriving costs the same at the ten-thousandth token as at the first. Anything but an append (a
3276
+ regenerate, an edit) settles at once rather than fading in the part that differs.
3277
+
3278
+ The entrance is `@starting-style`, not a keyframe, so it rides `--transitionTime` and disappears under
3279
+ `prefers-reduced-motion` with nothing declared for it; the caret is the `pulse` preset. It is **not** a
3280
+ live region — one announcing every token reads the message out a word at a time and again when it
3281
+ finishes — so what announces the turn is the transcript it lands in. Tree: `streamingText` (+ `segment`,
3282
+ `caret`).
3283
+
3284
+ `AgentUtils.initialStream`, `advanceStream` and `streamText` are the model behind it, framework-free and
3285
+ pure: advancing is keyed on the text rather than on a counter, so doing it twice is doing it once.
3286
+
3287
+ ## Markdown (`components/markdown`)
3288
+
3289
+ `markdownComponents` is the `components` map `react-markdown`, Streamdown and everything built on that
3290
+ shape already take, with this engine's classes on it — **not** a renderer, because a parser is a choice
3291
+ an app has usually already made, and wrapping Streamdown would ask the project for a Tailwind `@source`
3292
+ line and shadcn's design tokens in a global stylesheet.
3293
+
3294
+ ```tsx
3295
+ import Markdown from 'react-markdown';
3296
+ import { markdownComponents } from '@box-kite/react/components/markdown';
3297
+
3298
+ <Box component="markdown">
3299
+ <Markdown components={markdownComponents}>{message}</Markdown>
3300
+ </Box>;
3301
+ ```
3302
+
3303
+ Three things to know. It is a **constant, not a factory**: a map built inside render is a new set of
3304
+ component _types_ every token, which React answers by unmounting the whole message and mounting it again
3305
+ — override by spreading at module scope (`{ ...markdownComponents, h1: MyHeading }`). Whether a URL is
3306
+ safe stays the **renderer's** (`urlTransform`, `defaultUrlTransform`), because the href is parsed before
3307
+ a component is called; the map sets `rel="noreferrer"`. And wrapping in `<Box component="markdown">` is
3308
+ what gives the block its size and colour, which the nodes inherit.
3309
+
3310
+ Covers `h1`–`h6`, `p`, `a`, `ul`, `ol`, `li`, `blockquote`, `code`, `pre`, `hr`, `img`, `table`,
3311
+ `thead`, `tbody`, `tr`, `th`, `td`, `strong`, `em`, `del` and a task list's `input`. Tree: `markdown`
3312
+ (+ `heading` with `level1`–`level6`, `paragraph`, `link`, `list`, `item`, `quote`, `code`, `codeBlock`
3313
+ with `language` and `code`, `rule`, `image`, `tableWrap`, `table`, `row`, `cell`, `inline`, `checkbox`).
3314
+
3315
+ ## Skeleton (`components/skeleton`)
3316
+
3317
+ Where content will be, while it is being fetched.
3318
+
3319
+ | Prop | Type | What it does |
3320
+ | -------- | ----------- | ---------------------------------------------------------------------------- |
3321
+ | `lines` | `number` | How many bars. Default 1; past that the last one is short. |
3322
+ | `circle` | `boolean` | One round bar — an avatar. `width` sets its size and the height follows. |
3323
+ | `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`. |
3324
+
3325
+ ```tsx
3326
+ <Skeleton lines={3} label="Loading orders" />
3327
+ <Skeleton circle width={10} />
3328
+ ```
3329
+
3330
+ A `label` belongs on the one skeleton standing for a region and not on each bar — a screen of
3331
+ placeholders each announcing itself is a screen nobody can listen to, and a region inserted together
3332
+ with its text is not reliably announced anyway, so what it really buys is words for somebody who lands
3333
+ there. The gloss is a named duration in milliseconds, so it sits outside what `--transitionTime` zeroes
3334
+ and stops itself under `prefers-reduced-motion`. It renders on a server. Tree: `skeleton` (+ `bar` with
3335
+ `short` and `circle` variants, `gloss`).
3336
+
3337
+ ## The catalog (`@box-kite/react/catalog`)
3338
+
3339
+ What a **generated** UI is allowed to build, as JSON Schema — the runtime counterpart to everything
3340
+ above, which is for whatever writes the code at dev time. A generative-UI runtime (json-render,
3341
+ assistant-ui, CopilotKit, A2UI) takes a component catalog; this is that catalog, read off the live prop
3342
+ registry rather than written down.
3343
+
3344
+ ```ts
3345
+ import { catalog } from '@box-kite/react/catalog';
3346
+
3347
+ const allowed = catalog({ include: ['Flex', 'H2', 'P', 'Sparkline'], styleProps: ['d', 'gap', 'p', 'bgColor', 'fontSize'] });
3348
+
3349
+ allowed.components.Flex.props; // a strict JSON Schema: additionalProperties false, colours patterned to the palette
3350
+ allowed.rules; // the dividers — what a schema cannot state and a prompt must
3351
+ allowed.tokens; // the colours, the @keyframes names and the style-tree nodes a host may restyle
3352
+ ```
3353
+
3354
+ - **The engine says what a prop accepts, the manifest says what it means.** Values come off the live
3355
+ registry, so a `Box.extend()` prop or colour is in the catalog with nothing regenerated — call
3356
+ `catalog()` after the `extend()`. Prose is generated from the same JSDoc as the prop reference.
3357
+ - **A colour prop is a `pattern` over the palette**, so a generated tree cannot invent `#ff00ff`. Every
3358
+ closed value list is an `enum`; a prop the registry leaves open is `{ type: 'string' }` with its
3359
+ values as `examples`, never a constraint the library does not enforce.
3360
+ - **Function props are `events`** (named, for the host to bind) and a `ReactNode` prop is a **slot**
3361
+ (`children` is `default`). A prop a JSON spec cannot express is left out rather than half-described.
3362
+ - **The props that are a _shape_ are described by hand**, since the other half of each is React:
3363
+ `DataGrid`'s `def` (required — the columns and the grid-wide flags) and `data`, `DashboardGrid`'s
3364
+ `layout`/`defaultLayout`/`columns`, `Widget`'s `empty`, `ChartContainer`'s `series`. A column's nested
3365
+ `columns`, and `dataSource`/`onCellEdit`/`rowDetail`/`treeData`, stay out — they are the app's to pass.
3366
+ - **The allow-list is the app's.** `catalog()` with no options is all 78 components and all 235 props —
3367
+ ~4.6 MB serialized. `include`, `exclude` and `styleProps` are how an app narrows it.
3368
+
3369
+ Full reference: `docs/catalog.md`.
3370
+
3371
+ ---
3372
+
3373
+ ## The spec renderer (`@box-kite/react/spec`)
3374
+
3375
+ The other half of the catalog: the catalog says what a model may write, this renders what it wrote.
3376
+ Nothing in it imports Box — the components are the app's, handed over in a registry — so the entry
3377
+ carries no engine.
3378
+
3379
+ ```tsx
3380
+ import { catalog } from '@box-kite/react/catalog';
3381
+ import Flex from '@box-kite/react/components/flex';
3382
+ import { H2, P } from '@box-kite/react/components/semantics';
3383
+ import SpecRenderer, { createSpecRegistry } from '@box-kite/react/spec';
3384
+
3385
+ const allowed = catalog({ include: ['Flex', 'H2', 'P'], styleProps: ['d', 'gap', 'p', 'bgColor', 'fontSize'] });
3386
+ const registry = createSpecRegistry({ catalog: allowed, components: { Flex, H2, P } });
3387
+
3388
+ <SpecRenderer spec={spec} registry={registry} data={data} onAction={(action, details) => run(action, details)} />;
3389
+ ```
3390
+
3391
+ A node is JSON, and these are all the fields it has:
3392
+
3393
+ | Field | What it is |
3394
+ | ---------- | -------------------------------------------------------------------------------------------- |
3395
+ | `type` | The component. A name the registry does not hold renders nothing. |
3396
+ | `props` | Everything that component's own schema allows. A prop it refuses is dropped, not the node. |
3397
+ | `children` | The default slot: nodes, text, or `{ $data }` references to text. |
3398
+ | `slots` | A named slot (`Tooltip`'s `content`, a control's `label`). |
3399
+ | `on` | An event the catalog lists, bound to an action name: `{ onClick: 'refresh' }`. |
3400
+ | `repeat` | A reference to an array. The node renders once per item, with `$item` and `$index` in scope. |
3401
+ | `key` | React's key, where a node's identity has to survive a re-order. |
3402
+
3403
+ ```jsonc
3404
+ {
3405
+ "type": "Flex",
3406
+ "props": { "d": "column", "gap": 4, "bgColor": "sky-500/10" },
3407
+ "children": [
3408
+ { "type": "H2", "props": { "fontSize": 24 }, "children": ["Revenue"] },
3409
+ { "type": "P", "children": [{ "$data": "summary" }] },
3410
+ ],
3411
+ }
3412
+ ```
3413
+
3414
+ - **The registry is the allow-list.** `createSpecRegistry({ catalog, components })` pairs an
3415
+ implementation with the catalog's rules for it; a component of your own takes `{ component, props,
3416
+ slots, events }` with its own JSON Schema, and one registered **without** a schema takes no props at
3417
+ all.
3418
+ - **Only an `event` can become a function.** `on` binds nothing else, and what an action does is the
3419
+ host's — `onAction(action, { payload, args, prop, component, path })`. There is no tag from the
3420
+ spec, no `eval` and no `dangerouslySetInnerHTML`.
3421
+ - **A reference resolves before it is validated.** `{ $data: 'stats.revenue' }` (a dot path or a JSON
3422
+ Pointer), `{ $item: 'label' }` and `{ $index: true }` read from `data`, own properties only, and the
3423
+ resolved value is what the schema judges.
3424
+ - **A partial spec is the normal case.** A node with no `type` yet renders nothing and reports nothing;
3425
+ a half-written value fails its schema and is dropped until it is whole.
3426
+ - **One node cannot take the tree down.** Every node has an error boundary; `fallback` is what stands
3427
+ in its place (nothing, by default), and `maxNodes` (1,000) and `maxDepth` (32) end a runaway tree.
3428
+ - **Everything refused is reported.** `onIssues` gets a `{ code, path, message, component, prop }` per
3429
+ render whose issues changed: `unknown-component`, `unknown-prop`, `invalid-prop`, `unknown-slot`,
3430
+ `unknown-event`, `unresolved-data`, `missing-prop`, `invalid-child`, `invalid-repeat`, `too-deep`,
3431
+ `too-many-nodes`, `render-error`.
3432
+ - **A node missing a prop it cannot do without is held back.** A required prop that has not arrived —
3433
+ or was refused — would be `undefined` inside the component, so the node renders nothing and reports
3434
+ `missing-prop` instead of throwing into its own boundary once a frame.
3435
+ - **A generated node writes the controlled prop, never the `default…` twin.** `layout` not
3436
+ `defaultLayout`, `value` not `defaultValue`: React reads an uncontrolled default once, so the frame
3437
+ it first arrived whole in is the one that sticks, and a second spec rendered in the same place keeps
3438
+ the first one's state.
3439
+ - **What a node has been shown with, it is not stripped of.** A stream is not monotone — a column's
3440
+ `align` passes through `"e"` on its way to `"end"`, and that takes the whole `def` with it — so
3441
+ `<SpecRenderer>` keeps the last value each node was given for a prop it cannot render without. It
3442
+ remembers while it is mounted; `key` it to forget. Gating a heavy component is the app's call:
3443
+ point its name at a placeholder in the registry while the spec is arriving.
3444
+
3445
+ `specSchema(source, { bindings, root })` is the other direction: one JSON Schema for a whole tree, for
3446
+ `streamObject`, a structured-output API or `z.fromJSONSchema`. **Import it from
3447
+ `@box-kite/react/catalog` in a route handler** — it is exported there too, and that entry renders
3448
+ nothing, where `/spec` carries a `use client` banner; it takes a `catalog()` as readily as a registry,
3449
+ so the server half of the loop needs no components. `renderSpec(spec, options)` is the same walk with
3450
+ no hook in it — it returns `{ element, issues }`, so a static spec renders on a server.
3451
+
3452
+ Full reference: `docs/catalog.md`.
3453
+
3454
+ ## Ecosystem interop (`@box-kite/react/interop`)
3455
+
3456
+ The agentic runtimes' shapes and this library's, mapped onto each other. It **imports none of them**:
3457
+ an adapter that pulled in a runtime would be choosing one for the app. Framework-free and engine-free,
3458
+ so a route handler and a client both import from it.
3459
+
3460
+ ```ts
3461
+ import { toolPart, applyToolEvent } from '@box-kite/react/interop';
3462
+ import { a2uiApply, a2uiEmpty, a2uiSurface, a2uiToSpec, a2uiCatalog, a2uiComponentSchema } from '@box-kite/react/interop';
3463
+ import { fromGenerativeUi, toGenerativeUi } from '@box-kite/react/interop';
3464
+ ```
3465
+
3466
+ `toolPart(part)` reads a part from AI SDK, assistant-ui or CopilotKit and answers
3467
+ `{ kind, status, decision, input, output, error }` — `kind` is `'call'` or `'approval'`, `status` is
3468
+ the `pending`/`running`/`success`/`error` `<ToolCallCard>` takes, and `null` means it is not a tool part
3469
+ at all. **A decision is not a stage a call passes through, it is a question somebody answers**, which is
3470
+ why two components cover six states:
3471
+
3472
+ | Runtime | Call | Question |
3473
+ | ------------ | --------------------------------------------------------------------- | ----------------------------------------- |
3474
+ | AI SDK 6/7 | `input-streaming` `input-available` `output-available` `output-error` | `approval-requested` `approval-responded` |
3475
+ | assistant-ui | `status.type` `running` `complete` `incomplete` | `requires-action`, under either reason |
3476
+ | CopilotKit | `inProgress` `executing` `complete` | `executing` holding a `respond` |
3477
+ | AG-UI | `TOOL_CALL_START` `_ARGS` `_END` `_RESULT` | `INTERRUPT` |
3478
+
3479
+ **AG-UI reports events, not parts**, so its cards are a fold rather than a mapping:
3480
+ `applyToolEvent(parts, event)`, one call per event, oldest first, keyed by `toolCallId`.
3481
+
3482
+ ### A2UI
3483
+
3484
+ The only genuinely different shape here: a **flat adjacency list** of components referring to each other
3485
+ by id, arriving one message at a time, with a data model of its own per surface.
3486
+
3487
+ ```tsx
3488
+ const [state, setState] = useState(a2uiEmpty);
3489
+ // v0.8 and v0.9 both. Pure, and the same state back when a message changed nothing.
3490
+ const next = (message: unknown) => setState((current) => a2uiApply(current, message));
3491
+ const surface = a2uiSurface(state);
3492
+
3493
+ <SpecRenderer spec={a2uiToSpec(surface, { catalog })} registry={registry} data={surface?.data} onAction={run} />;
3494
+ ```
3495
+
3496
+ - **Its data binding is already `$data`.** `{ "path": "/user/email" }` is a JSON Pointer, which `$data`
3497
+ has always taken — a rename, not a parse. The surface's own data model goes in `<SpecRenderer data>`.
3498
+ - **A template is a `repeat`.** `children: { componentId, path }` is one node per item of an array.
3499
+ - **A half-arrived surface is the ordinary case.** A component nothing reaches from the root is not
3500
+ rendered yet; a cycle in the id graph is cut rather than walked.
3501
+ - **`id`, `component`, `child`, `children` and `action` never reach the registry as props.** `action`
3502
+ becomes an `on` binding under that name, so an app registering Box components under A2UI's own names
3503
+ declares `events: ['action']` on them. Pass `{ catalog }` instead where the agent generated against
3504
+ this library, and a prop the catalog calls an event is bound as one.
3505
+
3506
+ `a2uiCatalog(catalog(), { catalogId })` is the other direction — `catalog()` as an A2UI catalog
3507
+ document, where a component carries its own `id`, its type is a property rather than the key above it,
3508
+ and its children are **ids**. Serve it at the `catalogId` you gave it.
3509
+
3510
+ ### assistant-ui, CopilotKit, json-render
3511
+
3512
+ `fromGenerativeUi(spec)` reads their `GenerativeUISpec` (`component` where this says `type`, a bare
3513
+ string child that renders as text) and `toGenerativeUi(node)` goes back, returning `{ spec, losses }`:
3514
+ their nodes carry no data binding, no repeat and no action channel, so what could not come along is
3515
+ **reported** rather than dropped in silence. Tool UI is `makeAssistantToolUI` with the cards in it.
3516
+
3517
+ CopilotKit is two surfaces. Its A2UI renderer takes Zod, so `z.fromJSONSchema` over the catalog document
3518
+ is the bridge — and **a `$ref` resolves against the document, not against the piece you lifted out of
3519
+ it**, so `a2uiComponentSchema(document, name)` is what to convert (measured: zod 4.6 throws
3520
+ `Reference not found: #/$defs/color` otherwise). Its actions take a render function, where `render` is a
3521
+ call being shown and `renderAndWaitForResponse` is a question whose `respond` is what
3522
+ `onDecisionChange` calls. json-render is the same `z.fromJSONSchema` over `catalog()` itself.
3523
+
3524
+ Full reference: `docs/interop.md`.