@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.
- package/.claude/rules/box-kite-rules.md +24 -2
- package/.claude/skills/box-kite/SKILL.md +27 -5
- package/.claude/skills/box-kite/references/components.md +8 -3
- package/.claude/skills/box-kite/references/patterns.md +24 -1
- package/.claude/skills/box-kite/references/styling.md +23 -0
- package/.cursor/rules/box-kite.mdc +25 -3
- package/AGENTS.md +28 -3
- package/BOX_AI_CONTEXT.md +732 -12
- package/BOX_KITE_AI_CONTEXT.md +732 -12
- package/README.md +41 -9
- package/box.cjs +1 -1
- package/box.d.ts +13 -2
- package/box.mjs +25 -18
- package/catalog.cjs +1 -0
- package/catalog.d.ts +8 -0
- package/catalog.mjs +2347 -0
- package/client.cjs +1 -1
- package/client.mjs +59 -53
- package/components/agent.cjs +1 -0
- package/components/agent.d.ts +215 -0
- package/components/agent.mjs +405 -0
- package/components/button.cjs +1 -1
- package/components/button.mjs +10 -14
- package/components/chart.d.ts +1 -0
- package/components/dashboard.cjs +1 -0
- package/components/dashboard.d.ts +121 -0
- package/components/dashboard.mjs +690 -0
- package/components/dataGrid/components/dataGridBody.d.ts +2 -1
- package/components/dataGrid/components/dataGridDetailRow.d.ts +13 -2
- package/components/dataGrid/components/dataGridGroupRow.d.ts +13 -2
- package/components/dataGrid/components/dataGridRow.d.ts +13 -2
- package/components/dataGrid/contracts/dataGridContract.d.ts +66 -2
- package/components/dataGrid/models/cellModel.d.ts +3 -0
- package/components/dataGrid/models/editModel.d.ts +27 -0
- package/components/dataGrid/models/exportModel.d.ts +2 -1
- package/components/dataGrid/models/gridModel.d.ts +3 -0
- package/components/dataGrid/models/rangeModel.d.ts +119 -0
- package/components/dataGrid/models/viewportModel.d.ts +23 -2
- package/components/dataGrid/useGridNavigation.d.ts +10 -2
- package/components/dataGrid.cjs +4 -1
- package/components/dataGrid.d.ts +3 -0
- package/components/dataGrid.mjs +1056 -765
- package/components/flex.cjs +1 -1
- package/components/flex.mjs +10 -13
- package/components/form.cjs +1 -1
- package/components/form.mjs +7 -7
- package/components/grid.cjs +1 -1
- package/components/grid.mjs +10 -13
- package/components/markdown.cjs +1 -0
- package/components/markdown.d.ts +55 -0
- package/components/markdown.mjs +179 -0
- package/components/menu.cjs +1 -1
- package/components/menu.mjs +16 -16
- package/components/overlay.cjs +1 -1
- package/components/overlay.mjs +35 -34
- package/components/popover.cjs +1 -1
- package/components/popover.mjs +4 -4
- package/components/semantics.cjs +1 -1
- package/components/semantics.d.ts +29 -29
- package/components/semantics.mjs +9 -13
- package/components/skeleton.cjs +1 -0
- package/components/skeleton.d.ts +34 -0
- package/components/skeleton.mjs +25 -0
- package/components/svg.cjs +1 -1
- package/components/svg.mjs +38 -44
- package/components/textarea.cjs +1 -1
- package/components/textarea.mjs +12 -16
- package/components/textbox.cjs +1 -1
- package/components/textbox.mjs +12 -16
- package/components/toaster.cjs +1 -1
- package/components/toaster.mjs +42 -43
- package/components/visuallyHidden.cjs +1 -1
- package/components/visuallyHidden.mjs +10 -13
- package/dashboardSchema.cjs +1 -0
- package/dashboardSchema.mjs +78 -0
- package/docs/a11y.md +2 -3
- package/docs/catalog.md +451 -0
- package/docs/components.md +4 -0
- package/docs/index.md +3 -0
- package/docs/interop.md +237 -0
- package/docs/mcp.md +70 -0
- package/docs/props.md +135 -23
- package/interop.cjs +1 -0
- package/interop.d.ts +51 -0
- package/interop.mjs +454 -0
- package/layer.cjs +1 -1
- package/layer.mjs +61 -59
- package/mcp/src/data.d.ts +88 -0
- package/mcp/src/data.test.d.ts +1 -0
- package/mcp/src/engine.d.ts +23 -0
- package/mcp/src/index.d.ts +1 -0
- package/mcp/src/search.d.ts +26 -0
- package/mcp/src/search.test.d.ts +1 -0
- package/mcp/src/server.d.ts +18 -0
- package/mcp/src/server.test.d.ts +1 -0
- package/mcp/src/styles.d.ts +27 -0
- package/mcp/src/styles.test.d.ts +1 -0
- package/mcp/src/tools.d.ts +18 -0
- package/mcp/src/vocabulary.d.ts +41 -0
- package/package.json +170 -37
- package/react/boxComponent.d.ts +10 -0
- package/react/popover/useTopLayer.d.ts +17 -0
- package/react/spec/renderSpec.d.ts +51 -0
- package/react/spec/specBoundary.d.ts +25 -0
- package/react/spec/specRegistry.d.ts +42 -0
- package/react/spec/specRenderer.d.ts +10 -0
- package/react/theme/theme.d.ts +10 -0
- package/react/useStyles.d.ts +2 -0
- package/react-shared.cjs +1 -1
- package/react-shared.mjs +44 -37
- package/registry/blocks/dashboard-shell/dashboard-shell.d.ts +22 -0
- package/registry/blocks/dashboard-shell/dashboard-stats.d.ts +13 -0
- package/registry/blocks/data-grid/invoice-rows.d.ts +15 -0
- package/registry/blocks/data-grid/invoices-grid.d.ts +1 -0
- package/registry/blocks/settings-form/settings-form.d.ts +17 -0
- package/rsc.cjs +1 -1
- package/rsc.d.ts +11 -1
- package/rsc.mjs +15 -12
- package/spec.cjs +2 -0
- package/spec.d.ts +12 -0
- package/spec.mjs +382 -0
- package/specData.cjs +1 -0
- package/specData.mjs +69 -0
- package/specDefs.cjs +1 -0
- package/specDefs.mjs +33 -0
- package/specSchema.cjs +1 -0
- package/specSchema.mjs +88 -0
- package/ssg.cjs +1 -1
- package/ssg.d.ts +8 -2
- package/ssg.mjs +13 -9
- package/utils/agent/agentUtils.d.ts +81 -0
- package/utils/catalog/catalogContracts.d.ts +19 -0
- package/utils/catalog/catalogManifest.d.ts +8 -0
- package/utils/dashboard/dashboardSchema.d.ts +18 -0
- package/utils/dashboard/dashboardUtils.d.ts +97 -0
- package/utils/environment/environmentUtils.d.ts +5 -0
- package/utils/interop/a2uiInterop.d.ts +82 -0
- package/utils/interop/assistantUiInterop.d.ts +34 -0
- package/utils/interop/toolInterop.d.ts +30 -0
- package/utils/spec/specData.d.ts +19 -0
- package/utils/spec/specDefs.d.ts +11 -0
- package/utils/spec/specProps.d.ts +15 -0
- package/utils/spec/specSchema.d.ts +23 -0
- package/utils/spec/specTypes.d.ts +71 -0
- package/utils/spec/specValidate.d.ts +10 -0
|
@@ -1,23 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import n from "@box-kite/react";
|
|
1
|
+
import { f as e } from "../react-shared.mjs";
|
|
2
|
+
import { useBoxElement as t } from "@box-kite/react";
|
|
4
3
|
//#region src/components/visuallyHidden.tsx
|
|
5
|
-
var
|
|
6
|
-
function
|
|
7
|
-
return
|
|
8
|
-
ref: i,
|
|
4
|
+
var n = .25;
|
|
5
|
+
function r(e, r) {
|
|
6
|
+
return t({
|
|
9
7
|
position: "absolute",
|
|
10
|
-
width:
|
|
11
|
-
height:
|
|
8
|
+
width: n,
|
|
9
|
+
height: n,
|
|
12
10
|
p: 0,
|
|
13
11
|
b: 0,
|
|
14
12
|
overflow: "hidden",
|
|
15
13
|
whiteSpace: "nowrap",
|
|
16
14
|
clipPath: "inset(50%)",
|
|
17
15
|
...e
|
|
18
|
-
});
|
|
16
|
+
}, r);
|
|
19
17
|
}
|
|
20
|
-
var
|
|
21
|
-
a.displayName = "VisuallyHidden";
|
|
18
|
+
var i = e(r, "VisuallyHidden");
|
|
22
19
|
//#endregion
|
|
23
|
-
export {
|
|
20
|
+
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.
|
|
51
|
-
|
|
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
|
|
package/docs/catalog.md
ADDED
|
@@ -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.
|
package/docs/components.md
CHANGED
|
@@ -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.
|