@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
@@ -0,0 +1 @@
1
+ Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("../rolldown-runtime.cjs"),t=require("./visuallyHidden.cjs");let n=require("react/jsx-runtime"),r=require("@box-kite/react");r=e.t(r,1);function i(e){let{lines:i=1,circle:a=!1,label:o,props:s,...c}=e,l=a?1:Math.max(1,Math.trunc(i));return(0,n.jsxs)(r.default,{component:`skeleton`,...c,props:{...o===void 0?{"aria-hidden":!0}:{role:`status`},...s},children:[Array.from({length:l},(e,t)=>(0,n.jsx)(r.default,{component:`skeleton.bar`,variant:{circle:a,short:!a&&l>1&&t===l-1},children:(0,n.jsx)(r.default,{component:`skeleton.gloss`})},t)),o!==void 0&&(0,n.jsx)(t.default,{children:o})]})}exports.default=i;
@@ -0,0 +1,34 @@
1
+ import { ReactNode } from 'react';
2
+ import { BoxProps } from '../box';
3
+ import { ComponentsAndVariants } from '@box-kite/core/types';
4
+ export interface SkeletonProps<TKey extends keyof ComponentsAndVariants = 'skeleton'> extends BoxProps<'div', TKey> {
5
+ /** How many bars to draw. Default `1`; past that the last one is short, the way a paragraph's last line is. */
6
+ lines?: number;
7
+ /** A round placeholder — an avatar, a thumbnail. `width` sets its size and the height follows. */
8
+ circle?: boolean;
9
+ /**
10
+ * What is loading, for a reader. It makes the skeleton a `role="status"` with the words in it, so put
11
+ * it on the one skeleton that stands for a region and leave it off the rest — a screen of placeholders
12
+ * each announcing itself is a screen nobody can listen to.
13
+ */
14
+ label?: ReactNode;
15
+ }
16
+ /**
17
+ * Where content will be, while it is being fetched.
18
+ *
19
+ * ```tsx
20
+ * <Skeleton lines={3} label="Loading orders" />
21
+ * <Skeleton circle width={10} />
22
+ * ```
23
+ *
24
+ * **Decoration by default**: with no `label` the whole thing is `aria-hidden`, because a reader told
25
+ * "three empty bars" has been told nothing. What announces a load is the content arriving in a region the
26
+ * page already had — which is also why a `label` is what somebody hears on *landing* rather than a
27
+ * guaranteed announcement: a live region inserted together with its text is not reliably read out.
28
+ *
29
+ * The gloss crossing each bar is a named duration, so it sits outside what `--transitionTime` zeroes and
30
+ * stops itself under `prefers-reduced-motion`. It renders on a server: no state, no effect, no measurement.
31
+ *
32
+ * @a11y `aria-hidden` unless `label` is given, and then `role="status"` naming what is on its way.
33
+ */
34
+ export default function Skeleton<TKey extends keyof ComponentsAndVariants = 'skeleton'>(props: SkeletonProps<TKey>): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import e from "./visuallyHidden.mjs";
2
+ import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
+ import r from "@box-kite/react";
4
+ //#region src/components/skeleton.tsx
5
+ function i(i) {
6
+ let { lines: a = 1, circle: o = !1, label: s, props: c, ...l } = i, u = o ? 1 : Math.max(1, Math.trunc(a));
7
+ return /* @__PURE__ */ n(r, {
8
+ component: "skeleton",
9
+ ...l,
10
+ props: {
11
+ ...s === void 0 ? { "aria-hidden": !0 } : { role: "status" },
12
+ ...c
13
+ },
14
+ children: [Array.from({ length: u }, (e, n) => /* @__PURE__ */ t(r, {
15
+ component: "skeleton.bar",
16
+ variant: {
17
+ circle: o,
18
+ short: !o && u > 1 && n === u - 1
19
+ },
20
+ children: /* @__PURE__ */ t(r, { component: "skeleton.gloss" })
21
+ }, n)), s !== void 0 && /* @__PURE__ */ t(e, { children: s })]
22
+ });
23
+ }
24
+ //#endregion
25
+ export { i as default };
@@ -0,0 +1 @@
1
+ var e={type:`object`,description:`A dashboard layout: a column space and one place per widget, in cells.`,required:[`version`,`columns`,`items`],additionalProperties:!1,properties:{version:{type:`integer`,description:`The layout format's version. 1 today.`},columns:{type:`integer`,description:`The column space the items are written in, 1 to 24.`},items:{type:`array`,items:{type:`object`,required:[`id`,`x`,`y`,`w`,`h`],additionalProperties:!1,properties:{id:{type:`string`,description:`The id of the widget this place belongs to.`},x:{type:`integer`,description:`Column, zero-based.`},y:{type:`integer`,description:`Row, zero-based.`},w:{type:`integer`,description:`Columns spanned, at least 1.`},h:{type:`integer`,description:`Rows spanned, at least 1.`},minW:{type:`integer`,description:`The narrowest it may be resized to.`},minH:{type:`integer`,description:`The shortest it may be resized to.`},maxW:{type:`integer`,description:`The widest it may be resized to.`},maxH:{type:`integer`,description:`The tallest it may be resized to.`},fixed:{type:`boolean`,description:`Neither moved nor resized; everything else flows around it.`}}}}}};Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return e}});
@@ -0,0 +1,78 @@
1
+ var e = {
2
+ type: "object",
3
+ description: "A dashboard layout: a column space and one place per widget, in cells.",
4
+ required: [
5
+ "version",
6
+ "columns",
7
+ "items"
8
+ ],
9
+ additionalProperties: !1,
10
+ properties: {
11
+ version: {
12
+ type: "integer",
13
+ description: "The layout format's version. 1 today."
14
+ },
15
+ columns: {
16
+ type: "integer",
17
+ description: "The column space the items are written in, 1 to 24."
18
+ },
19
+ items: {
20
+ type: "array",
21
+ items: {
22
+ type: "object",
23
+ required: [
24
+ "id",
25
+ "x",
26
+ "y",
27
+ "w",
28
+ "h"
29
+ ],
30
+ additionalProperties: !1,
31
+ properties: {
32
+ id: {
33
+ type: "string",
34
+ description: "The id of the widget this place belongs to."
35
+ },
36
+ x: {
37
+ type: "integer",
38
+ description: "Column, zero-based."
39
+ },
40
+ y: {
41
+ type: "integer",
42
+ description: "Row, zero-based."
43
+ },
44
+ w: {
45
+ type: "integer",
46
+ description: "Columns spanned, at least 1."
47
+ },
48
+ h: {
49
+ type: "integer",
50
+ description: "Rows spanned, at least 1."
51
+ },
52
+ minW: {
53
+ type: "integer",
54
+ description: "The narrowest it may be resized to."
55
+ },
56
+ minH: {
57
+ type: "integer",
58
+ description: "The shortest it may be resized to."
59
+ },
60
+ maxW: {
61
+ type: "integer",
62
+ description: "The widest it may be resized to."
63
+ },
64
+ maxH: {
65
+ type: "integer",
66
+ description: "The tallest it may be resized to."
67
+ },
68
+ fixed: {
69
+ type: "boolean",
70
+ description: "Neither moved nor resized; everything else flows around it."
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ };
77
+ //#endregion
78
+ export { e as t };
package/docs/a11y.md CHANGED
@@ -47,9 +47,8 @@ setOpen(false, { reason: 'escape', event });
47
47
  `onChange(value, { reason, event })` is the point of the hook. A component that only reports
48
48
  `onOpenChange(false)` makes its consumer guess whether the popup closed because something was
49
49
  picked, because Escape was pressed, because the user clicked away, or because the component decided
50
- on its own — and each of those wants different behaviour from the surrounding app. This is the
51
- Base UI shape, and it is the answer to the long-standing complaints about controlled state in
52
- Radix.
50
+ on its own — and each of those wants different behaviour from the surrounding app. A reason on
51
+ every change is what a controlled component owes its consumer.
53
52
 
54
53
  Details worth knowing:
55
54
 
@@ -0,0 +1,451 @@
1
+ # Generative UI
2
+
3
+ Two halves of one loop, in two entries. `catalog()` (`@box-kite/react/catalog`) describes what a
4
+ generated UI is allowed to build, and `<SpecRenderer>` (`@box-kite/react/spec`) renders what came
5
+ back — against the same description, so the thing a model is constrained to and the thing that renders
6
+ cannot disagree.
7
+
8
+ ## The catalog
9
+
10
+ `catalog()` describes what a generated UI is allowed to build: every component a spec may name, and every
11
+ value its props may take, as JSON Schema. It is the piece a generative-UI runtime needs and the one no
12
+ styling library could give it before — because in this library the typed props already _are_ a constrained,
13
+ serializable design language, so a Box tree and a JSON UI spec are the same thing written twice.
14
+
15
+ ```ts
16
+ import { catalog } from '@box-kite/react/catalog';
17
+
18
+ const allowed = catalog({
19
+ include: ['Flex', 'H2', 'P', 'Sparkline'],
20
+ styleProps: ['d', 'gap', 'p', 'bgColor', 'color', 'fontSize'],
21
+ });
22
+ ```
23
+
24
+ ```jsonc
25
+ {
26
+ "version": 1,
27
+ "library": "@box-kite/react",
28
+ "tokens": { "colors": ["slate-50", …], "animations": ["spin", …], "components": ["button", …] },
29
+ "rules": ["Spacing, sizing, gap, borderRadius and the inset props divide by 4: p={4} is 1rem (16px).", …],
30
+ "components": {
31
+ "Flex": {
32
+ "description": "A flex row or column.",
33
+ "import": "import Flex from '@box-kite/react/components/flex';",
34
+ "slots": ["default"],
35
+ "events": [],
36
+ "props": {
37
+ "type": "object",
38
+ "$defs": { "color": { "type": "string", "pattern": "^(?:(?:slate|gray|…|rose)-(?:50|…|950)|currentColor|…)(?:\\/(?:100|\\d{1,2}(?:\\.\\d+)?))?$" } },
39
+ "properties": {
40
+ "d": { "type": "string", "enum": ["row", "column", "row-reverse", "column-reverse"] },
41
+ "p": { "anyOf": [{ "type": "string", "enum": ["1/2", …] }, { "type": "string", "pattern": "^-?\\d+(?:\\.\\d+)?%$" }, { "type": "number" }] },
42
+ "bgColor": { "$ref": "#/$defs/color", "description": "The background-color CSS property…" }
43
+ },
44
+ "additionalProperties": false
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## Two sources, and why neither would do alone
52
+
53
+ The **engine** says what a prop accepts. That half is read off the live prop registry at the moment you
54
+ call `catalog()`, which is why a prop or a colour added by `Box.extend()` is in the catalog with no build
55
+ step and no regeneration — call `catalog()` after the `extend()` that should be in it.
56
+
57
+ The **manifest** says what a prop means. That half is generated (`npm run docs:catalog`) from the same
58
+ JSDoc the prop reference and the AI context come from, because no registry entry knows that `fontSize`
59
+ divides by 16.
60
+
61
+ What a JSON Schema cannot express at all is in `rules`: the dividers, the millisecond times, the unitless
62
+ SVG lengths, and that HTML attributes go in `props`. Put them in the prompt — they are the errors that
63
+ still validate and still render.
64
+
65
+ ## The guardrail
66
+
67
+ A colour prop is a `pattern` over the palette, not a free string, so a generated tree can only ask for
68
+ colours the theme has:
69
+
70
+ ```ts
71
+ import * as z from 'zod';
72
+
73
+ const flex = z.fromJSONSchema(catalog({ include: ['Flex'] }).components.Flex.props);
74
+
75
+ flex.safeParse({ bgColor: 'sky-500/40' }).success; // true
76
+ flex.safeParse({ bgColor: '#ff00ff' }).success; // false
77
+ flex.safeParse({ invented: 1 }).success; // false — every component schema is additionalProperties: false
78
+ ```
79
+
80
+ The same holds for every closed value list (`d`, `position`, `textAlign`…) and for the grammars behind the
81
+ open ones (a percentage, a ratio, an `anchor-size()`). A prop the registry leaves genuinely open is
82
+ `{ type: 'string' }` with its listed values as `examples`, which is the honest shape: the catalog never
83
+ states a constraint the library does not enforce.
84
+
85
+ ## The allow-list is yours
86
+
87
+ `catalog()` with no options describes all 78 components and all 221 style props. That is ~4.6 MB
88
+ serialized, and a prompt carrying it would spend more tokens on the catalog than on the UI. It is the
89
+ library saying what it _can_ render; narrowing it is the app's job, and the app is the only one that knows
90
+ which components it wants a generator reaching for.
91
+
92
+ | Option | What it does |
93
+ | ------------ | -------------------------------------------------------------------------------------------------- |
94
+ | `include` | Only these components, by name. |
95
+ | `exclude` | Everything but these. Applied after `include`. |
96
+ | `styleProps` | `true` (every registered prop, the default), `false` (the component's own props alone), or a list. |
97
+
98
+ ## What is not in it
99
+
100
+ **Function props are `events`, named rather than described.** A JSON spec cannot carry a function, so
101
+ `onValueChange` is listed on the component as an event and the host binds it — which is where a
102
+ human-in-the-loop decision belongs anyway.
103
+
104
+ **A `ReactNode` prop is a slot.** `children` is the `default` slot and a named one (`Tooltip`'s
105
+ `content`) is a slot of its own name. Every component that can hold children has `default`; one whose
106
+ element takes none — `Img`, `Textbox`, `Textarea` — has no default slot at all, and that is what tells
107
+ a generator (and `<SpecRenderer>`) that children have nowhere to go there.
108
+
109
+ **Anything else a JSON spec cannot express is left out.** A prop whose type is a row renderer or a
110
+ datasource is absent rather than half-described: a partial schema would state a constraint that is not
111
+ true. The exception is the next section — a handful of props that are a _shape_, where the part a spec can
112
+ write is worth describing on its own.
113
+
114
+ ## The shapes: a grid's columns, a dashboard's layout
115
+
116
+ Extraction maps _types_, and a few props are a shape whose other half is React: a `ColumnType` carries a
117
+ `Cell` renderer and an `onCellEdit`, a `WidgetProps.empty` is a `ReactNode`. A mapping has to drop such a
118
+ prop whole — which left the catalog able to place a `<DashboardGrid>` and neither lay it out nor put
119
+ anything in it, and a `<DataGrid>` with no `def` at all, though `def` is required. Those are the two things
120
+ a dashboard _is_.
121
+
122
+ So four components carry a hand-written contract for the part a spec can write, and it is in the catalog
123
+ like any other prop:
124
+
125
+ | Component | Props | What a spec may write |
126
+ | ---------------- | ------------------------------------ | --------------------------------------------------------------------------------------- |
127
+ | `DataGrid` | `def` (**required**), `data` | The columns and the grid-wide flags; the rows, as the host's own objects. |
128
+ | `DashboardGrid` | `layout`, `defaultLayout`, `columns` | `DashboardUtils.SCHEMA` — the same artifact a drag reports back — and the column space. |
129
+ | `Widget` | `empty` | `true` for the default line, or the words to use instead. |
130
+ | `ChartContainer` | `series` | The series names, or a name-to-colour record. |
131
+
132
+ ```json
133
+ {
134
+ "type": "DataGrid",
135
+ "props": {
136
+ "data": { "$data": "orders" },
137
+ "def": {
138
+ "rowKey": "id",
139
+ "title": "Orders",
140
+ "footer": true,
141
+ "columns": [
142
+ { "key": "customer", "header": "Customer" },
143
+ { "key": "total", "header": "Total", "align": "end", "aggregate": "sum" }
144
+ ]
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ `data` is the one prop in the catalog that carries values rather than styling, so its schema says "objects"
151
+ and stops — `{ $data: 'orders' }` is the usual answer, and the rows stay the host's. Everything else is
152
+ judged: a column with no `key`, an `aggregate` that is not one of the five, a `Cell` renderer written as a
153
+ string — each of them fails the schema, so the prop is dropped and the node renders without it.
154
+
155
+ Two things are deliberately absent. A column's own `columns` (the header groups) would be a recursive
156
+ schema, which is what a structured-output API is worst at and what the catalog has no `$defs` of its own to
157
+ carry. And `dataSource`, `onCellEdit`, `rowDetail` and `treeData` are functions or components: they are the
158
+ app's to pass, beside the spec rather than in it.
159
+
160
+ ## Rendering what came back: `<SpecRenderer>`
161
+
162
+ The catalog says what a model may write. `@box-kite/react/spec` renders what it wrote — against a
163
+ registry the app builds, which is the same allow-list one step further on: the catalog is what the
164
+ library _can_ render, the registry is what this app _will_.
165
+
166
+ ```tsx
167
+ import { catalog } from '@box-kite/react/catalog';
168
+ import Button from '@box-kite/react/components/button';
169
+ import { Sparkline } from '@box-kite/react/components/chart';
170
+ import Flex from '@box-kite/react/components/flex';
171
+ import { H2, P } from '@box-kite/react/components/semantics';
172
+ import SpecRenderer, { createSpecRegistry } from '@box-kite/react/spec';
173
+
174
+ const allowed = catalog({ include: ['Flex', 'H2', 'P', 'Button', 'Sparkline'], styleProps: ['d', 'gap', 'p', 'bgColor', 'fontSize'] });
175
+
176
+ const registry = createSpecRegistry({ catalog: allowed, components: { Flex, H2, P, Button, Sparkline } });
177
+
178
+ export default function Generated({ spec, data }: { spec: unknown; data: unknown }) {
179
+ return <SpecRenderer spec={spec} registry={registry} data={data} onAction={(action) => run(action)} />;
180
+ }
181
+ ```
182
+
183
+ A spec is a tree of nodes, and a node is JSON:
184
+
185
+ ```jsonc
186
+ {
187
+ "type": "Flex",
188
+ "props": { "d": "column", "gap": 4, "p": 4, "bgColor": "sky-500/10" },
189
+ "children": [
190
+ { "type": "H2", "props": { "fontSize": 24 }, "children": ["Revenue"] },
191
+ { "type": "Sparkline", "props": { "data": { "$data": "weekly" } } },
192
+ { "type": "Button", "props": { "label": "Refresh" }, "on": { "onClick": "refresh" } },
193
+ ],
194
+ }
195
+ ```
196
+
197
+ `type` names a component, `props` is everything its own schema allows, `children` is the default slot,
198
+ `slots` fills a named one, `on` binds an event to an action, and `repeat` renders the node once per item
199
+ of an array. Nothing else is a field, and every one of them is checked.
200
+
201
+ ## What a spec cannot do
202
+
203
+ The list is short because the design is: a node is data, and the only things it can reach are the ones
204
+ the app registered.
205
+
206
+ - **It cannot name a component the app did not register.** The node renders nothing and the app is told
207
+ which name was asked for. There is no tag from the spec, no `eval`, no `dangerouslySetInnerHTML`.
208
+ - **It cannot set a prop the component's schema refuses.** Every prop is validated against the
209
+ catalog's own schema for that component — so a colour is a palette token, a `d` is one of four
210
+ values, and a prop nobody has is dropped. The prop is dropped, not the node.
211
+ - **It cannot make a function.** `on` binds only the props the catalog lists as `events`, and what it
212
+ binds is a _name_: `onAction(action, details)` is called, and what that means is the app's. That is
213
+ human-in-the-loop by construction — an action can open a confirmation as easily as it can run.
214
+ - **It cannot reach outside the data it was given.** A `{ $data: … }` reference is a path, read with
215
+ own properties only, against the object passed as `data`. There is no expression to evaluate.
216
+ - **It cannot run away.** `maxNodes` (1,000) and `maxDepth` (32) end a tree that recurses or repeats
217
+ over something enormous, and both are props.
218
+ - **It cannot take the page down.** Every node renders inside an error boundary of its own, so a
219
+ component that throws on the props it was handed costs that node and nothing around it — and a node
220
+ missing a prop its component cannot do without is held back before it is ever rendered.
221
+
222
+ Everything refused is reported rather than swallowed: `onIssues` is called with the whole list after
223
+ each render whose issues changed, and `renderSpec()` returns it. Each issue carries a `code`
224
+ (`unknown-component`, `invalid-prop`, `unknown-event`, `unresolved-data`, `too-deep`, `render-error`…),
225
+ the `path` it happened at (`spec.children.1.props.bgColor`) and a sentence.
226
+
227
+ ```tsx
228
+ <SpecRenderer
229
+ spec={spec}
230
+ registry={registry}
231
+ onIssues={(issues) => issues.forEach((issue) => track('generated-ui-issue', issue))}
232
+ fallback={(issue) => <P color="rose-500">{issue.message}</P>}
233
+ />
234
+ ```
235
+
236
+ `fallback` renders nothing by default: a generated dashboard is shown to somebody who did not write it,
237
+ and a red box is a worse answer than a missing card. In development it is the fastest way to see what a
238
+ model got wrong.
239
+
240
+ ## Still arriving
241
+
242
+ A spec streams. `streamObject`'s `partialObjectStream` hands over the same object a few more characters
243
+ at a time, so the renderer treats a half-written tree as the normal case rather than the error case:
244
+
245
+ - a node whose `type` has not arrived renders nothing **and reports nothing** — it is a frame, not a
246
+ fault;
247
+ - a prop whose value is still half a string fails its schema and is dropped, so `sky-5` never paints
248
+ and `sky-500` appears when it is whole;
249
+ - a node whose **required** prop has not arrived is held back — the component would read `undefined`
250
+ and throw, and only its own boundary would catch that, so a stream would paint the same blank space
251
+ with a caught crash per frame behind it. It reports `missing-prop`, which at rest is a real fault
252
+ and mid-stream is most nodes for a moment;
253
+ - a node that threw on one frame is tried again on the next, because the spec object itself is what
254
+ resets the boundaries;
255
+ - **what a node has been shown with, it is not stripped of.** A stream is not monotone: a column's
256
+ `align` passes through `"e"` on its way to `"end"`, and a value that fails its own schema takes the
257
+ object it sits in with it — so a prop the component cannot do without can go missing for a frame.
258
+ `<SpecRenderer>` keeps the last value each node was given for such a prop, so a grid on screen is not
259
+ unmounted and rebuilt half a dozen times in the closing moments of a spec. It remembers for as long as
260
+ it is mounted; a host showing an unrelated second document gives it a `key`, the way it would to reset
261
+ any other state. `renderSpec()` on its own has no memory — it is one render, and there is no last time.
262
+
263
+ **A heavy component is the app's to gate.** The registry is where a name is pointed at an
264
+ implementation, so pointing it somewhere else while the spec is arriving is a one-line decision:
265
+
266
+ ```tsx
267
+ const streaming = createSpecRegistry({
268
+ catalog: allowed,
269
+ components: {
270
+ ...components,
271
+ DataGrid: { component: GridPlaceholder, props: { ...allowed.components.DataGrid.props, required: undefined } },
272
+ },
273
+ });
274
+
275
+ <SpecRenderer spec={spec} registry={isLoading ? streaming : registry} data={data} />;
276
+ ```
277
+
278
+ Dropping `required` is what makes the placeholder stand in from the frame the node first exists rather
279
+ than from the frame its `def` is first whole. The docs demo does exactly this: a grid's columns arrive
280
+ one at a time, and watching one rebuild itself is not what a reader came for.
281
+
282
+ **Write the controlled prop, never the `default…` twin.** React reads an uncontrolled default once, on
283
+ the first render, and ignores it afterwards — which a stream breaks on twice over: the frame in which
284
+ `defaultLayout` first arrived whole is the one that sticks, and a second spec rendered in the same place
285
+ keeps the first one's state, because the component instance is the same. A generated node writes
286
+ `layout`, `value`, `open`; every frame re-applies it, and the last frame is what stays on screen. The
287
+ catalog says which is which in each prop's description.
288
+
289
+ ```tsx
290
+ const { partialObjectStream } = streamObject({ model, schema: z.fromJSONSchema(specSchema(registry)), prompt });
291
+
292
+ for await (const partial of partialObjectStream) setSpec(partial);
293
+ ```
294
+
295
+ ## `specSchema()`: what the model is allowed to write
296
+
297
+ The other direction. `specSchema(registry)` turns the registry into one JSON Schema for a whole tree —
298
+ the component names are an enum of what the app allowed, each one's props are its own schema, and
299
+ `children` is offered only where there is a slot to put them in. It is what goes to `streamObject`, to a
300
+ structured-output API, or through `z.fromJSONSchema` to validate a finished spec before rendering it.
301
+
302
+ ```ts
303
+ import { specSchema } from '@box-kite/react/spec';
304
+
305
+ const schema = specSchema(registry, { bindings: true });
306
+ ```
307
+
308
+ **On a server, import it from `@box-kite/react/catalog` instead**, where it is exported as well — the
309
+ call that needs it is a route handler, and the entry that renders a spec is a client entry:
310
+
311
+ ```ts
312
+ import { catalog, specSchema } from '@box-kite/react/catalog';
313
+
314
+ const allowed = catalog({ include: ['DashboardGrid', 'Widget', 'Sparkline', 'DataGrid'] });
315
+
316
+ export async function POST(request: Request) {
317
+ const result = streamObject({ model, schema: jsonSchema(specSchema(allowed, { bindings: true })), prompt });
318
+
319
+ return result.toTextStreamResponse();
320
+ }
321
+ ```
322
+
323
+ It takes a catalog as readily as a registry — a `catalog()` entry already has the `props`, `slots` and
324
+ `events` a tree schema needs — so the server side of the loop needs no components and no React at all.
325
+ `examples/next-app/app/generative` is the whole thing, route and page, in three files.
326
+
327
+ `bindings` widens every prop to "this, or a reference to it" and adds `repeat`; it is off by default,
328
+ because it nearly doubles the document and a static view needs none of it. `root` narrows what may
329
+ stand at the top of the tree without narrowing what may stand inside it.
330
+
331
+ The constraint and the renderer are built from the same rules, so what a model is told it may write and
332
+ what the renderer lets through cannot drift apart.
333
+
334
+ ## Data, and the one thing the host still owns
335
+
336
+ A model writes the shape of a view; the numbers are the app's. A reference stands anywhere a value can:
337
+
338
+ | Written | Resolves to |
339
+ | ------------------------------ | ------------------------------------------------------------- |
340
+ | `{ "$data": "stats.revenue" }` | a path into `data` — a dot path, or a JSON Pointer (`/a/b/0`) |
341
+ | `{ "$item": "label" }` | a field of the current `repeat` item (`""` is the item) |
342
+ | `{ "$index": true }` | the current `repeat` index |
343
+
344
+ ```jsonc
345
+ { "type": "Ul", "children": [{ "type": "Li", "repeat": { "$data": "rows" }, "children": [{ "$item": "label" }] }] }
346
+ ```
347
+
348
+ **A reference is resolved before it is validated**, so the value the host supplied is the one the
349
+ schema judges: a `$data` that resolves to a string cannot land in a numeric prop. A reference that
350
+ resolves to nothing drops its prop and reports `unresolved-data`, which mid-stream is ordinary.
351
+
352
+ ## Registering a component of your own
353
+
354
+ A registry entry is a component, or a component with the rules around it:
355
+
356
+ ```tsx
357
+ const registry = createSpecRegistry({
358
+ catalog: allowed,
359
+ components: {
360
+ Flex,
361
+ H2,
362
+ StatCard: {
363
+ component: StatCard,
364
+ props: {
365
+ type: 'object',
366
+ properties: { label: { type: 'string' }, value: { type: 'number' }, tone: { type: 'string', enum: ['up', 'down'] } },
367
+ required: ['label', 'value'],
368
+ additionalProperties: false,
369
+ },
370
+ slots: [],
371
+ events: ['onSelect'],
372
+ },
373
+ },
374
+ });
375
+ ```
376
+
377
+ **A component registered with no schema takes no props at all.** That is the safe direction to be wrong
378
+ in, and writing the schema is the point: it is what the model is constrained to and what the renderer
379
+ enforces.
380
+
381
+ The same mechanism is how an HTML attribute gets through. The catalog leaves them out — `href` is not a
382
+ prop, it goes in `props={{ href }}` — so a generated `<Link>` can point nowhere until the app says where
383
+ it may point:
384
+
385
+ ```tsx
386
+ Link: {
387
+ component: Link,
388
+ props: {
389
+ ...allowed.components.Link.props,
390
+ properties: {
391
+ ...allowed.components.Link.props.properties,
392
+ props: { type: 'object', properties: { href: { type: 'string', pattern: '^(?:/|https://)' } }, additionalProperties: false },
393
+ },
394
+ },
395
+ },
396
+ ```
397
+
398
+ A `javascript:` URL then fails the pattern and is dropped, with an `invalid-prop` to say so. Note that
399
+ JSON Schema's `pattern` is unanchored: anchor yours.
400
+
401
+ ## On a server, and without React at all
402
+
403
+ `renderSpec(spec, options)` is `<SpecRenderer>` with no hook in it — it returns `{ element, issues }`,
404
+ which is what a test asserts on and what a server render uses when there is nothing to report to:
405
+
406
+ ```tsx
407
+ const { element, issues } = renderSpec(spec, { registry, data });
408
+ ```
409
+
410
+ A static spec renders through the ordinary SSR path with its CSS, like any other Box tree. The entry
411
+ carries a `'use client'` banner, since `<SpecRenderer>` itself holds an effect.
412
+
413
+ ## Rendering it somewhere else: json-render
414
+
415
+ [`@json-render/react`](https://json-render.dev) wants a Zod schema per component, and `z.fromJSONSchema` is
416
+ the whole adapter:
417
+
418
+ ```ts
419
+ import { schema } from '@json-render/react';
420
+ import * as z from 'zod';
421
+ import { catalog } from '@box-kite/react/catalog';
422
+
423
+ const box = catalog({ include: ['Flex', 'H2', 'P'], styleProps: ['d', 'gap', 'p', 'bgColor', 'fontSize'] });
424
+
425
+ const jsonRender = schema.createCatalog({
426
+ actions: {},
427
+ components: Object.fromEntries(
428
+ Object.entries(box.components).map(([name, component]) => [
429
+ name,
430
+ { props: z.fromJSONSchema(component.props), slots: component.slots, description: component.description, example: {} },
431
+ ]),
432
+ ),
433
+ });
434
+ ```
435
+
436
+ **Apply the props guardrail yourself.** Measured against `@json-render/react` 0.20.0: its `propsOf` schema
437
+ type resolves to `z.record(z.string(), z.unknown())` for every catalog holding more than one component, so
438
+ `jsonRender.validate(spec)` checks which components a spec names and lets any props through. Each
439
+ component's `props` is a self-contained schema for exactly this reason — check a node against its own
440
+ before you render it.
441
+
442
+ The same shape serves a structured-output API (`components[name].props` is a strict JSON Schema) and any
443
+ runtime that takes a component catalog: assistant-ui, CopilotKit, A2UI.
444
+
445
+ ## Seeing it run
446
+
447
+ [box-kite.dev/generative-ui](https://www.box-kite.dev/generative-ui/) is the loop end to end: three
448
+ prompts, a dashboard of grids and charts arriving a character at a time, the theme flipped under it, and
449
+ a second demo showing what the renderer refuses and what it reports about each refusal. The site is
450
+ static, so those generations are recordings — the catalog, the registry and the renderer on that page are
451
+ the real ones, and the live `streamObject` route is `examples/next-app/app/generative` in the repository.
@@ -14,9 +14,11 @@ No hook, no effect, no DOM: a React Server Component can render these directly.
14
14
  | `@box-kite/react/components/flex` | `Flex` (default) | |
15
15
  | `@box-kite/react/components/grid` | `Grid` (default) | |
16
16
  | `@box-kite/react/components/icon` | `Icon` (default) | |
17
+ | `@box-kite/react/components/markdown` | `markdown` (default), `markdownComponents` | |
17
18
  | `@box-kite/react/components/progress` | `Progress` (default) | |
18
19
  | `@box-kite/react/components/radioButton` | `RadioButton` (default) | |
19
20
  | `@box-kite/react/components/semantics` | `Article`, `Aside`, `Details`, `Figcaption`, `Figure`, `Footer`, `H1`, `H2`, `H3`, `H4`, `H5`, `H6`, `Header`, `Img`, `Label`, `Li`, `Link`, `Main`, `Mark`, `Menu`, `MenuList`, `Nav`, `Ol`, `P`, `Section`, `Span`, `Summary`, `Time`, `Ul` | **deprecated** |
21
+ | `@box-kite/react/components/skeleton` | `i` (default) | |
20
22
  | `@box-kite/react/components/svg` | `Circle`, `ClipPath`, `Defs`, `Ellipse`, `G`, `Line`, `LinearGradient`, `Marker`, `Mask`, `Path`, `Polygon`, `Polyline`, `RadialGradient`, `Rect`, `Stop`, `Svg`, `SvgSymbol`, `SvgText`, `TSpan`, `Use` | |
21
23
  | `@box-kite/react/components/textarea` | `Textarea` (default) | |
22
24
  | `@box-kite/react/components/textbox` | `Textbox` (default) | |
@@ -29,8 +31,10 @@ These hold state, measure the DOM or portal into it. A Server Component may impo
29
31
  | Import | Components | Notes |
30
32
  | --- | --- | --- |
31
33
  | `@box-kite/react/components/accordion` | `Accordion` (default), `Collapsible` | |
34
+ | `@box-kite/react/components/agent` | `AgentUtils`, `ApprovalCard`, `Reasoning`, `StreamingText`, `ToolCallCard` | |
32
35
  | `@box-kite/react/components/checkbox` | `Checkbox` (default) | |
33
36
  | `@box-kite/react/components/combobox` | `Combobox` (default), `ComboboxUtils` | |
37
+ | `@box-kite/react/components/dashboard` | `DashboardGrid` (default), `DashboardUtils`, `Widget` | |
34
38
  | `@box-kite/react/components/dataGrid` | `dataGrid` (default) | |
35
39
  | `@box-kite/react/components/dialog` | `Dialog` (default), `AlertDialog` | |
36
40
  | `@box-kite/react/components/dropdown` | `Dropdown` (default) | **deprecated** |
package/docs/index.md CHANGED
@@ -7,5 +7,8 @@ The markdown `@box-kite/react` ships for whatever is reading its source tree. No
7
7
  - [`components.md`](components.md) — every component, its import specifier and whether it renders on a server
8
8
  - [`a11y.md`](a11y.md) — the behaviour hooks (`@box-kite/react/a11y`) for a pattern this library does not ship
9
9
  - [`anchor.md`](anchor.md) — where a floating layer goes (`@box-kite/react/anchor`), in CSS and in the measured fallback
10
+ - [`catalog.md`](catalog.md) — what a generated UI may build (`@box-kite/react/catalog`) and how it renders (`@box-kite/react/spec`)
11
+ - [`interop.md`](interop.md) — the agentic runtimes (`@box-kite/react/interop`): AI SDK, assistant-ui, CopilotKit, A2UI and AG-UI, each verified against its own package
12
+ - [`mcp.md`](mcp.md) — the MCP server (`npx @box-kite/mcp`), whose `check_styles` tool is the only way to be sure a value works
10
13
 
11
14
  The rest sits at the package root: `BOX_KITE_AI_CONTEXT.md`, the long-form reference (prop tables, the DataGrid API, debugging); `.claude/skills/box-kite/`, the same rules as an installable skill with four references beside it; and `.cursor/rules/box-kite.mdc`, which is that skill as a Cursor rule — copy either into the project rather than reading it from here, so it is loaded without anyone asking.