@box-kite/react 2.3.1 → 2.4.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 +58 -94
- package/.claude/skills/box-kite/SKILL.md +69 -104
- package/.claude/skills/box-kite/references/components.md +1 -1
- package/.claude/skills/box-kite/references/patterns.md +1 -1
- package/.claude/skills/box-kite/references/rules.md +115 -0
- package/.cursor/rules/box-kite.mdc +60 -96
- package/AGENTS.md +61 -97
- package/BOX_AI_CONTEXT.md +39 -38
- package/BOX_KITE_AI_CONTEXT.md +39 -38
- package/README.md +200 -721
- package/client.cjs +1 -1
- package/client.mjs +81 -81
- package/components/combobox/comboboxListbox.d.ts +2 -1
- package/components/combobox.cjs +1 -1
- package/components/combobox.d.ts +1 -1
- package/components/combobox.mjs +199 -188
- package/components/dataGrid/contracts/dataGridContract.d.ts +2 -1
- package/components/dataGrid/useListLayout.d.ts +1 -1
- package/components/dataGrid.cjs +3 -3
- package/components/dataGrid.mjs +453 -453
- package/components/radioGroup.cjs +1 -1
- package/components/radioGroup.mjs +1 -0
- package/components/semantics.d.ts +29 -29
- package/components/skeleton.cjs +1 -1
- package/components/skeleton.d.ts +1 -1
- package/components/skeleton.mjs +2 -1
- package/components/textarea.d.ts +1 -1
- package/components/textbox.d.ts +1 -1
- package/components/timeZoneSelect.d.ts +1 -1
- package/components/toaster.cjs +1 -1
- package/components/toaster.mjs +13 -14
- package/docs/a11y.md +1 -2
- package/docs/index.md +1 -1
- package/effects.cjs +1 -1
- package/effects.mjs +2 -2
- package/identity.cjs +1 -1
- package/identity.mjs +5 -7
- package/package.json +9 -9
- package/react/effects.d.ts +5 -6
- package/react/identity/useIdentifier.d.ts +0 -3
package/README.md
CHANGED
|
@@ -1,791 +1,270 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[](LICENSE)
|
|
3
|
+
<a href="https://www.box-kite.dev">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/box-kite/box-kite/main/.github/assets/banner.png" alt="Box Kite — every CSS property is a typed prop. The React styling library your AI already knows." width="100%" />
|
|
5
|
+
</a>
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
pass at runtime and caches every rule by its content, so the same value anywhere in the app reuses
|
|
10
|
-
one class — a project styles itself in TypeScript, with no CSS files to write and no class-name
|
|
11
|
-
convention to remember.
|
|
7
|
+
<h3>The AI-first, fully typed styling library for React</h3>
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
<p>
|
|
10
|
+
<a href="https://www.npmjs.com/package/@box-kite/react"><img src="https://img.shields.io/npm/v/@box-kite/react?color=7c3aed&label=npm" alt="npm version" /></a>
|
|
11
|
+
<a href="https://github.com/box-kite/box-kite/actions/workflows/test.yml"><img src="https://github.com/box-kite/box-kite/actions/workflows/test.yml/badge.svg" alt="Tests" /></a>
|
|
12
|
+
<img src="https://img.shields.io/badge/TypeScript-every%20value-3178c6?logo=typescript&logoColor=white" alt="TypeScript: every value is typed" />
|
|
13
|
+
<a href="#-packages"><img src="https://img.shields.io/badge/dependencies-no%20third--party-10b981" alt="No third-party dependencies" /></a>
|
|
14
|
+
<a href="#-built-for-ai-agents"><img src="https://img.shields.io/badge/AGENTS.md-in%20the%20package-8b5cf6" alt="AGENTS.md ships in the package" /></a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@box-kite/mcp"><img src="https://img.shields.io/badge/MCP-server-a855f7" alt="MCP server" /></a>
|
|
16
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/@box-kite/react?color=64748b" alt="MIT license" /></a>
|
|
17
|
+
</p>
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
<p>
|
|
20
|
+
<a href="https://www.box-kite.dev"><b>Docs</b></a> ·
|
|
21
|
+
<a href="https://www.box-kite.dev/start-with-ai/"><b>Start with AI</b></a> ·
|
|
22
|
+
<a href="https://www.box-kite.dev/playground/">Playground</a> ·
|
|
23
|
+
<a href="https://www.box-kite.dev/showcase/">Showcase</a> ·
|
|
24
|
+
<a href="https://www.box-kite.dev/blog/">Blog</a> ·
|
|
25
|
+
<a href="https://github.com/box-kite/box-kite/releases">Releases</a>
|
|
26
|
+
</p>
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
inside the package, generated from the prop registry itself — so what your agent reads is never a
|
|
24
|
-
version behind what it is writing against: [Built for AI](https://www.box-kite.dev/ai-context/) ·
|
|
25
|
-
[`npx @box-kite/mcp`](https://www.npmjs.com/package/@box-kite/mcp) ·
|
|
26
|
-
[generative UI](https://www.box-kite.dev/generative-ui/)
|
|
28
|
+
</div>
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
---
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
**Box Kite** styles React in TypeScript. Every CSS property is a prop on `Box`, every value it takes is a type, and the CSS is
|
|
33
|
+
written for you at runtime — one shared class per value, no stylesheet to import, no class names to invent, no build plugin.
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
React 16.14 or newer, including 18 and 19 (both are covered by CI). TypeScript is optional but the
|
|
37
|
-
props are typed for it.
|
|
38
|
-
|
|
39
|
-
Two packages ship, and a React project installs one of them:
|
|
40
|
-
|
|
41
|
-
| Package | What it is |
|
|
42
|
-
| ------------------------------------------------- | ----------------------------------------------------------------- |
|
|
43
|
-
| **`@box-kite/react`** | `Box`, every component, the hooks. Depends on the one below. |
|
|
44
|
-
| [`@box-kite/core`](#using-the-core-without-react) | The engine and the types, no framework. For an app with no React. |
|
|
45
|
-
|
|
46
|
-
`@box-kite/core/types` is where a project declares its own props and components — the augmentation
|
|
47
|
-
target lives with the prop registry it extends, not with a binding.
|
|
48
|
-
|
|
49
|
-
2. Use it
|
|
50
|
-
|
|
51
|
-
Spacing counts in quarters of a rem: `p={3}` is `0.75rem`, `m={2}` is `0.5rem`. The example below is
|
|
52
|
-
a box with `margin: 0.5rem` and `padding: 1.75rem`.
|
|
35
|
+
It is built to be **written by an AI**: the rules and a reference measured from the engine ship inside the package, an MCP
|
|
36
|
+
server checks a prop bag against the real engine, and the compiler refuses a value the library would not render.
|
|
53
37
|
|
|
38
|
+
<!-- prettier-ignore -->
|
|
54
39
|
```tsx
|
|
55
|
-
import
|
|
40
|
+
import Button from '@box-kite/react/components/button';
|
|
41
|
+
import Flex from '@box-kite/react/components/flex';
|
|
42
|
+
import { H2, P } from '@box-kite/react/components/semantics';
|
|
56
43
|
|
|
57
|
-
export
|
|
44
|
+
export function PlanCard() {
|
|
58
45
|
return (
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
<Flex d="column" gap={3} p={6} md={{ p: 8 }} borderRadius={3} shadow="md"
|
|
47
|
+
bgColor="white" theme={{ dark: { bgColor: 'slate-900' } }}>
|
|
48
|
+
<H2 fontSize={20} fontWeight={600}>Pro plan</H2>
|
|
49
|
+
<P color="slate-600">Every value on this card is checked by the compiler.</P>
|
|
50
|
+
<Button hover={{ bgColor: 'indigo-600' }}>Upgrade</Button>
|
|
51
|
+
</Flex>
|
|
62
52
|
);
|
|
63
53
|
}
|
|
64
54
|
```
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
are plain pixels (`b={1}` is 1px).
|
|
56
|
+
Each prop becomes one shared class: `p={6}` is `padding: 1.5rem`, `md` wraps it in a media query, `theme` scopes it to the dark
|
|
57
|
+
subtree, and the same value anywhere else in the app reuses the same rule.
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
the CSS it writes — `p={4} → padding: 1rem`, `fontSize={14} → font-size: 0.875rem` — and those lines
|
|
72
|
-
are generated by running the engine, so the editor cannot tell you a scale the library stopped using.
|
|
73
|
-
|
|
74
|
-
## Colours
|
|
75
|
-
|
|
76
|
-
The palette is Tailwind's, in OKLCH: twenty-six families of eleven steps — `slate-50` through
|
|
77
|
-
`rose-950`, the four Tailwind 4.3 added (`mauve`, `mist`, `olive`, `taupe`) included — each one a CSS
|
|
78
|
-
variable written with its own value as the fallback, so a page declares none of them in `:root` and
|
|
79
|
-
any stylesheet can override one. Any colour value takes an opacity modifier:
|
|
80
|
-
|
|
81
|
-
```JS
|
|
82
|
-
<Box bgColor="blue-500/40" borderColor="black/10" hover={{ bgColor: 'blue-500/60' }} />
|
|
83
|
-
```
|
|
59
|
+
## ⚡ Quick start
|
|
84
60
|
|
|
85
|
-
|
|
86
|
-
**variable**, so a themed token is still themed and everything asking for the same value still shares
|
|
87
|
-
one class. It is not `opacity`, which fades the element, its text and its children — this fades one
|
|
88
|
-
declaration. Every colour prop takes it (`color`, `bgColor`, `borderColor`, `outlineColor`, `fill`,
|
|
89
|
-
`stroke`), as does a `vars` entry and a variable of your own from `Box.extend()`.
|
|
61
|
+
Start a new Vite + React app with a design, a sample page and the agent setup already in it:
|
|
90
62
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
A gradient is a value, not a string you assemble: the key names the kind and carries its geometry, and
|
|
94
|
-
the stops are palette colours, so the whole thing is themed, takes the opacity modifier and shares one
|
|
95
|
-
class with every other element asking for it.
|
|
96
|
-
|
|
97
|
-
```JS
|
|
98
|
-
<Box bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }} />
|
|
99
|
-
<Box bgGradient={{ radial: 'circle', at: 'top left', colors: [['sky-500', '20%'], 'indigo-900'] }} />
|
|
100
|
-
<Box bgGradient={{ conic: 45, colors: ['red-500', 'yellow-500', 'red-500'], interpolate: 'oklch' }} />
|
|
63
|
+
```bash
|
|
64
|
+
npm create @box-kite@latest
|
|
101
65
|
```
|
|
102
66
|
|
|
103
|
-
|
|
104
|
-
not. `oklch-longer` takes the long way round the hue circle, which is what turns two stops into a
|
|
105
|
-
spectrum. A gradient is judged whole — one unknown stop or one misspelt key and it emits no rule at
|
|
106
|
-
all, rather than quietly painting something else.
|
|
107
|
-
|
|
108
|
-
`box-shadow` is one CSS property, which normally means one shadow wins. Here it is **four layers** that
|
|
109
|
-
compose, so an elevation, a ring and an inner hairline coexist:
|
|
67
|
+
Or add it to the app you have:
|
|
110
68
|
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
<Box insetShadow="sm" insetRing={1} shadow="lg" shadowColor="blue-500/40" />
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
`shadow` is Tailwind's elevation scale (`xxs` through `xxl`, and the older `small`/`medium`/`large`
|
|
117
|
-
presets still work), `insetShadow` draws inside the border box, and `ring`/`insetRing` are a width in
|
|
118
|
-
px. A ring follows `borderRadius` and costs no layout, which is what makes it different from
|
|
119
|
-
`outline`. Each layer has its own colour prop — `shadowColor`, `ringColor`, and so on — which shows
|
|
120
|
-
nothing until the layer it belongs to is painted, exactly as `borderColor` needs a border width.
|
|
121
|
-
`textShadow` and `textShadowColor` are the text-side pair.
|
|
122
|
-
|
|
123
|
-
Filters stack the same way, and for the same reason: `filter` is one property whose value is a list, so
|
|
124
|
-
each of the nine functions sets a layer of its own and all nine write one composed declaration.
|
|
125
|
-
|
|
126
|
-
```JS
|
|
127
|
-
<Box blur="sm" grayscale={100} />
|
|
128
|
-
<Box brightness={110} saturate={150} hueRotate={90} />
|
|
129
|
-
<Box dropShadow="lg" dropShadowColor="indigo-500/40" />
|
|
130
|
-
<Box backdropBlur="sm" backdropSaturate={180} bgColor="white/20" />
|
|
69
|
+
```bash
|
|
70
|
+
npm install @box-kite/react
|
|
131
71
|
```
|
|
132
72
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
73
|
+
React 18 or newer, and **no third-party dependencies**: the install is Box Kite and its own engine, nothing else. TypeScript is
|
|
74
|
+
optional, but it is where the library shines.
|
|
75
|
+
|
|
76
|
+
## ✨ Why Box Kite
|
|
77
|
+
|
|
78
|
+
<table>
|
|
79
|
+
<tr>
|
|
80
|
+
<td width="50%" valign="top">
|
|
81
|
+
<h4>🤖 AI-first</h4>
|
|
82
|
+
<p>The package carries its own <code>AGENTS.md</code>, a <code>docs/</code> folder and a skill, all generated from the prop
|
|
83
|
+
registry so they can never be a version behind. An MCP server answers mid-task.</p>
|
|
84
|
+
</td>
|
|
85
|
+
<td width="50%" valign="top">
|
|
86
|
+
<h4>🔒 Typed down to the value</h4>
|
|
87
|
+
<p>Every prop accepts a closed set: palette tokens, scale steps, keywords. A typo is a compile error with a
|
|
88
|
+
<i>“Did you mean”</i>, not a silently missing style.</p>
|
|
89
|
+
</td>
|
|
90
|
+
</tr>
|
|
91
|
+
<tr>
|
|
92
|
+
<td valign="top">
|
|
93
|
+
<h4>🧩 Zero config, zero dependencies</h4>
|
|
94
|
+
<p>No CSS files, no PostCSS, no bundler plugin, no provider — and no third-party packages in your lockfile. Styles are
|
|
95
|
+
generated on demand and deduplicated by content.</p>
|
|
96
|
+
</td>
|
|
97
|
+
<td valign="top">
|
|
98
|
+
<h4>🖥️ Server Components</h4>
|
|
99
|
+
<p>Renders in a React Server Component with no <code>'use client'</code>: each rule travels with the markup as a
|
|
100
|
+
<code><style></code> React 19 hoists and dedupes.</p>
|
|
101
|
+
</td>
|
|
102
|
+
</tr>
|
|
103
|
+
<tr>
|
|
104
|
+
<td valign="top">
|
|
105
|
+
<h4>♿ Accessible components</h4>
|
|
106
|
+
<p>Dialog, Menu, Tabs, Combobox, DatePicker, DataGrid and more — each with the ARIA and keyboard of its APG pattern, swept
|
|
107
|
+
by axe in CI.</p>
|
|
108
|
+
</td>
|
|
109
|
+
<td valign="top">
|
|
110
|
+
<h4>🎨 Modern CSS, as props</h4>
|
|
111
|
+
<p>OKLCH palette, themes, container queries, <code>@starting-style</code>, anchor positioning, view transitions, scroll
|
|
112
|
+
timelines — and the top layer instead of <code>z-index</code>.</p>
|
|
113
|
+
</td>
|
|
114
|
+
</tr>
|
|
115
|
+
</table>
|
|
116
|
+
|
|
117
|
+
## 🤖 Built for AI agents
|
|
118
|
+
|
|
119
|
+
A model's memory of a CSS-in-JS API is usually a memory of a _different_ API — the prop names here collide with Tailwind's
|
|
120
|
+
and Chakra's while the numbers mean something else. So Box Kite does not rely on what a model remembers:
|
|
121
|
+
|
|
122
|
+
| What | How you get it |
|
|
123
|
+
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
124
|
+
| **`AGENTS.md` + `docs/`** inside the npm package | Automatic — `node_modules/@box-kite/react/AGENTS.md`, with every prop and the CSS it writes |
|
|
125
|
+
| **Project wiring** (`AGENTS.md`, `CLAUDE.md`, MCP) | `npm create @box-kite@latest -- --agents-only` — merges into an existing repo, and updates on a rerun |
|
|
126
|
+
| **Skill** for Claude Code, Cursor, Codex, Copilot… | `npx skills add box-kite/box-kite` |
|
|
127
|
+
| **MCP server** — no key, no network | `claude mcp add box-kite -- npx -y @box-kite/mcp` (or the stdio config your editor takes) |
|
|
128
|
+
| **llms.txt** and markdown docs | [box-kite.dev/llms.txt](https://www.box-kite.dev/llms.txt) — append `.md` to any docs URL |
|
|
129
|
+
|
|
130
|
+
**`check_styles` runs your props through the real engine.** A value the library does not accept writes no rule and no class
|
|
131
|
+
name — by design, so a typo never emits a broken declaration. The MCP tool makes that visible before anything renders:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
✅ p → .p-4{padding:1rem}
|
|
135
|
+
❌ bgColor does not accept "blue-550" — no rule and no class name were written.
|
|
136
|
+
✅ fontSize → .fontSize-14{font-size:0.875rem}
|
|
137
|
+
⚠️ href an HTML attribute, not a style prop. Written at the top level it typechecks
|
|
138
|
+
and is then dropped — it goes in props={{ "href": … }}.
|
|
148
139
|
```
|
|
149
140
|
|
|
150
|
-
|
|
141
|
+
**The dev console is written as a prompt.** Each warning says what rendered and the line to write instead, so an agent reading
|
|
142
|
+
a test run fixes itself:
|
|
151
143
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
```JS
|
|
159
|
-
import Box from "@box-kite/react";
|
|
144
|
+
```text
|
|
145
|
+
[box-kite] bgColor="bleu-500" wrote no CSS: bgColor does not take that value, so the prop
|
|
146
|
+
was dropped along with its class. Did you mean "blue-500"?
|
|
147
|
+
[box-kite] fontSize={4} is 4px text: fontSize's divider is 16, not the spacing scale's 4,
|
|
148
|
+
so the number is the pixel size. For 16px write fontSize={16}.
|
|
160
149
|
```
|
|
161
150
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
| `H1`–`H6`, `P`, `Span`, `Link`, `Img`, `Nav`, `Header`, `Main`, `Section`, … | `components/semantics` | one `Box` per semantic tag — 25 of them |
|
|
169
|
-
| `Svg`, `Path`, `Circle`, `Rect`, `SvgText`, … | `components/svg` | one component per SVG element — 20 of them, so a drawing never writes `tag` |
|
|
170
|
-
| `Icon` | `components/icon` | Box props on an icon somebody else drew — lucide, Tabler, react-icons, a raw `<svg>` |
|
|
171
|
-
| `Sparkline`, `ProgressRing`, `Gauge`, `MiniDonut` | `components/chart` | chart micro-primitives over those elements — a chart that takes Box props and no chart library |
|
|
172
|
-
| `ChartContainer` | `components/chart` | the theming bridge: the variables a Recharts (or any) chart reads, declared in both themes |
|
|
173
|
-
| `BaseSvg` | `components/baseSvg` | deprecated: `Svg` with the 24×24 icon preset |
|
|
174
|
-
| `VisuallyHidden` | `components/visuallyHidden` | text for a screen reader only — clipped away rather than hidden, so it stays in the a11y tree |
|
|
175
|
-
|
|
176
|
-
### Form controls
|
|
177
|
-
|
|
178
|
-
| Component | Module | What it is |
|
|
179
|
-
| ------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
180
|
-
| `Button` | `components/button` | `<button>`, with `type` and `onClick` lifted out of `props` |
|
|
181
|
-
| `Textbox` | `components/textbox` | `<input>` |
|
|
182
|
-
| `Textarea` | `components/textarea` | `<textarea>` |
|
|
183
|
-
| `Checkbox` | `components/checkbox` | a real `<input type="checkbox">`; `label` renders the wrapping `<label>` for you, and `indeterminate` reports `aria-checked="mixed"` |
|
|
184
|
-
| `Switch` | `components/switch` | the same input as `role="switch"` — Enter toggles it as well as Space |
|
|
185
|
-
| `RadioGroup`, `RadioGroup.Item` | `components/radioGroup` | the APG radio group: one shared `name`, one tab stop, arrow keys between the options |
|
|
186
|
-
| `RadioButton` | `components/radioButton` | a single labelled `<input type="radio">`, for a group you assemble yourself |
|
|
187
|
-
| `Select` | `components/select` | a `Dropdown` over a list of rows: `data={rows} def={{ valueKey, displayKey }}` |
|
|
188
|
-
| `Form` | `components/form` | a `<form>` that reads its own fields on submit and hands them to you as one object |
|
|
189
|
-
|
|
190
|
-
### Overlays and data
|
|
191
|
-
|
|
192
|
-
| Component | Module | What it is |
|
|
193
|
-
| ----------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
194
|
-
| `Overlay` | `components/overlay` | the positioning primitive: a portal rendered where it is declared, so its children escape `overflow: hidden`. No ARIA, no open state, no dismissal |
|
|
195
|
-
| `Tooltip` | `components/tooltip` | the APG tooltip on top of `Overlay` — hover _and_ focus, `aria-describedby`, Escape, hoverable (WCAG 1.4.13) |
|
|
196
|
-
| `Dropdown`, `Dropdown.Item`, `Dropdown.Display` | `components/dropdown` | the APG combobox: select-only, or the editable one with `isSearchable` |
|
|
197
|
-
| `DataGrid` | `components/dataGrid` | the APG grid — sorting, filtering, grouping, row selection, row details, virtualized rows and columns, and a keyboard that covers all of it |
|
|
198
|
-
|
|
199
|
-
Two whose shape is not obvious from a table:
|
|
151
|
+
**And a UI a model writes at runtime is a first-class citizen.** `catalog()` turns every component and every value its props
|
|
152
|
+
take into JSON Schema — the shape structured output, json-render and A2UI want — and `<SpecRenderer>` renders what comes back,
|
|
153
|
+
checking each node against an allow-list you control. `toolPart()` and `a2uiToSpec()` read AI SDK, assistant-ui, CopilotKit,
|
|
154
|
+
AG-UI and A2UI, and `<ToolCallCard>`, `<ApprovalCard>`, `<Reasoning>` and `<StreamingText>` draw an agent's turn.
|
|
155
|
+
→ [Generative UI](https://www.box-kite.dev/generative-ui/) · [Agent components](https://www.box-kite.dev/agent/) ·
|
|
156
|
+
[Interop](https://www.box-kite.dev/interop/)
|
|
200
157
|
|
|
201
|
-
|
|
202
|
-
// The tooltip's trigger is a render prop, so `aria-describedby` lands on the control itself
|
|
203
|
-
// rather than on a wrapper around it.
|
|
204
|
-
import Tooltip from '@box-kite/react/components/tooltip';
|
|
158
|
+
## 🔒 Typed down to the value
|
|
205
159
|
|
|
206
|
-
|
|
207
|
-
|
|
160
|
+
Props are not `string`. Colours are palette tokens (with an optional `/opacity`), spacing is a scale, keywords are unions — so
|
|
161
|
+
the editor autocompletes them and the compiler catches the rest:
|
|
208
162
|
|
|
163
|
+
<!-- prettier-ignore -->
|
|
209
164
|
```tsx
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
import Form from '@box-kite/react/components/form';
|
|
213
|
-
import Textbox from '@box-kite/react/components/textbox';
|
|
214
|
-
|
|
215
|
-
interface Credentials {
|
|
216
|
-
email: string;
|
|
217
|
-
password: string;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
<Form<Credentials> p={4} onSubmit={(values) => signIn(values)}>
|
|
221
|
-
<Textbox name="email" type="email" />
|
|
222
|
-
<Textbox name="password" type="password" />
|
|
223
|
-
<Button type="submit">Sign in</Button>
|
|
224
|
-
</Form>;
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
`onSubmit(values, event)` runs after the event's `preventDefault()`. A field is in `values` if it
|
|
228
|
-
has a `name`: a lone checkbox or radio contributes its `checked` boolean, a name used more than
|
|
229
|
-
once collects the checked or entered values into an array, and a dotted name (`address.city`,
|
|
230
|
-
`lines[0].qty`) nests. Everything else arrives as the string the DOM holds — the type argument
|
|
231
|
-
describes the object you expect, it is not checked against the fields.
|
|
232
|
-
|
|
233
|
-
## SVG
|
|
234
|
-
|
|
235
|
-
Twenty-three SVG properties are Box props, so a shape is themed, hovered and made responsive the
|
|
236
|
-
same way a `<div>` is — and there is a component for every SVG element, so a drawing never writes
|
|
237
|
-
`tag` either.
|
|
238
|
-
|
|
239
|
-
```JSX
|
|
240
|
-
import { Path, Svg } from '@box-kite/react/components/svg';
|
|
241
|
-
|
|
242
|
-
<Svg
|
|
243
|
-
viewBox="0 0 200 48"
|
|
244
|
-
width="200px"
|
|
245
|
-
fill="none"
|
|
246
|
-
strokeWidth={3}
|
|
247
|
-
strokeLinecap="round"
|
|
248
|
-
strokeDasharray={320}
|
|
249
|
-
strokeDashoffset={320}
|
|
250
|
-
hover={{ strokeDashoffset: 0 }}
|
|
251
|
-
theme={{ dark: { stroke: 'violet-400' }, light: { stroke: 'violet-600' } }}
|
|
252
|
-
>
|
|
253
|
-
<Path d="M8 40 L56 12 L104 34 L152 8 L192 24" />
|
|
254
|
-
</Svg>;
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
That draws the line on hover, and nothing in it declares a transition: every `<svg>` and the shapes
|
|
258
|
-
inside it already transition on `--svgTransitionTime`, which `prefers-reduced-motion: reduce` sets
|
|
259
|
-
to `0s`.
|
|
260
|
-
|
|
261
|
-
| Prop | Notes |
|
|
262
|
-
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
|
|
263
|
-
| `fill`, `stroke` | any colour variable, or `'none'` |
|
|
264
|
-
| `fillOpacity`, `strokeOpacity` | `0`–`1` in tenths, the same scale as `opacity` |
|
|
265
|
-
| `fillRule` | `'nonzero'`, `'evenodd'` |
|
|
266
|
-
| `strokeWidth`, `strokeDasharray`, `strokeDashoffset`, `strokeMiterlimit` | numbers in **SVG user units — no divider**. A dash pattern with its own gap is a string, `'12 4'` |
|
|
267
|
-
| `strokeLinecap`, `strokeLinejoin` | `'butt'`/`'round'`/`'square'`, `'miter'`/`'round'`/`'bevel'` |
|
|
268
|
-
| `paintOrder` | `'normal'`, `'fill'`, `'stroke'`, `'markers'` — `'stroke'` is how outlined text stays legible |
|
|
269
|
-
| `vectorEffect` | `'none'`, `'non-scaling-stroke'` |
|
|
270
|
-
| `shapeRendering` | `'auto'`, `'optimizeSpeed'`, `'crispEdges'`, `'geometricPrecision'` |
|
|
271
|
-
| `textAnchor` | `'start'`, `'middle'`, `'end'` — which part of a label sits on its `x` |
|
|
272
|
-
| `dominantBaseline` | `'alphabetic'`, `'central'`, `'hanging'`, … — which part sits on its `y` |
|
|
273
|
-
| `cx`, `cy`, `r`, `rx`, `ry`, `x`, `y` | the SVG 2 geometry, in **user units**, or a percentage. `rx`/`ry` also take `'auto'` |
|
|
274
|
-
|
|
275
|
-
Every paint and stroke property here is an inherited one, so setting it on the `<svg>` reaches the
|
|
276
|
-
shapes inside — with two exceptions. `vector-effect` and `dominant-baseline` are not inherited, so
|
|
277
|
-
those two props write a rule that names the element _and_ its descendants: `vectorEffect="non-scaling-stroke"`
|
|
278
|
-
on the `<svg>` keeps a hairline one pixel wide however far the `viewBox` is scaled, and
|
|
279
|
-
`dominantBaseline="central"` on the `<svg>` reaches every label in it.
|
|
280
|
-
|
|
281
|
-
The geometry props are different again. They belong to one shape, so they go on the shape — but they
|
|
282
|
-
are real CSS, which means they transition, and an animated gauge or a growing bar needs no JavaScript:
|
|
283
|
-
|
|
284
|
-
```JSX
|
|
285
|
-
<Circle cx={48} cy={48} r={38} strokeDasharray={239} strokeDashoffset={239} hover={{ strokeDashoffset: 60, r: 40 }} />;
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
Where a prop name and an SVG attribute collide, the element decides which one wins. On `Rect`,
|
|
289
|
-
`width={40}` is forty user units rather than the layout scale's `10rem`; on `Path`, `d` is path data
|
|
290
|
-
rather than `flex-direction`; on `SvgText`, `x` and `y` are the attributes, because the CSS geometry
|
|
291
|
-
properties do not apply to `<text>`. On `Circle` those same names stay the CSS props that transition.
|
|
292
|
-
|
|
293
|
-
### Icons
|
|
294
|
-
|
|
295
|
-
An icon set is somebody else's component, so there is no `tag` that renders one and Box cannot wrap
|
|
296
|
-
it. `Icon` styles it through the one channel it does offer — the `className` it spreads onto its own
|
|
297
|
-
`<svg>`:
|
|
298
|
-
|
|
299
|
-
```JSX
|
|
300
|
-
import Icon from '@box-kite/react/components/icon';
|
|
301
|
-
import { Sun } from 'lucide-react';
|
|
302
|
-
|
|
303
|
-
<Icon size={5} color="amber-500" hover={{ color: 'amber-300' }} label="Sunny">
|
|
304
|
-
<Sun />
|
|
305
|
-
</Icon>;
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
It works with lucide, Tabler, react-icons or an `<svg>` you paste in, and it knows none of their
|
|
309
|
-
APIs: `size` is a number on the ÷4 scale (`size={6}` is 24px, the default) that lands in the class,
|
|
310
|
-
where a CSS declaration outranks the `width`/`height` attributes the icon writes for itself. Without
|
|
311
|
-
a `label` an icon is `aria-hidden`; with one it is `role="img"` and that name. For SVG you draw
|
|
312
|
-
yourself, reach for `Svg` instead — it takes these props directly.
|
|
313
|
-
|
|
314
|
-
Behind it is `useClassNames`, which is public for the same reason: a `motion.div`, a router's
|
|
315
|
-
`NavLink` or a third-party chart takes a `className` and nothing else.
|
|
316
|
-
|
|
317
|
-
```JSX
|
|
318
|
-
import { useClassNames } from '@box-kite/react';
|
|
319
|
-
|
|
320
|
-
const { className, styles } = useClassNames({ color: 'sky-500', hover: { color: 'sky-300' } });
|
|
321
|
-
|
|
322
|
-
<>
|
|
323
|
-
{styles}
|
|
324
|
-
<NavLink to="/" className={className} />
|
|
325
|
-
</>;
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
`styles` is defined in element mode only, where the CSS travels as hoistable `<style>` elements
|
|
329
|
-
rather than going to a stylesheet; elsewhere it is undefined and rendering it costs nothing.
|
|
330
|
-
|
|
331
|
-
### Charts
|
|
332
|
-
|
|
333
|
-
Four micro-primitives, built from the SVG components — the small drawings a dashboard needs rather
|
|
334
|
-
than a chart library:
|
|
335
|
-
|
|
336
|
-
```JSX
|
|
337
|
-
import { Gauge, MiniDonut, ProgressRing, Sparkline } from '@box-kite/react/components/chart';
|
|
338
|
-
|
|
339
|
-
<Sparkline data={[4, 9, 6, 12, 10, 15]} variant="area" color="sky-500" width="7rem" />;
|
|
340
|
-
<ProgressRing value={0.62} color="emerald-500" label="62% complete" />;
|
|
341
|
-
<Gauge value={0.4} sweep={180} color="rose-500" />;
|
|
342
|
-
<MiniDonut data={[5, 3, 2]} />;
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
There are no axes, no legends and no data transformations: what they give you instead is that a
|
|
346
|
-
chart is a Box, so its colour, its size, its dark mode and its hover state are the props you already
|
|
347
|
-
know. A sparkline stretches to whatever box it is in (and keeps its line one width thick, through
|
|
348
|
-
`vectorEffect="non-scaling-stroke"`), a ring's arc eases between values with no animation code, and
|
|
349
|
-
naming follows `Svg`: no `label` means `aria-hidden`, a `label` means `role="img"`.
|
|
350
|
-
|
|
351
|
-
They are cheap enough for one in every row of a virtualized grid, because the part that differs per
|
|
352
|
-
row — the shape — is the `d` attribute, which the styling engine never sees. What _is_ a style prop
|
|
353
|
-
is the paint, which the rows share. `fill` and `stroke` also take `url(#gradient)` and
|
|
354
|
-
`var(--chart-1)` now, so a gradient fill is a value with a theme and a `hover`, not an attribute.
|
|
355
|
-
|
|
356
|
-
### Theming a chart library
|
|
357
|
-
|
|
358
|
-
The other half of charts is the chart library you already use. A `<ChartContainer>` declares the
|
|
359
|
-
variables a chart reads, so the chart itself names no colour at all:
|
|
360
|
-
|
|
361
|
-
```JSX
|
|
362
|
-
import { ChartContainer } from '@box-kite/react/components/chart';
|
|
363
|
-
import { Area, AreaChart, ResponsiveContainer, XAxis } from 'recharts';
|
|
364
|
-
|
|
365
|
-
<ChartContainer series={['revenue', 'cost']} height={60}>
|
|
366
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
367
|
-
<AreaChart data={months}>
|
|
368
|
-
<XAxis dataKey="month" />
|
|
369
|
-
<Area dataKey="revenue" stroke="var(--color-revenue)" fill="var(--color-revenue)" fillOpacity={0.15} />
|
|
370
|
-
<Area dataKey="cost" stroke="var(--color-cost)" fill="var(--color-cost)" fillOpacity={0.15} />
|
|
371
|
-
</AreaChart>
|
|
372
|
-
</ResponsiveContainer>
|
|
373
|
-
</ChartContainer>;
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
It declares `--chart-1` … `--chart-6` in both themes and one `--color-<series>` per series you name,
|
|
377
|
-
so the chart flips light to dark with nothing in its own code changing — and the names are the ones
|
|
378
|
-
the ecosystem already uses, so a chart lifted out of shadcn's charts works unchanged.
|
|
379
|
-
|
|
380
|
-
What makes that cheap is the prop behind it. `vars` is the one prop whose declaration _names_ come
|
|
381
|
-
from its value:
|
|
382
|
-
|
|
383
|
-
```JSX
|
|
384
|
-
<Flex vars={{ 'color-revenue': 'sky-500', 'chart-gap': '4px' }} theme={{ dark: { vars: { 'color-revenue': 'sky-400' } } }}>
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
declares `--color-revenue: var(--sky-500)` and `--chart-gap: 4px` on the element, inherited by
|
|
388
|
-
everything inside it — including markup this library never rendered. A colour token resolves to the
|
|
389
|
-
variable behind it, anything else is written out as it stands. Being an ordinary prop, it nests in a
|
|
390
|
-
theme, a breakpoint and a pseudo-class, and it lands in a **class**: two subtrees declaring the same
|
|
391
|
-
palette share one rule, and nothing needs a `<style>` tag or an `id` to scope it to one chart.
|
|
392
|
-
|
|
393
|
-
## Extend props
|
|
394
|
-
|
|
395
|
-
It is possible to add your own props to define your custom styles.
|
|
396
|
-
You need to do a few steps.
|
|
397
|
-
|
|
398
|
-
1. In a project root file you need to define your extends
|
|
399
|
-
|
|
400
|
-
```JS
|
|
401
|
-
import Box from "@box-kite/react";
|
|
402
|
-
|
|
403
|
-
export const { extendedProps, extendedPropTypes } = Box.extend(
|
|
404
|
-
// css variables
|
|
405
|
-
{
|
|
406
|
-
dark: '#123123',
|
|
407
|
-
light: '#ededed',
|
|
408
|
-
cssVarName: 'cssVarValue',
|
|
409
|
-
myShadow: '10px 5px 5px red',
|
|
410
|
-
myGradient1: 'linear-gradient(#e66465, #9198e5)',
|
|
411
|
-
myGradient2: 'linear-gradient(black, white)',
|
|
412
|
-
},
|
|
413
|
-
// new custom props
|
|
414
|
-
{
|
|
415
|
-
background: [
|
|
416
|
-
{
|
|
417
|
-
values: ['myGradient1', 'myGradient2'] as const,
|
|
418
|
-
valueFormat: (value: string) => `var(--background${value})`,
|
|
419
|
-
},
|
|
420
|
-
],
|
|
421
|
-
},
|
|
422
|
-
// extend values for existing props
|
|
423
|
-
{
|
|
424
|
-
color: [
|
|
425
|
-
{
|
|
426
|
-
values: ['dark', 'light'],
|
|
427
|
-
valueFormat: (value, getVariableValue) => getVariableValue(value),
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
}
|
|
431
|
-
);
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
2. Now you have to add typings to the Box in order to have intellisense for you new props and value.
|
|
435
|
-
You need to create a `box.d.ts`
|
|
436
|
-
|
|
437
|
-
```JS
|
|
438
|
-
import '@box-kite/react';
|
|
439
|
-
import { ExtractBoxStyles, ExtractComponentsAndVariants } from '@box-kite/core/types';
|
|
440
|
-
import { extendedProps, extendedPropTypes, components } from './path-to-your-b0x-extends-declaration';
|
|
441
|
-
|
|
442
|
-
declare module '@box-kite/core/types' {
|
|
443
|
-
namespace Augmented {
|
|
444
|
-
interface BoxProps extends ExtractBoxStyles<typeof extendedProps> {}
|
|
445
|
-
interface BoxPropTypes extends ExtractBoxStyles<typeof extendedPropTypes> {}
|
|
446
|
-
interface ComponentsTypes extends ExtractComponentsAndVariants<typeof components> {}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
```
|
|
165
|
+
<Box bgColor="bleu-500" />
|
|
166
|
+
// ~~~~~~~ ts(2820): Type '"bleu-500"' is not assignable… Did you mean '"blue-500"'?
|
|
450
167
|
|
|
451
|
-
|
|
168
|
+
<Box display="flexx" />
|
|
169
|
+
// ~~~~~~~ ts(2820): … Did you mean '"flex"'?
|
|
452
170
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
a prop does and renders on a server:
|
|
171
|
+
<Box opacity={50} />
|
|
172
|
+
// ~~~~~~~ ts(2322): Type '50' is not assignable to type '0 | 1 | 0.1 | 0.2 | …'
|
|
456
173
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
```
|
|
174
|
+
<Button variant="primaryy">Save</Button>
|
|
175
|
+
// ~~~~~~~ ts(2322): a component's variants are typed too
|
|
460
176
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
property. Prefer a prop, then `Box.extend()` for anything used twice.
|
|
464
|
-
|
|
465
|
-
## Theme for components
|
|
466
|
-
|
|
467
|
-
In the project root file (main.tsx) use `Theme.setup`
|
|
468
|
-
|
|
469
|
-
```JS
|
|
470
|
-
import Box from '@box-kite/react';
|
|
471
|
-
|
|
472
|
-
Box.components({
|
|
473
|
-
button: {
|
|
474
|
-
styles: {
|
|
475
|
-
px: 4
|
|
476
|
-
},
|
|
477
|
-
variants: {
|
|
478
|
-
mytheme: {
|
|
479
|
-
px: 8
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
},
|
|
483
|
-
...
|
|
484
|
-
})
|
|
177
|
+
<Box css={{ mixBlendMod: 'multiply' }} />
|
|
178
|
+
// ~~~~~~~~~~~ ts(2561): … Did you mean to write 'mixBlendMode'?
|
|
485
179
|
```
|
|
486
180
|
|
|
487
|
-
|
|
181
|
+
- **Hover a prop, see its CSS.** Every prop's JSDoc carries an example such as `p={4} → padding: 1rem` — generated by running
|
|
182
|
+
the engine, so the editor can never teach a scale the library stopped using.
|
|
183
|
+
- **Your own tokens join the types.** `Box.extend()` adds variables, props and values; one `declare module` makes them
|
|
184
|
+
autocomplete like the built-ins. `Box.components()` variants are typed the same way.
|
|
185
|
+
- **The escape hatch is typed too.** `css={{ … }}` is checked against `csstype` and still compiles to a shared class — never a
|
|
186
|
+
`style` attribute.
|
|
488
187
|
|
|
489
|
-
|
|
490
|
-
import Button from '@box-kite/react/components/button';
|
|
188
|
+
## 📐 The scales
|
|
491
189
|
|
|
492
|
-
|
|
493
|
-
return <Button>Click me</Button>
|
|
494
|
-
}
|
|
495
|
-
```
|
|
190
|
+
The one thing worth learning by heart — the divider is per prop:
|
|
496
191
|
|
|
497
|
-
|
|
192
|
+
| Prop | Scale | Example |
|
|
193
|
+
| ---------------------------------------- | ------------------- | ---------------------------------------- |
|
|
194
|
+
| `p`, `m`, `gap`, `width`, `borderRadius` | ÷4 (quarter rem) | `p={4}` → `1rem` (16px) |
|
|
195
|
+
| `fontSize` | ÷16 | `fontSize={14}` → `0.875rem` (14px) |
|
|
196
|
+
| `b` (border width), `lineHeight` | px | `b={1}` → `1px` |
|
|
197
|
+
| `animationDuration`, `transitionDelay`… | ms | `transitionDuration={150}` → `150ms` |
|
|
198
|
+
| colours | tokens + `/opacity` | `bgColor="blue-500/40"` → `color-mix(…)` |
|
|
498
199
|
|
|
499
|
-
|
|
500
|
-
|
|
200
|
+
Three conventions follow from "every visual value is a prop": the element comes from the component (`<Flex>`, `<Button>`,
|
|
201
|
+
`<H1>`, `<Path>`), HTML attributes go in `props` (`<Link props={{ href: '/about' }}>`), and there is no `style={{ }}`.
|
|
501
202
|
|
|
502
|
-
|
|
503
|
-
return <Button variant="mytheme">Click me</Button>
|
|
504
|
-
}
|
|
505
|
-
```
|
|
203
|
+
## 🧱 Components
|
|
506
204
|
|
|
507
|
-
|
|
205
|
+
Every component is a `Box` underneath — same props, same theming, same `variant`s — and each is its own entry point, so a
|
|
206
|
+
`Textbox` never pulls the `DataGrid` into your bundle.
|
|
508
207
|
|
|
509
|
-
|
|
208
|
+
| Area | Components |
|
|
209
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
210
|
+
| **Layout & content** | [`Flex`](https://www.box-kite.dev/flex/), [`Grid`](https://www.box-kite.dev/grid/), `H1`–`H6`, `P`, `Link`, `Nav`…, [`List`](https://www.box-kite.dev/list/), [`Svg` + every SVG element](https://www.box-kite.dev/svg/), [`Icon`](https://www.box-kite.dev/icon/), `Skeleton` |
|
|
211
|
+
| **Forms** | [`Button`](https://www.box-kite.dev/button/), [`Textbox`](https://www.box-kite.dev/textbox/), [`Checkbox`](https://www.box-kite.dev/checkbox/), [`Switch`](https://www.box-kite.dev/switch/), [`RadioGroup`](https://www.box-kite.dev/radiobutton/), [`Combobox`](https://www.box-kite.dev/combobox/), [`Slider`](https://www.box-kite.dev/slider/), `Form` |
|
|
212
|
+
| **Dates** | [`DateField`](https://www.box-kite.dev/date-field/), [`Calendar`](https://www.box-kite.dev/calendar/), [`DatePicker`](https://www.box-kite.dev/date-picker/), `TimeZoneSelect` — over a zone-safe [date module](https://www.box-kite.dev/dates/) |
|
|
213
|
+
| **Overlays** | [`Dialog`](https://www.box-kite.dev/dialog/), [`Popover`](https://www.box-kite.dev/popover/), [`Menu`](https://www.box-kite.dev/menu/), [`Tooltip`](https://www.box-kite.dev/tooltip/), [`Toaster`](https://www.box-kite.dev/toaster/) — in the top layer, no portal, no `z-index` |
|
|
214
|
+
| **Disclosure** | [`Tabs`](https://www.box-kite.dev/tabs/), [`Accordion`](https://www.box-kite.dev/accordion/), [`Progress`](https://www.box-kite.dev/progress/) |
|
|
215
|
+
| **Data & dashboards** | [`DataGrid`](https://www.box-kite.dev/datagrid/) (sort, filter, group, edit, paste, virtualized), [`DashboardGrid`](https://www.box-kite.dev/dashboard/), [`Sparkline`, `Gauge`, `ChartContainer`](https://www.box-kite.dev/charts/) |
|
|
216
|
+
| **AI** | [`ToolCallCard`, `ApprovalCard`, `Reasoning`, `StreamingText`](https://www.box-kite.dev/agent/), `markdownComponents`, [`SpecRenderer`](https://www.box-kite.dev/generative-ui/) |
|
|
510
217
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
--outlineColor: black;
|
|
514
|
-
--lineHeight: 1.2;
|
|
515
|
-
--fontSize: 14px;
|
|
516
|
-
--transitionTime: 0.25s;
|
|
517
|
-
```
|
|
218
|
+
The behaviour behind them ships on its own as `@box-kite/react/a11y` — focus return, roving focus, dismissal and controllable
|
|
219
|
+
state — for the widgets you build yourself.
|
|
518
220
|
|
|
519
|
-
##
|
|
520
|
-
|
|
521
|
-
Box renders inside a Server Component with no `'use client'` — the styles travel with the markup
|
|
522
|
-
instead of being injected by an effect. A server component just imports the package as usual:
|
|
221
|
+
## 🖥️ Server Components
|
|
523
222
|
|
|
524
223
|
```tsx
|
|
525
224
|
// app/page.tsx — a Server Component. No 'use client', no provider, no CSS import.
|
|
526
225
|
import Flex from '@box-kite/react/components/flex';
|
|
527
|
-
import { H1
|
|
226
|
+
import { H1 } from '@box-kite/react/components/semantics';
|
|
528
227
|
|
|
529
228
|
export default function Page() {
|
|
530
229
|
return (
|
|
531
|
-
<Flex d="column" gap={2} p={6}
|
|
230
|
+
<Flex d="column" gap={2} p={6}>
|
|
532
231
|
<H1 fontSize={24}>Rendered on the server</H1>
|
|
533
|
-
<P color="slate-600" sm={{ fontSize: 16 }}>
|
|
534
|
-
Its CSS was hoisted into <head> by React, not inserted by a client runtime.
|
|
535
|
-
</P>
|
|
536
232
|
</Flex>
|
|
537
233
|
);
|
|
538
234
|
}
|
|
539
235
|
```
|
|
540
236
|
|
|
541
|
-
The `react-server` export condition resolves to a build of Box
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
**Client components** (anything with `'use client'`) can use the same emission path, and should in an
|
|
547
|
-
RSC app, so their CSS is in the HTML too. One line, in a module the root layout imports:
|
|
548
|
-
|
|
549
|
-
```tsx
|
|
550
|
-
'use client';
|
|
551
|
-
import Box from '@box-kite/react';
|
|
237
|
+
The `react-server` export condition resolves to a hook-free build of Box; each rule comes back as a `<style href precedence>`
|
|
238
|
+
element that React 19 hoists into `<head>` and dedupes, so it is stream-safe and the cascade is held by `@layer`, not by element
|
|
239
|
+
order. → [How it works](https://www.box-kite.dev/server-components/) ·
|
|
240
|
+
[measured against CSS modules](https://www.box-kite.dev/blog/styling-server-components/) ·
|
|
241
|
+
[a whole Next.js app](examples/next-app)
|
|
552
242
|
|
|
553
|
-
|
|
554
|
-
```
|
|
243
|
+
## 📦 Packages
|
|
555
244
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
the result. The consequence worth knowing: **unlayered CSS of your own always wins** over Box
|
|
563
|
-
props here, which matches where the library puts its `<style>` element in the other modes.
|
|
564
|
-
- **Declare `Box.extend()` props before the first render.** CSS appends a layer it meets for the
|
|
565
|
-
first time after every layer already named, so a prop registered mid-render sorts after the
|
|
566
|
-
built-ins.
|
|
567
|
-
- **Class names are content-hashed** in this mode instead of counted, so a class resolved on the
|
|
568
|
-
server matches the one the client bundle resolves for the same props.
|
|
569
|
-
- **Still client-only:** hover-callback children (`{({ isHover }) => …}`) and `Box.Theme`, which
|
|
570
|
-
needs state, storage and a media-query listener. Theme _styles_ are unaffected: a `theme` prop
|
|
571
|
-
generates rules scoped to the theme's own subtree, so setting the theme class and `data-theme` on
|
|
572
|
-
`<html>` in a server component is enough.
|
|
573
|
-
- **Most pre-built components render on the server too.** `Flex`, `Grid`, `Button`, `Textbox`,
|
|
574
|
-
`Textarea`, `RadioButton`, `Icon`, the SVG elements, `VisuallyHidden` and the semantic tags (`H1`, `P`, `Link`, `Nav` …) are
|
|
575
|
-
hook-free wrappers around Box, and their published chunks import the package by name — so the
|
|
576
|
-
`react-server` condition reaches them and they resolve the same hook-free Box. Import them
|
|
577
|
-
straight into a Server Component.
|
|
578
|
-
- **The stateful ones become client boundaries.** `Dropdown`, `Tooltip`, `Overlay`, `DataGrid`,
|
|
579
|
-
`Checkbox`, `Switch`, `RadioGroup`, `Select` and `Form` hold state or measure the DOM, so their chunks ship a
|
|
580
|
-
`'use client'` banner.
|
|
581
|
-
A Server Component may still import one — the bundler opens the boundary for you — but it is
|
|
582
|
-
hydrated like any client component, and its CSS is in the server-rendered HTML only if a client
|
|
583
|
-
module of yours has already called `Box.configure({ sink: 'element' })`.
|
|
584
|
-
- **React 19 only.** On React 18 the elements cannot be hoisted and render inline; keep the default
|
|
585
|
-
sink there (a documented App Router fallback for React 18 is still to come).
|
|
586
|
-
- **Cost:** one base element per page (the reset, `:root`, and the layer order — ~1.8 KB gzipped)
|
|
587
|
-
plus one `<style>` element per distinct rule, deduped across every Box that uses it.
|
|
588
|
-
|
|
589
|
-
A whole Next.js App Router app built this way — streamed Suspense boundary, client island, theme
|
|
590
|
-
toggle, and a page of pre-built components rendered on the server — is in
|
|
591
|
-
[`examples/next-app`](examples/next-app): `npm run build:next-app` and `npm run smoke:next-app`,
|
|
592
|
-
which starts the production server and asserts on the HTML it serves.
|
|
593
|
-
|
|
594
|
-
## Behaviour primitives for your own components
|
|
595
|
-
|
|
596
|
-
Every accessible widget needs the same mechanics, and none of them are visible in the markup:
|
|
597
|
-
return focus to whatever opened a layer, move through a list with the arrow keys, close on Escape
|
|
598
|
-
or a press outside, hold a value the consumer may or may not own, generate ids that wire one
|
|
599
|
-
element to another. They are what this library's components are built from, and they ship on their
|
|
600
|
-
own for the patterns it does not cover:
|
|
245
|
+
| Package | What it is |
|
|
246
|
+
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
247
|
+
| [`@box-kite/react`](https://www.npmjs.com/package/@box-kite/react) | `Box`, every component and hook; depends on `@box-kite/core` and nothing else |
|
|
248
|
+
| [`@box-kite/core`](https://www.npmjs.com/package/@box-kite/core) | the styling engine with no framework and no dependencies — `createStyleEngine().classNames(props)` ([example](examples/vanilla)) |
|
|
249
|
+
| [`@box-kite/mcp`](https://www.npmjs.com/package/@box-kite/mcp) | the MCP server: docs search, props, components, rules and `check_styles` |
|
|
250
|
+
| [`@box-kite/create`](https://www.npmjs.com/package/@box-kite/create) | `npm create @box-kite` — a Vite + React app with a design and the agent setup |
|
|
601
251
|
|
|
602
|
-
|
|
603
|
-
import { useControllableState, useDismiss, useFocusReturn, useIdentifier, useRovingFocus } from '@box-kite/react/a11y';
|
|
604
|
-
import VisuallyHidden from '@box-kite/react/components/visuallyHidden';
|
|
252
|
+
## 📚 Learn more
|
|
605
253
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
254
|
+
- **[Documentation](https://www.box-kite.dev)** — every prop and component, with live demos
|
|
255
|
+
- **[Start with AI](https://www.box-kite.dev/start-with-ai/)** — build an app in three prompts with Claude Code, Cursor, Codex or Copilot
|
|
256
|
+
- **[Built for AI](https://www.box-kite.dev/ai-context/)** — what ships for agents, and why
|
|
257
|
+
- **[Theming](https://www.box-kite.dev/theme-setup/)**, **[Extending](https://www.box-kite.dev/box-functions/)**,
|
|
258
|
+
**[Escape hatch](https://www.box-kite.dev/escape-hatch/)**
|
|
259
|
+
- **[`BOX_KITE_AI_CONTEXT.md`](src/BOX_KITE_AI_CONTEXT.md)** — the whole surface on one page
|
|
609
260
|
|
|
610
|
-
|
|
261
|
+
## 🤝 Contributing
|
|
611
262
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
```
|
|
263
|
+
Issues and pull requests are welcome — start with [CONTRIBUTING.md](CONTRIBUTING.md), which covers the architecture and the
|
|
264
|
+
framework-free core boundary. Questions go to [SUPPORT.md](SUPPORT.md), vulnerabilities to [SECURITY.md](SECURITY.md).
|
|
616
265
|
|
|
617
|
-
|
|
618
|
-
any CSS you like. Changes are **event-sourced**: `onChange(value, { reason, event })` tells you
|
|
619
|
-
_why_ something closed (`'escape'`, `'outside-pointer'`, a reason of your own), which is what makes
|
|
620
|
-
a controlled component controllable in practice. Roles and ARIA are deliberately left to you: a
|
|
621
|
-
listbox and a menu navigate identically and are named completely differently.
|
|
622
|
-
|
|
623
|
-
Full reference: [docs/a11y-primitives.md](docs/a11y-primitives.md).
|
|
624
|
-
|
|
625
|
-
## Using the core without React
|
|
626
|
-
|
|
627
|
-
The engine that turns props into CSS has no framework in it, and it is published on its own:
|
|
628
|
-
|
|
629
|
-
```js
|
|
630
|
-
import { createStyleEngine } from '@box-kite/core';
|
|
631
|
-
|
|
632
|
-
const engine = createStyleEngine();
|
|
633
|
-
|
|
634
|
-
document.querySelector('#card').className = engine.classNames({
|
|
635
|
-
p: 4,
|
|
636
|
-
bgColor: 'blue-500',
|
|
637
|
-
borderRadius: 2,
|
|
638
|
-
hover: { bgColor: 'blue-600' },
|
|
639
|
-
md: { p: 8 },
|
|
640
|
-
});
|
|
641
|
-
```
|
|
266
|
+
If Box Kite saves you time, a ⭐ on [GitHub](https://github.com/box-kite/box-kite) helps other people find it.
|
|
642
267
|
|
|
643
|
-
|
|
644
|
-
themes, `component`/`variant` — and the CSS behind them is written to a `<style>` element the
|
|
645
|
-
engine creates, on its own microtask. Nothing has to be flushed, mounted or provided.
|
|
646
|
-
|
|
647
|
-
```js
|
|
648
|
-
const engine = createStyleEngine();
|
|
649
|
-
|
|
650
|
-
// Custom variables and props, the vanilla form of Box.extend()
|
|
651
|
-
engine.extend(
|
|
652
|
-
{ brand: '#6d28d9' },
|
|
653
|
-
{},
|
|
654
|
-
{ bgColor: [{ styleName: 'background-color', values: ['brand'], valueFormat: (v, get) => get(v) }] },
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
// Component defaults with variants, the vanilla form of Box.components()
|
|
658
|
-
engine.components({ panel: { styles: { p: 6, borderRadius: 3 }, variants: { accent: { b: 2, borderColor: 'brand' } } } });
|
|
659
|
-
|
|
660
|
-
engine.classNames({ component: 'panel', variant: 'accent' });
|
|
661
|
-
engine.flushSync(); // only when computed styles are read in the same tick
|
|
662
|
-
engine.getStyles(); // the stylesheet as text, for static output
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
Theming is the same state machine `<Box.Theme>` runs, as a plain object:
|
|
666
|
-
|
|
667
|
-
```js
|
|
668
|
-
import { createThemeController } from '@box-kite/core';
|
|
669
|
-
|
|
670
|
-
// Reads prefers-color-scheme, restores a stored choice, writes the theme class and data-theme onto
|
|
671
|
-
// <html>, and follows the system preference until something overrides it. A theme rule is scoped to
|
|
672
|
-
// the subtree that theme owns (`@scope (.dark) to ([data-theme])`), so one class restyles everything
|
|
673
|
-
// inside it and a nested theme takes over from there.
|
|
674
|
-
const theme = createThemeController({ storageKey: 'theme' });
|
|
675
|
-
|
|
676
|
-
theme.subscribe((name) => console.log('theme is now', name));
|
|
677
|
-
theme.set('dark'); // theme.set(null) hands control back to the system
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
Notes:
|
|
681
|
-
|
|
682
|
-
- **The engine is explicit here.** The React entries share one lazily-created default instance so
|
|
683
|
-
`Box` and `Box.extend()` agree with no configuration; a vanilla app has no such ambient thing to
|
|
684
|
-
agree with, and separate instances are what keeps a widget independent of its host page.
|
|
685
|
-
- **No React reaches this entry**, in the sources or in the bundle — both are checked in CI. It is
|
|
686
|
-
14 KB gzipped, all engine.
|
|
687
|
-
- `sink: 'string'` collects CSS in memory with no DOM (`getStyles()` reads it back), which is how
|
|
688
|
-
server rendering works; `sink: 'element'` is the React 19 mode above and has no meaning here, so
|
|
689
|
-
`classNames()` refuses to run in it.
|
|
690
|
-
|
|
691
|
-
A complete page — props, pseudo-classes, breakpoints, themes, `extend`, `components`, no framework
|
|
692
|
-
— is in [`examples/vanilla`](examples/vanilla): `npm run dev:vanilla`.
|
|
693
|
-
|
|
694
|
-
## Architecture
|
|
695
|
-
|
|
696
|
-
The styling engine is framework-free. Everything that generates CSS — the prop definitions,
|
|
697
|
-
the value formatters, class-name generation, rule ordering, the style sinks (CSSOM, `textContent`,
|
|
698
|
-
string for SSR, style elements for React 19), the flush scheduler, CSS variables and the theme
|
|
699
|
-
runtime — lives in `src/core/` and imports no React at all. CI fails the build if it ever does
|
|
700
|
-
(`npm run check:boundaries`), and it ships on its own as
|
|
701
|
-
[`@box-kite/core`](#using-the-core-without-react).
|
|
702
|
-
|
|
703
|
-
React is a thin adapter on top of it:
|
|
704
|
-
|
|
705
|
-
| | Files | Lines | Share |
|
|
706
|
-
| ---------------------------------------------------------- | ----- | ----- | -------- |
|
|
707
|
-
| Core engine (`src/core/`, `core.ts`) | 23 | 5,702 | 90.9% |
|
|
708
|
-
| React binding (`src/react/`, `box.ts`, `rsc.ts`, `ssg.ts`) | 14 | 573 | **9.1%** |
|
|
709
|
-
|
|
710
|
-
The binding is the whole React-specific surface: resolve class names during render, flush the
|
|
711
|
-
pending rules from `useInsertionEffect`, render the style elements of the Server-Component path,
|
|
712
|
-
and hold the theme state. React feature code the components share sits alongside it and is counted
|
|
713
|
-
separately — three helper hooks (`useVisibility`, `usePortalContainer`, `useVirtualization`), the
|
|
714
|
-
modules behind [`@box-kite/react/a11y`](#behaviour-primitives-for-your-own-components), the
|
|
715
|
-
markup the form controls share, and the ARIA the SVG and chart components share, 911 lines together. A Vue adapter would need its own arrow-key navigation for the same reason it
|
|
716
|
-
would need its own components, which says nothing about how much of the styling engine is
|
|
717
|
-
React-specific.
|
|
718
|
-
|
|
719
|
-
That ratio is not theoretical: the same engine is driven straight from the DOM in
|
|
720
|
-
[`examples/vanilla`](examples/vanilla) with no framework loaded, and from a server with no
|
|
721
|
-
`document` in `ssg.ts`. Numbers are printed by `npm run check:boundaries` and refreshed with the
|
|
722
|
-
code.
|
|
723
|
-
|
|
724
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md#the-core-boundary) for the boundary rules and where new code
|
|
725
|
-
belongs.
|
|
726
|
-
|
|
727
|
-
## AI editors and agents
|
|
728
|
-
|
|
729
|
-
The package ships its own instructions, because an agent has priors about a library this new and they
|
|
730
|
-
are wrong. `AGENTS.md` at the package root is the file agents look for — the rules, the dividers, one
|
|
731
|
-
worked example — and `docs/` sits beside it: every prop with the CSS it writes (`docs/props.md`),
|
|
732
|
-
every component with its import and whether it renders on a server (`docs/components.md`), and the
|
|
733
|
-
behaviour hooks (`docs/a11y.md`). All of it is generated during the build from the prop registry, the
|
|
734
|
-
rules file and the built chunks' own exports, so none of it can drift from the library it describes.
|
|
735
|
-
|
|
736
|
-
```shell
|
|
737
|
-
npm create @box-kite@latest -- --agents-only
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
That adds a root `AGENTS.md` (merged into yours, if you have one) with the four facts a model gets wrong
|
|
741
|
-
and a pointer to the package's own, so an upgrade brings the rules with it; a `CLAUDE.md` importing it;
|
|
742
|
-
and the MCP server below, in `.mcp.json` and in the config of each editor whose folder is there. Run again,
|
|
743
|
-
it updates rather than repeats. `npm create @box-kite` writes all of it into a new app already
|
|
744
|
-
(`--no-agents` leaves it out).
|
|
745
|
-
|
|
746
|
-
Codex, Cursor, Copilot, VS Code, Windsurf, Cline and Zed read a root `AGENTS.md` natively; Claude
|
|
747
|
-
Code reads `CLAUDE.md`, which is why the one line `@AGENTS.md` is there. The long-form
|
|
748
|
-
[BOX_KITE_AI_CONTEXT.md](src/BOX_KITE_AI_CONTEXT.md) ships in the tarball too.
|
|
749
|
-
|
|
750
|
-
The same rules are also a skill, which roughly forty-five coding agents install with one command:
|
|
751
|
-
|
|
752
|
-
```shell
|
|
753
|
-
npx skills add box-kite/box-kite # Claude Code, Cursor, Codex, Copilot, Gemini CLI, Zed…
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
In Claude Code this repository is also a plugin marketplace — `/plugin marketplace add box-kite/box-kite`
|
|
757
|
-
then `/plugin install box-kite@box-kite` — and Cursor takes the rules as a rule file of its own:
|
|
758
|
-
|
|
759
|
-
```shell
|
|
760
|
-
mkdir -p .cursor/rules && cp node_modules/@box-kite/react/.cursor/rules/box-kite.mdc .cursor/rules/
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
Both are generated from the same rules file as `AGENTS.md`, and both are served by the docs site
|
|
764
|
-
([skill.md](https://www.box-kite.dev/skill.md), [box-kite.mdc](https://www.box-kite.dev/box-kite.mdc))
|
|
765
|
-
for an agent that can fetch a URL but not run a command.
|
|
766
|
-
|
|
767
|
-
### The MCP server
|
|
768
|
-
|
|
769
|
-
A file is read once, at the start; an MCP server is asked mid-task, which is when the question comes
|
|
770
|
-
up. `@box-kite/mcp` is six tools at capability level — `search_docs`, `get_props`, `get_component`,
|
|
771
|
-
`check_styles`, `get_rules`, `get_blocks` — with no key, no network and no state, because the
|
|
772
|
-
references and the engine are built into it.
|
|
773
|
-
|
|
774
|
-
```shell
|
|
775
|
-
claude mcp add box-kite -- npx -y @box-kite/mcp # or the stdio config your editor takes
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
`check_styles` is the one a file cannot replace. Every prop here accepts a closed set of values, and
|
|
779
|
-
a value it does not accept writes **no rule and no class name** — silently, by design, so that a typo
|
|
780
|
-
never emits a broken declaration. It is the right behaviour and it is invisible, so the tool hands
|
|
781
|
-
your props to the real engine and reports what each one wrote:
|
|
782
|
-
|
|
783
|
-
```shell
|
|
784
|
-
✅ p → .p-4{padding:1rem}
|
|
785
|
-
❌ bgColor does not accept "blue-550" — no rule and no class name were written.
|
|
786
|
-
✅ fontSize → .fontSize-14{font-size:0.875rem}
|
|
787
|
-
⚠️ href an HTML attribute, not a style prop. It goes in props={{ "href": … }}.
|
|
788
|
-
```
|
|
268
|
+
## License
|
|
789
269
|
|
|
790
|
-
|
|
791
|
-
it runs 1, 2, 4 and 8 through the engine and shows what came out. See [mcp/README.md](mcp/README.md).
|
|
270
|
+
[MIT](LICENSE) © Maxim Procopenco
|