@box-kite/react 0.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/.claude/rules/box-kite-rules.md +65 -0
  2. package/.claude/skills/box-kite/SKILL.md +138 -0
  3. package/.claude/skills/box-kite/references/components.md +235 -0
  4. package/.claude/skills/box-kite/references/extending.md +42 -0
  5. package/.claude/skills/box-kite/references/patterns.md +27 -0
  6. package/.claude/skills/box-kite/references/styling.md +222 -0
  7. package/.cursor/rules/box-kite.mdc +73 -0
  8. package/AGENTS.md +102 -0
  9. package/BOX_AI_CONTEXT.md +2804 -0
  10. package/BOX_KITE_AI_CONTEXT.md +2804 -0
  11. package/LICENSE +21 -0
  12. package/README.md +744 -7
  13. package/a11y.cjs +1 -0
  14. package/a11y.d.ts +15 -0
  15. package/a11y.mjs +4 -0
  16. package/anchor.cjs +1 -0
  17. package/anchor.d.ts +14 -0
  18. package/anchor.mjs +3 -0
  19. package/behavior.cjs +1 -0
  20. package/behavior.mjs +277 -0
  21. package/box.cjs +1 -0
  22. package/box.d.ts +46 -0
  23. package/box.mjs +23 -0
  24. package/client.cjs +1 -0
  25. package/client.mjs +164 -0
  26. package/components/accordion.cjs +1 -0
  27. package/components/accordion.d.ts +114 -0
  28. package/components/accordion.mjs +251 -0
  29. package/components/baseSvg.cjs +1 -0
  30. package/components/baseSvg.d.ts +5 -0
  31. package/components/baseSvg.mjs +21 -0
  32. package/components/button.cjs +1 -0
  33. package/components/button.d.ts +19 -0
  34. package/components/button.mjs +19 -0
  35. package/components/chart.cjs +1 -0
  36. package/components/chart.d.ts +83 -0
  37. package/components/chart.mjs +236 -0
  38. package/components/checkbox.cjs +1 -0
  39. package/components/checkbox.d.ts +36 -0
  40. package/components/checkbox.mjs +38 -0
  41. package/components/combobox/comboboxChips.d.ts +21 -0
  42. package/components/combobox/comboboxListbox.d.ts +47 -0
  43. package/components/combobox.cjs +1 -0
  44. package/components/combobox.d.ts +121 -0
  45. package/components/combobox.mjs +563 -0
  46. package/components/dataGrid/components/dataGridAggregateValue.d.ts +10 -0
  47. package/components/dataGrid/components/dataGridBody.d.ts +7 -0
  48. package/components/dataGrid/components/dataGridBottomBar.d.ts +6 -0
  49. package/components/dataGrid/components/dataGridCell.d.ts +16 -0
  50. package/components/dataGrid/components/dataGridCellEditor.d.ts +15 -0
  51. package/components/dataGrid/components/dataGridCellPlaceholder.d.ts +14 -0
  52. package/components/dataGrid/components/dataGridCellRowDetail.d.ts +6 -0
  53. package/components/dataGrid/components/dataGridCellRowSelection.d.ts +6 -0
  54. package/components/dataGrid/components/dataGridCellText.d.ts +6 -0
  55. package/components/dataGrid/components/dataGridCellTree.d.ts +16 -0
  56. package/components/dataGrid/components/dataGridColumnFilter.d.ts +9 -0
  57. package/components/dataGrid/components/dataGridColumnGroups.d.ts +7 -0
  58. package/components/dataGrid/components/dataGridContent.d.ts +6 -0
  59. package/components/dataGrid/components/dataGridDataSourceError.d.ts +12 -0
  60. package/components/dataGrid/components/dataGridDetailRow.d.ts +8 -0
  61. package/components/dataGrid/components/dataGridEmptyColumns.d.ts +6 -0
  62. package/components/dataGrid/components/dataGridExport.d.ts +10 -0
  63. package/components/dataGrid/components/dataGridFilterCell.d.ts +11 -0
  64. package/components/dataGrid/components/dataGridFilterRow.d.ts +6 -0
  65. package/components/dataGrid/components/dataGridFooter.d.ts +11 -0
  66. package/components/dataGrid/components/dataGridGlobalFilter.d.ts +6 -0
  67. package/components/dataGrid/components/dataGridGroupRow.d.ts +8 -0
  68. package/components/dataGrid/components/dataGridHeader.d.ts +6 -0
  69. package/components/dataGrid/components/dataGridHeaderCell.d.ts +9 -0
  70. package/components/dataGrid/components/dataGridHeaderCellContextMenu.d.ts +11 -0
  71. package/components/dataGrid/components/dataGridHeaderCellResizer.d.ts +14 -0
  72. package/components/dataGrid/components/dataGridLoader.d.ts +11 -0
  73. package/components/dataGrid/components/dataGridPagination.d.ts +6 -0
  74. package/components/dataGrid/components/dataGridRow.d.ts +8 -0
  75. package/components/dataGrid/components/dataGridTopBar.d.ts +6 -0
  76. package/components/dataGrid/components/dataGridTopBarContextMenu.d.ts +6 -0
  77. package/components/dataGrid/contracts/dataGridContract.d.ts +671 -0
  78. package/components/dataGrid/export/gridExport.d.ts +10 -0
  79. package/components/dataGrid/gridNavigationContext.d.ts +32 -0
  80. package/components/dataGrid/models/aggregateCellModel.d.ts +34 -0
  81. package/components/dataGrid/models/aggregationModel.d.ts +32 -0
  82. package/components/dataGrid/models/cellKeys.d.ts +11 -0
  83. package/components/dataGrid/models/cellModel.d.ts +44 -0
  84. package/components/dataGrid/models/columnModel.d.ts +160 -0
  85. package/components/dataGrid/models/columnVisibilityModel.d.ts +24 -0
  86. package/components/dataGrid/models/dataSourceModel.d.ts +177 -0
  87. package/components/dataGrid/models/detailRowModel.d.ts +22 -0
  88. package/components/dataGrid/models/editModel.d.ts +77 -0
  89. package/components/dataGrid/models/exportModel.d.ts +51 -0
  90. package/components/dataGrid/models/filterModel.d.ts +22 -0
  91. package/components/dataGrid/models/gridModel.d.ts +293 -0
  92. package/components/dataGrid/models/groupRowCellModel.d.ts +34 -0
  93. package/components/dataGrid/models/groupRowModel.d.ts +68 -0
  94. package/components/dataGrid/models/headerCellModel.d.ts +83 -0
  95. package/components/dataGrid/models/paginationModel.d.ts +32 -0
  96. package/components/dataGrid/models/rowModel.d.ts +46 -0
  97. package/components/dataGrid/models/sourceGroupRowModel.d.ts +45 -0
  98. package/components/dataGrid/models/sourceLevel.d.ts +71 -0
  99. package/components/dataGrid/models/sourceRowModel.d.ts +29 -0
  100. package/components/dataGrid/models/sourceTreeRowModel.d.ts +23 -0
  101. package/components/dataGrid/models/treeModel.d.ts +94 -0
  102. package/components/dataGrid/models/treeRow.d.ts +22 -0
  103. package/components/dataGrid/models/treeRowModel.d.ts +50 -0
  104. package/components/dataGrid/models/viewportModel.d.ts +44 -0
  105. package/components/dataGrid/useGrid.d.ts +8 -0
  106. package/components/dataGrid/useGridNavigation.d.ts +21 -0
  107. package/components/dataGrid.cjs +1 -0
  108. package/components/dataGrid.d.ts +4 -0
  109. package/components/dataGrid.mjs +4907 -0
  110. package/components/dialog.cjs +1 -0
  111. package/components/dialog.d.ts +151 -0
  112. package/components/dialog.mjs +223 -0
  113. package/components/dropdown/dropdownContext.d.ts +44 -0
  114. package/components/dropdown/dropdownItems.d.ts +22 -0
  115. package/components/dropdown/dropdownRowRenderer.d.ts +8 -0
  116. package/components/dropdown/dropdownSearch.d.ts +21 -0
  117. package/components/dropdown/utils.d.ts +1 -0
  118. package/components/dropdown.cjs +1 -0
  119. package/components/dropdown.d.ts +77 -0
  120. package/components/dropdown.mjs +491 -0
  121. package/components/flex.cjs +1 -0
  122. package/components/flex.d.ts +6 -0
  123. package/components/flex.mjs +16 -0
  124. package/components/form.cjs +1 -0
  125. package/components/form.d.ts +10 -0
  126. package/components/form.mjs +58 -0
  127. package/components/grid.cjs +1 -0
  128. package/components/grid.d.ts +6 -0
  129. package/components/grid.mjs +16 -0
  130. package/components/icon.cjs +1 -0
  131. package/components/icon.d.ts +37 -0
  132. package/components/icon.mjs +41 -0
  133. package/components/menu.cjs +1 -0
  134. package/components/menu.d.ts +234 -0
  135. package/components/menu.mjs +422 -0
  136. package/components/overlay.cjs +1 -0
  137. package/components/overlay.d.ts +47 -0
  138. package/components/overlay.mjs +52 -0
  139. package/components/popover.cjs +1 -0
  140. package/components/popover.d.ts +100 -0
  141. package/components/popover.mjs +69 -0
  142. package/components/presence.cjs +1 -0
  143. package/components/presence.d.ts +36 -0
  144. package/components/presence.mjs +11 -0
  145. package/components/progress.cjs +1 -0
  146. package/components/progress.d.ts +47 -0
  147. package/components/progress.mjs +28 -0
  148. package/components/radioButton.cjs +1 -0
  149. package/components/radioButton.d.ts +32 -0
  150. package/components/radioButton.mjs +32 -0
  151. package/components/radioGroup.cjs +1 -0
  152. package/components/radioGroup.d.ts +51 -0
  153. package/components/radioGroup.mjs +83 -0
  154. package/components/select.cjs +1 -0
  155. package/components/select.d.ts +51 -0
  156. package/components/select.mjs +36 -0
  157. package/components/semantics.cjs +1 -0
  158. package/components/semantics.d.ts +39 -0
  159. package/components/semantics.mjs +17 -0
  160. package/components/slider.cjs +1 -0
  161. package/components/slider.d.ts +98 -0
  162. package/components/slider.mjs +250 -0
  163. package/components/svg.cjs +1 -0
  164. package/components/svg.d.ts +65 -0
  165. package/components/svg.mjs +141 -0
  166. package/components/switch.cjs +1 -0
  167. package/components/switch.d.ts +29 -0
  168. package/components/switch.mjs +25 -0
  169. package/components/tabs.cjs +1 -0
  170. package/components/tabs.d.ts +87 -0
  171. package/components/tabs.mjs +324 -0
  172. package/components/textarea.cjs +1 -0
  173. package/components/textarea.d.ts +41 -0
  174. package/components/textarea.mjs +33 -0
  175. package/components/textbox.cjs +1 -0
  176. package/components/textbox.d.ts +38 -0
  177. package/components/textbox.mjs +33 -0
  178. package/components/toaster.cjs +1 -0
  179. package/components/toaster.d.ts +106 -0
  180. package/components/toaster.mjs +295 -0
  181. package/components/tooltip.cjs +1 -0
  182. package/components/tooltip.d.ts +98 -0
  183. package/components/tooltip.mjs +110 -0
  184. package/components/visuallyHidden.cjs +1 -0
  185. package/components/visuallyHidden.d.ts +6 -0
  186. package/components/visuallyHidden.mjs +23 -0
  187. package/docs/a11y.md +199 -0
  188. package/docs/anchor.md +139 -0
  189. package/docs/components.md +48 -0
  190. package/docs/index.md +11 -0
  191. package/docs/props.md +1777 -0
  192. package/effects.cjs +1 -0
  193. package/effects.mjs +6 -0
  194. package/forms.cjs +1 -0
  195. package/forms.mjs +21 -0
  196. package/gridExport.cjs +2 -0
  197. package/gridExport.mjs +244 -0
  198. package/icons/dotsIcon.d.ts +2 -0
  199. package/icons/expandIcon.d.ts +2 -0
  200. package/icons/groupingIcon.d.ts +2 -0
  201. package/icons/pinIcon.d.ts +2 -0
  202. package/icons/searchIcon.d.ts +2 -0
  203. package/icons/sortIcon.d.ts +2 -0
  204. package/identity.cjs +1 -0
  205. package/identity.mjs +12 -0
  206. package/keyboard.cjs +1 -0
  207. package/keyboard.mjs +30 -0
  208. package/layer.cjs +1 -0
  209. package/layer.mjs +127 -0
  210. package/motion.cjs +1 -0
  211. package/motion.mjs +66 -0
  212. package/package.json +327 -7
  213. package/placement.cjs +1 -0
  214. package/placement.mjs +180 -0
  215. package/platform.cjs +1 -0
  216. package/platform.mjs +45 -0
  217. package/react/a11y/callbacks.d.ts +12 -0
  218. package/react/a11y/useControllableState.d.ts +25 -0
  219. package/react/a11y/useDismiss.d.ts +25 -0
  220. package/react/a11y/useFocusReturn.d.ts +18 -0
  221. package/react/a11y/useRovingFocus.d.ts +84 -0
  222. package/react/anchor/useAnchorPosition.d.ts +79 -0
  223. package/react/animation/usePresence.d.ts +32 -0
  224. package/react/boxClassNames.d.ts +25 -0
  225. package/react/boxProps.d.ts +49 -0
  226. package/react/boxTagProps.d.ts +7 -0
  227. package/react/clientRuntime.d.ts +11 -0
  228. package/react/dialog/useScrollLock.d.ts +12 -0
  229. package/react/effects.d.ts +10 -0
  230. package/react/forms/labelledControl.d.ts +17 -0
  231. package/react/forms/placeholderProp.d.ts +13 -0
  232. package/react/hooks/usePortalContainer.d.ts +6 -0
  233. package/react/hooks/useVirtualization.d.ts +32 -0
  234. package/react/hooks/useVisibility.d.ts +9 -0
  235. package/react/identity/useIdentifier.d.ts +17 -0
  236. package/react/popover/usePopoverLayer.d.ts +87 -0
  237. package/react/reactTypes.d.ts +10 -0
  238. package/react/resolveStyles.d.ts +18 -0
  239. package/react/styleElements.d.ts +3 -0
  240. package/react/svg/attributesInProps.d.ts +4 -0
  241. package/react/svg/svgNaming.d.ts +15 -0
  242. package/react/theme/theme.d.ts +26 -0
  243. package/react/theme/themeContext.d.ts +7 -0
  244. package/react/useStyles.d.ts +22 -0
  245. package/react-shared.cjs +1 -0
  246. package/react-shared.mjs +122 -0
  247. package/rolldown-runtime.cjs +1 -0
  248. package/rsc.cjs +1 -0
  249. package/rsc.d.ts +27 -0
  250. package/rsc.mjs +18 -0
  251. package/ssg.cjs +1 -0
  252. package/ssg.d.ts +24 -0
  253. package/ssg.mjs +25 -0
  254. package/utils/accordion/accordionUtils.d.ts +48 -0
  255. package/utils/anchor/anchorUtils.d.ts +79 -0
  256. package/utils/animation/animationUtils.d.ts +29 -0
  257. package/utils/array/arrayUtils.d.ts +15 -0
  258. package/utils/array/lazyList.d.ts +10 -0
  259. package/utils/box/boxUtils.d.ts +5 -0
  260. package/utils/chart/chartPalette.d.ts +15 -0
  261. package/utils/chart/chartUtils.d.ts +92 -0
  262. package/utils/combobox/comboboxUtils.d.ts +89 -0
  263. package/utils/dom/domUtils.d.ts +32 -0
  264. package/utils/environment/environmentUtils.d.ts +28 -0
  265. package/utils/export/csvUtils.d.ts +23 -0
  266. package/utils/export/downloadUtils.d.ts +12 -0
  267. package/utils/export/xlsxUtils.d.ts +56 -0
  268. package/utils/export/zipUtils.d.ts +26 -0
  269. package/utils/fn/fnUtils.d.ts +4 -0
  270. package/utils/form/formUtils.d.ts +8 -0
  271. package/utils/keyboard/typeaheadUtils.d.ts +31 -0
  272. package/utils/memo.d.ts +10 -0
  273. package/utils/menu/menuUtils.d.ts +25 -0
  274. package/utils/object/objectUtils.d.ts +13 -0
  275. package/utils/slider/sliderUtils.d.ts +67 -0
  276. package/utils/string/fuzzySearch.d.ts +27 -0
  277. package/utils/string/stringUtils.d.ts +4 -0
  278. package/utils/tabs/tabsUtils.d.ts +75 -0
  279. package/utils/toast/toastStore.d.ts +73 -0
  280. package/utils/virtual/virtualUtils.d.ts +47 -0
package/README.md CHANGED
@@ -1,13 +1,750 @@
1
- # @box-kite/react
1
+ # Box Kite
2
2
 
3
- This name is reserved for the React bindings of the **boxkite** project — the upcoming new
4
- identity of [`@cronocode/react-box`](https://www.npmjs.com/package/@cronocode/react-box).
3
+ [![npm](https://img.shields.io/npm/v/@box-kite/react)](https://www.npmjs.com/package/@box-kite/react)
4
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@box-kite/react)](https://bundlephobia.com/package/@box-kite/react)
5
+ [![Tests](https://github.com/box-kite/box-kite/actions/workflows/test.yml/badge.svg)](https://github.com/box-kite/box-kite/actions/workflows/test.yml)
6
+ [![license](https://img.shields.io/npm/l/@box-kite/react)](LICENSE)
5
7
 
6
- **This package is not usable yet.** Until the rename ships, install the library under its
7
- current name:
8
+ `Box` is a single React component with 221 typed CSS props. It generates the CSS for the values you
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.
12
+
13
+ [Docs and live demos](https://www.box-kite.dev) · [Contributing](CONTRIBUTING.md) ·
14
+ [Support](SUPPORT.md) · [Security](SECURITY.md) ·
15
+ [Releases](https://github.com/box-kite/box-kite/releases)
16
+
17
+ > **Renamed at 1.0.0.** This was `@cronocode/react-box`. The API is unchanged — the move is one
18
+ > find-and-replace, and `@cronocode/react-box@3.4.0` re-exports this package if you are not ready
19
+ > yet. [What to change](https://box-kite.dev/migrating/).
20
+
21
+ ## Getting Started
22
+
23
+ 1. Installation
8
24
 
9
25
  ```bash
10
- npm install @cronocode/react-box
26
+ npm install @box-kite/react
27
+ ```
28
+
29
+ React 16.14 or newer, including 18 and 19 (both are covered by CI). TypeScript is optional but the
30
+ props are typed for it.
31
+
32
+ Two packages ship, and a React project installs one of them:
33
+
34
+ | Package | What it is |
35
+ | ------------------------------------------------- | ----------------------------------------------------------------- |
36
+ | **`@box-kite/react`** | `Box`, every component, the hooks. Depends on the one below. |
37
+ | [`@box-kite/core`](#using-the-core-without-react) | The engine and the types, no framework. For an app with no React. |
38
+
39
+ `@box-kite/core/types` is where a project declares its own props and components — the augmentation
40
+ target lives with the prop registry it extends, not with a binding.
41
+
42
+ 2. Use it
43
+
44
+ Spacing counts in quarters of a rem: `p={3}` is `0.75rem`, `m={2}` is `0.5rem`. The example below is
45
+ a box with `margin: 0.5rem` and `padding: 1.75rem`.
46
+
47
+ ```JS
48
+ import Box from "@box-kite/react";
49
+
50
+ export default function Component(props: Props) {
51
+ return (
52
+ <Box className="custom-class" m={2} p={7}>
53
+ basic example
54
+ </Box>
55
+ );
56
+ }
57
+ ```
58
+
59
+ **NOTE**: the root `font-size` is `16px`, so a quarter of a rem is 4px. Not every prop divides by
60
+ four — `fontSize` divides by 16 (`fontSize={14}` is `0.875rem`), and border widths and `lineHeight`
61
+ are plain pixels (`b={1}` is 1px).
62
+
63
+ There is nothing to memorise, though: every prop's hover documentation carries a worked example of
64
+ the CSS it writes — `p={4} → padding: 1rem`, `fontSize={14} → font-size: 0.875rem` — and those lines
65
+ are generated by running the engine, so the editor cannot tell you a scale the library stopped using.
66
+
67
+ ## Colours
68
+
69
+ The palette is Tailwind's, in OKLCH: twenty-six families of eleven steps — `slate-50` through
70
+ `rose-950`, the four Tailwind 4.3 added (`mauve`, `mist`, `olive`, `taupe`) included — declared as CSS
71
+ variables the first time a page asks for one. Any colour value takes an opacity modifier:
72
+
73
+ ```JS
74
+ <Box bgColor="blue-500/40" borderColor="black/10" hover={{ bgColor: 'blue-500/60' }} />
75
+ ```
76
+
77
+ That compiles to `color-mix(in oklab, var(--blue-500) 40%, transparent)`: the mix wraps the
78
+ **variable**, so a themed token is still themed and everything asking for the same value still shares
79
+ one class. It is not `opacity`, which fades the element, its text and its children — this fades one
80
+ declaration. Every colour prop takes it (`color`, `bgColor`, `borderColor`, `outlineColor`, `fill`,
81
+ `stroke`), as does a `vars` entry and a variable of your own from `Box.extend()`.
82
+
83
+ ## Gradients, shadows and filters
84
+
85
+ A gradient is a value, not a string you assemble: the key names the kind and carries its geometry, and
86
+ the stops are palette colours, so the whole thing is themed, takes the opacity modifier and shares one
87
+ class with every other element asking for it.
88
+
89
+ ```JS
90
+ <Box bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }} />
91
+ <Box bgGradient={{ radial: 'circle', at: 'top left', colors: [['sky-500', '20%'], 'indigo-900'] }} />
92
+ <Box bgGradient={{ conic: 45, colors: ['red-500', 'yellow-500', 'red-500'], interpolate: 'oklch' }} />
93
+ ```
94
+
95
+ `interpolate` is worth knowing: sRGB drags a two-stop gradient through a grey middle, and `oklch` does
96
+ not. `oklch-longer` takes the long way round the hue circle, which is what turns two stops into a
97
+ spectrum. A gradient is judged whole — one unknown stop or one misspelt key and it emits no rule at
98
+ all, rather than quietly painting something else.
99
+
100
+ `box-shadow` is one CSS property, which normally means one shadow wins. Here it is **four layers** that
101
+ compose, so an elevation, a ring and an inner hairline coexist:
102
+
103
+ ```JS
104
+ <Box shadow="md" ring={2} ringColor="indigo-500" borderRadius={2} />
105
+ <Box insetShadow="sm" insetRing={1} shadow="lg" shadowColor="blue-500/40" />
106
+ ```
107
+
108
+ `shadow` is Tailwind's elevation scale (`xxs` through `xxl`, and the older `small`/`medium`/`large`
109
+ presets still work), `insetShadow` draws inside the border box, and `ring`/`insetRing` are a width in
110
+ px. A ring follows `borderRadius` and costs no layout, which is what makes it different from
111
+ `outline`. Each layer has its own colour prop — `shadowColor`, `ringColor`, and so on — which shows
112
+ nothing until the layer it belongs to is painted, exactly as `borderColor` needs a border width.
113
+ `textShadow` and `textShadowColor` are the text-side pair.
114
+
115
+ Filters stack the same way, and for the same reason: `filter` is one property whose value is a list, so
116
+ each of the nine functions sets a layer of its own and all nine write one composed declaration.
117
+
118
+ ```JS
119
+ <Box blur="sm" grayscale={100} />
120
+ <Box brightness={110} saturate={150} hueRotate={90} />
121
+ <Box dropShadow="lg" dropShadowColor="indigo-500/40" />
122
+ <Box backdropBlur="sm" backdropSaturate={180} bgColor="white/20" />
123
+ ```
124
+
125
+ A number is the function's own unit — a percentage for `brightness`, `contrast`, `grayscale`,
126
+ `invert`, `saturate` and `sepia`, degrees for `hueRotate`, pixels for `blur`, which takes Tailwind's
127
+ `xs`–`xxxl` scale too. `dropShadow` is cast by the _shape_ rather than the box, which is what a
128
+ transparent PNG or an SVG path wants. The nine `backdrop*` props are the same functions applied to
129
+ what is behind the element — the glassmorphism half.
130
+
131
+ `maskImage` takes the gradient record over again and reads its alpha channel, so a fade to
132
+ `transparent` is an edge fade; and `bgClip="text"` with `color="transparent"` is how a gradient
133
+ becomes lettering.
134
+
135
+ ```JS
136
+ <Box maskImage={{ linear: 'b', colors: ['black', ['black', '55%'], 'transparent'] }} />
137
+ <H1 color="transparent" bgClip="text" bgGradient={{ linear: 'r', colors: ['violet-500', 'cyan-400'] }}>
138
+ Painted by the background
139
+ </H1>
140
+ ```
141
+
142
+ ## Components
143
+
144
+ Every component here is a `Box` underneath — the same style props, the same theming, the same
145
+ `component`/`variant` defaults — with the right HTML tag, the ARIA its pattern owes and, where the
146
+ pattern has one, its keyboard. Each is a separate entry point, so a `Textbox` does not pull the
147
+ `DataGrid` into your bundle. Module paths below are relative to the package name, i.e.
148
+ `@box-kite/react/components/flex`.
149
+
150
+ ```JS
151
+ import Box from "@box-kite/react";
152
+ ```
153
+
154
+ ### Layout and content
155
+
156
+ | Component | Module | What it is |
157
+ | ---------------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------- |
158
+ | `Flex` | `components/flex` | `Box` with `display: flex` |
159
+ | `Grid` | `components/grid` | `Box` with `display: grid` |
160
+ | `H1`–`H6`, `P`, `Span`, `Link`, `Img`, `Nav`, `Header`, `Main`, `Section`, … | `components/semantics` | one `Box` per semantic tag — 25 of them |
161
+ | `Svg`, `Path`, `Circle`, `Rect`, `SvgText`, … | `components/svg` | one component per SVG element — 20 of them, so a drawing never writes `tag` |
162
+ | `Icon` | `components/icon` | Box props on an icon somebody else drew — lucide, Tabler, react-icons, a raw `<svg>` |
163
+ | `Sparkline`, `ProgressRing`, `Gauge`, `MiniDonut` | `components/chart` | chart micro-primitives over those elements — a chart that takes Box props and no chart library |
164
+ | `ChartContainer` | `components/chart` | the theming bridge: the variables a Recharts (or any) chart reads, declared in both themes |
165
+ | `BaseSvg` | `components/baseSvg` | deprecated: `Svg` with the 24×24 icon preset |
166
+ | `VisuallyHidden` | `components/visuallyHidden` | text for a screen reader only — clipped away rather than hidden, so it stays in the a11y tree |
167
+
168
+ ### Form controls
169
+
170
+ | Component | Module | What it is |
171
+ | ------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
172
+ | `Button` | `components/button` | `<button>`, with `type` and `onClick` lifted out of `props` |
173
+ | `Textbox` | `components/textbox` | `<input>` |
174
+ | `Textarea` | `components/textarea` | `<textarea>` |
175
+ | `Checkbox` | `components/checkbox` | a real `<input type="checkbox">`; `label` renders the wrapping `<label>` for you, and `indeterminate` reports `aria-checked="mixed"` |
176
+ | `Switch` | `components/switch` | the same input as `role="switch"` — Enter toggles it as well as Space |
177
+ | `RadioGroup`, `RadioGroup.Item` | `components/radioGroup` | the APG radio group: one shared `name`, one tab stop, arrow keys between the options |
178
+ | `RadioButton` | `components/radioButton` | a single labelled `<input type="radio">`, for a group you assemble yourself |
179
+ | `Select` | `components/select` | a `Dropdown` over a list of rows: `data={rows} def={{ valueKey, displayKey }}` |
180
+ | `Form` | `components/form` | a `<form>` that reads its own fields on submit and hands them to you as one object |
181
+
182
+ ### Overlays and data
183
+
184
+ | Component | Module | What it is |
185
+ | ----------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
186
+ | `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 |
187
+ | `Tooltip` | `components/tooltip` | the APG tooltip on top of `Overlay` — hover _and_ focus, `aria-describedby`, Escape, hoverable (WCAG 1.4.13) |
188
+ | `Dropdown`, `Dropdown.Item`, `Dropdown.Display` | `components/dropdown` | the APG combobox: select-only, or the editable one with `isSearchable` |
189
+ | `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 |
190
+
191
+ Two whose shape is not obvious from a table:
192
+
193
+ ```tsx
194
+ // The tooltip's trigger is a render prop, so `aria-describedby` lands on the control itself
195
+ // rather than on a wrapper around it.
196
+ import Tooltip from '@box-kite/react/components/tooltip';
197
+
198
+ <Tooltip content="Deletes the row for good">{(trigger) => <Button {...trigger}>Delete</Button>}</Tooltip>;
199
+ ```
200
+
201
+ ```tsx
202
+ // `Form` reads the named fields under it when it submits, and hands them over as one object.
203
+ import Button from '@box-kite/react/components/button';
204
+ import Form from '@box-kite/react/components/form';
205
+ import Textbox from '@box-kite/react/components/textbox';
206
+
207
+ interface Credentials {
208
+ email: string;
209
+ password: string;
210
+ }
211
+
212
+ <Form<Credentials> p={4} onSubmit={(values) => signIn(values)}>
213
+ <Textbox name="email" type="email" />
214
+ <Textbox name="password" type="password" />
215
+ <Button type="submit">Sign in</Button>
216
+ </Form>;
217
+ ```
218
+
219
+ `onSubmit(values, event)` runs after the event's `preventDefault()`. A field is in `values` if it
220
+ has a `name`: a lone checkbox or radio contributes its `checked` boolean, a name used more than
221
+ once collects the checked or entered values into an array, and a dotted name (`address.city`,
222
+ `lines[0].qty`) nests. Everything else arrives as the string the DOM holds — the type argument
223
+ describes the object you expect, it is not checked against the fields.
224
+
225
+ ## SVG
226
+
227
+ Twenty-three SVG properties are Box props, so a shape is themed, hovered and made responsive the
228
+ same way a `<div>` is — and there is a component for every SVG element, so a drawing never writes
229
+ `tag` either.
230
+
231
+ ```JSX
232
+ import { Path, Svg } from '@box-kite/react/components/svg';
233
+
234
+ <Svg
235
+ viewBox="0 0 200 48"
236
+ width="200px"
237
+ fill="none"
238
+ strokeWidth={3}
239
+ strokeLinecap="round"
240
+ strokeDasharray={320}
241
+ strokeDashoffset={320}
242
+ hover={{ strokeDashoffset: 0 }}
243
+ theme={{ dark: { stroke: 'violet-400' }, light: { stroke: 'violet-600' } }}
244
+ >
245
+ <Path d="M8 40 L56 12 L104 34 L152 8 L192 24" />
246
+ </Svg>;
247
+ ```
248
+
249
+ That draws the line on hover, and nothing in it declares a transition: every `<svg>` and the shapes
250
+ inside it already transition on `--svgTransitionTime`, which `prefers-reduced-motion: reduce` sets
251
+ to `0s`.
252
+
253
+ | Prop | Notes |
254
+ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
255
+ | `fill`, `stroke` | any colour variable, or `'none'` |
256
+ | `fillOpacity`, `strokeOpacity` | `0`–`1` in tenths, the same scale as `opacity` |
257
+ | `fillRule` | `'nonzero'`, `'evenodd'` |
258
+ | `strokeWidth`, `strokeDasharray`, `strokeDashoffset`, `strokeMiterlimit` | numbers in **SVG user units — no divider**. A dash pattern with its own gap is a string, `'12 4'` |
259
+ | `strokeLinecap`, `strokeLinejoin` | `'butt'`/`'round'`/`'square'`, `'miter'`/`'round'`/`'bevel'` |
260
+ | `paintOrder` | `'normal'`, `'fill'`, `'stroke'`, `'markers'` — `'stroke'` is how outlined text stays legible |
261
+ | `vectorEffect` | `'none'`, `'non-scaling-stroke'` |
262
+ | `shapeRendering` | `'auto'`, `'optimizeSpeed'`, `'crispEdges'`, `'geometricPrecision'` |
263
+ | `textAnchor` | `'start'`, `'middle'`, `'end'` — which part of a label sits on its `x` |
264
+ | `dominantBaseline` | `'alphabetic'`, `'central'`, `'hanging'`, … — which part sits on its `y` |
265
+ | `cx`, `cy`, `r`, `rx`, `ry`, `x`, `y` | the SVG 2 geometry, in **user units**, or a percentage. `rx`/`ry` also take `'auto'` |
266
+
267
+ Every paint and stroke property here is an inherited one, so setting it on the `<svg>` reaches the
268
+ shapes inside — with two exceptions. `vector-effect` and `dominant-baseline` are not inherited, so
269
+ those two props write a rule that names the element _and_ its descendants: `vectorEffect="non-scaling-stroke"`
270
+ on the `<svg>` keeps a hairline one pixel wide however far the `viewBox` is scaled, and
271
+ `dominantBaseline="central"` on the `<svg>` reaches every label in it.
272
+
273
+ The geometry props are different again. They belong to one shape, so they go on the shape — but they
274
+ are real CSS, which means they transition, and an animated gauge or a growing bar needs no JavaScript:
275
+
276
+ ```JSX
277
+ <Circle cx={48} cy={48} r={38} strokeDasharray={239} strokeDashoffset={239} hover={{ strokeDashoffset: 60, r: 40 }} />;
278
+ ```
279
+
280
+ Where a prop name and an SVG attribute collide, the element decides which one wins. On `Rect`,
281
+ `width={40}` is forty user units rather than the layout scale's `10rem`; on `Path`, `d` is path data
282
+ rather than `flex-direction`; on `SvgText`, `x` and `y` are the attributes, because the CSS geometry
283
+ properties do not apply to `<text>`. On `Circle` those same names stay the CSS props that transition.
284
+
285
+ ### Icons
286
+
287
+ An icon set is somebody else's component, so there is no `tag` that renders one and Box cannot wrap
288
+ it. `Icon` styles it through the one channel it does offer — the `className` it spreads onto its own
289
+ `<svg>`:
290
+
291
+ ```JSX
292
+ import Icon from '@box-kite/react/components/icon';
293
+ import { Sun } from 'lucide-react';
294
+
295
+ <Icon size={5} color="amber-500" hover={{ color: 'amber-300' }} label="Sunny">
296
+ <Sun />
297
+ </Icon>;
298
+ ```
299
+
300
+ It works with lucide, Tabler, react-icons or an `<svg>` you paste in, and it knows none of their
301
+ APIs: `size` is a number on the ÷4 scale (`size={6}` is 24px, the default) that lands in the class,
302
+ where a CSS declaration outranks the `width`/`height` attributes the icon writes for itself. Without
303
+ a `label` an icon is `aria-hidden`; with one it is `role="img"` and that name. For SVG you draw
304
+ yourself, reach for `Svg` instead — it takes these props directly.
305
+
306
+ Behind it is `useClassNames`, which is public for the same reason: a `motion.div`, a router's
307
+ `NavLink` or a third-party chart takes a `className` and nothing else.
308
+
309
+ ```JSX
310
+ import { useClassNames } from '@box-kite/react';
311
+
312
+ const { className, styles } = useClassNames({ color: 'sky-500', hover: { color: 'sky-300' } });
313
+
314
+ <>
315
+ {styles}
316
+ <NavLink to="/" className={className} />
317
+ </>;
318
+ ```
319
+
320
+ `styles` is defined in element mode only, where the CSS travels as hoistable `<style>` elements
321
+ rather than going to a stylesheet; elsewhere it is undefined and rendering it costs nothing.
322
+
323
+ ### Charts
324
+
325
+ Four micro-primitives, built from the SVG components — the small drawings a dashboard needs rather
326
+ than a chart library:
327
+
328
+ ```JSX
329
+ import { Gauge, MiniDonut, ProgressRing, Sparkline } from '@box-kite/react/components/chart';
330
+
331
+ <Sparkline data={[4, 9, 6, 12, 10, 15]} variant="area" color="sky-500" width="7rem" />;
332
+ <ProgressRing value={0.62} color="emerald-500" label="62% complete" />;
333
+ <Gauge value={0.4} sweep={180} color="rose-500" />;
334
+ <MiniDonut data={[5, 3, 2]} />;
335
+ ```
336
+
337
+ There are no axes, no legends and no data transformations: what they give you instead is that a
338
+ chart is a Box, so its colour, its size, its dark mode and its hover state are the props you already
339
+ know. A sparkline stretches to whatever box it is in (and keeps its line one width thick, through
340
+ `vectorEffect="non-scaling-stroke"`), a ring's arc eases between values with no animation code, and
341
+ naming follows `Svg`: no `label` means `aria-hidden`, a `label` means `role="img"`.
342
+
343
+ They are cheap enough for one in every row of a virtualized grid, because the part that differs per
344
+ row — the shape — is the `d` attribute, which the styling engine never sees. What _is_ a style prop
345
+ is the paint, which the rows share. `fill` and `stroke` also take `url(#gradient)` and
346
+ `var(--chart-1)` now, so a gradient fill is a value with a theme and a `hover`, not an attribute.
347
+
348
+ ### Theming a chart library
349
+
350
+ The other half of charts is the chart library you already use. A `<ChartContainer>` declares the
351
+ variables a chart reads, so the chart itself names no colour at all:
352
+
353
+ ```JSX
354
+ import { ChartContainer } from '@box-kite/react/components/chart';
355
+ import { Area, AreaChart, ResponsiveContainer, XAxis } from 'recharts';
356
+
357
+ <ChartContainer series={['revenue', 'cost']} height={60}>
358
+ <ResponsiveContainer width="100%" height="100%">
359
+ <AreaChart data={months}>
360
+ <XAxis dataKey="month" />
361
+ <Area dataKey="revenue" stroke="var(--color-revenue)" fill="var(--color-revenue)" fillOpacity={0.15} />
362
+ <Area dataKey="cost" stroke="var(--color-cost)" fill="var(--color-cost)" fillOpacity={0.15} />
363
+ </AreaChart>
364
+ </ResponsiveContainer>
365
+ </ChartContainer>;
366
+ ```
367
+
368
+ It declares `--chart-1` … `--chart-6` in both themes and one `--color-<series>` per series you name,
369
+ so the chart flips light to dark with nothing in its own code changing — and the names are the ones
370
+ the ecosystem already uses, so a chart lifted out of shadcn's charts works unchanged.
371
+
372
+ What makes that cheap is the prop behind it. `vars` is the one prop whose declaration _names_ come
373
+ from its value:
374
+
375
+ ```JSX
376
+ <Flex vars={{ 'color-revenue': 'sky-500', 'chart-gap': '4px' }} theme={{ dark: { vars: { 'color-revenue': 'sky-400' } } }}>
377
+ ```
378
+
379
+ declares `--color-revenue: var(--sky-500)` and `--chart-gap: 4px` on the element, inherited by
380
+ everything inside it — including markup this library never rendered. A colour token resolves to the
381
+ variable behind it, anything else is written out as it stands. Being an ordinary prop, it nests in a
382
+ theme, a breakpoint and a pseudo-class, and it lands in a **class**: two subtrees declaring the same
383
+ palette share one rule, and nothing needs a `<style>` tag or an `id` to scope it to one chart.
384
+
385
+ ## Extend props
386
+
387
+ It is possible to add your own props to define your custom styles.
388
+ You need to do a few steps.
389
+
390
+ 1. In a project root file you need to define your extends
391
+
392
+ ```JS
393
+ import Box from "@box-kite/react";
394
+
395
+ export const { extendedProps, extendedPropTypes } = Box.extend(
396
+ // css variables
397
+ {
398
+ dark: '#123123',
399
+ light: '#ededed',
400
+ cssVarName: 'cssVarValue',
401
+ myShadow: '10px 5px 5px red',
402
+ myGradient1: 'linear-gradient(#e66465, #9198e5)',
403
+ myGradient2: 'linear-gradient(black, white)',
404
+ },
405
+ // new custom props
406
+ {
407
+ background: [
408
+ {
409
+ values: ['myGradient1', 'myGradient2'] as const,
410
+ valueFormat: (value: string) => `var(--background${value})`,
411
+ },
412
+ ],
413
+ },
414
+ // extend values for existing props
415
+ {
416
+ color: [
417
+ {
418
+ values: ['dark', 'light'],
419
+ valueFormat: (value, getVariableValue) => getVariableValue(value),
420
+ },
421
+ ],
422
+ }
423
+ );
424
+ ```
425
+
426
+ 2. Now you have to add typings to the Box in order to have intellisense for you new props and value.
427
+ You need to create a `box.d.ts`
428
+
429
+ ```JS
430
+ import '@box-kite/react';
431
+ import { ExtractBoxStyles, ExtractComponentsAndVariants } from '@box-kite/core/types';
432
+ import { extendedProps, extendedPropTypes, components } from './path-to-your-b0x-extends-declaration';
433
+
434
+ declare module '@box-kite/core/types' {
435
+ namespace Augmented {
436
+ interface BoxProps extends ExtractBoxStyles<typeof extendedProps> {}
437
+ interface BoxPropTypes extends ExtractBoxStyles<typeof extendedPropTypes> {}
438
+ interface ComponentsTypes extends ExtractComponentsAndVariants<typeof components> {}
439
+ }
440
+ }
441
+ ```
442
+
443
+ ### The escape hatch
444
+
445
+ For a one-off property nobody will type twice, `css` takes a style object and compiles it the way
446
+ every other prop is compiled — into a shared class, never a `style` attribute — so it nests wherever
447
+ a prop does and renders on a server:
448
+
449
+ ```JSX
450
+ <Box css={{ mixBlendMode: 'multiply', WebkitLineClamp: 2 }} hover={{ css: { isolation: 'isolate' } }} />
451
+ ```
452
+
453
+ Names are camelCase and typed, values are CSS written as they stand (a colour token resolves to its
454
+ variable, a length wants its unit), and it sorts last so it wins over a typed prop naming the same
455
+ property. Prefer a prop, then `Box.extend()` for anything used twice.
456
+
457
+ ## Theme for components
458
+
459
+ In the project root file (main.tsx) use `Theme.setup`
460
+
461
+ ```JS
462
+ import Box from '@box-kite/react';
463
+
464
+ Box.components({
465
+ button: {
466
+ styles: {
467
+ px: 4
468
+ },
469
+ variants: {
470
+ mytheme: {
471
+ px: 8
472
+ }
473
+ }
474
+ },
475
+ ...
476
+ })
477
+ ```
478
+
479
+ All styles will be applied to Button component
480
+
481
+ ```JS
482
+ import Button from '@box-kite/react/components/button';
483
+
484
+ function MyComponent() {
485
+ return <Button>Click me</Button>
486
+ }
487
+ ```
488
+
489
+ or is possible to use Button with specific variant
490
+
491
+ ```JS
492
+ import Button from '@box-kite/react/components/button';
493
+
494
+ function MyComponent() {
495
+ return <Button variant="mytheme">Click me</Button>
496
+ }
497
+ ```
498
+
499
+ ## Theme variables
500
+
501
+ In CSS file is possible to override default values for:
502
+
503
+ ```CSS
504
+ --borderColor: black;
505
+ --outlineColor: black;
506
+ --lineHeight: 1.2;
507
+ --fontSize: 14px;
508
+ --transitionTime: 0.25s;
509
+ ```
510
+
511
+ ## React Server Components (React 19)
512
+
513
+ Box renders inside a Server Component with no `'use client'` — the styles travel with the markup
514
+ instead of being injected by an effect. A server component just imports the package as usual:
515
+
516
+ ```tsx
517
+ // app/page.tsx — a Server Component. No 'use client', no provider, no CSS import.
518
+ import Flex from '@box-kite/react/components/flex';
519
+ import { H1, P } from '@box-kite/react/components/semantics';
520
+
521
+ export default function Page() {
522
+ return (
523
+ <Flex d="column" gap={2} p={6} bgColor="slate-50" borderRadius={2}>
524
+ <H1 fontSize={24}>Rendered on the server</H1>
525
+ <P color="slate-600" sm={{ fontSize: 16 }}>
526
+ Its CSS was hoisted into &lt;head&gt; by React, not inserted by a client runtime.
527
+ </P>
528
+ </Flex>
529
+ );
530
+ }
531
+ ```
532
+
533
+ The `react-server` export condition resolves to a build of Box that calls no hook, schedules no
534
+ effect and never touches the DOM: each generated rule comes back as a `<style href precedence>`
535
+ element rendered next to the markup, which React 19 hoists into `<head>` and dedupes by `href`.
536
+ That is also what makes it stream-safe — nothing has to wait for a commit that Suspense may split.
537
+
538
+ **Client components** (anything with `'use client'`) can use the same emission path, and should in an
539
+ RSC app, so their CSS is in the HTML too. One line, in a module the root layout imports:
540
+
541
+ ```tsx
542
+ 'use client';
543
+ import Box from '@box-kite/react';
544
+
545
+ Box.configure({ sink: 'element' }); // React 19 only — see below
546
+ ```
547
+
548
+ Notes and limits:
549
+
550
+ - **The cascade comes from `@layer`, not from element order.** React hoists elements in render
551
+ order, so a responsive rule could otherwise land ahead of the base rule it has to override. In
552
+ this mode every rule is wrapped in a cascade layer (one per breakpoint × prop) and the base
553
+ element declares the layer order once, up front — the order the elements end up in cannot change
554
+ the result. The consequence worth knowing: **unlayered CSS of your own always wins** over Box
555
+ props here, which matches where the library puts its `<style>` element in the other modes.
556
+ - **Declare `Box.extend()` props before the first render.** CSS appends a layer it meets for the
557
+ first time after every layer already named, so a prop registered mid-render sorts after the
558
+ built-ins.
559
+ - **Class names are content-hashed** in this mode instead of counted, so a class resolved on the
560
+ server matches the one the client bundle resolves for the same props.
561
+ - **Still client-only:** hover-callback children (`{({ isHover }) => …}`) and `Box.Theme`, which
562
+ needs state, storage and a media-query listener. Theme _styles_ are unaffected: a `theme` prop
563
+ generates ancestor-scoped rules, so setting the theme class on `<html>` in a server component is
564
+ enough.
565
+ - **Most pre-built components render on the server too.** `Flex`, `Grid`, `Button`, `Textbox`,
566
+ `Textarea`, `RadioButton`, `Icon`, the SVG elements, `VisuallyHidden` and the semantic tags (`H1`, `P`, `Link`, `Nav` …) are
567
+ hook-free wrappers around Box, and their published chunks import the package by name — so the
568
+ `react-server` condition reaches them and they resolve the same hook-free Box. Import them
569
+ straight into a Server Component.
570
+ - **The stateful ones become client boundaries.** `Dropdown`, `Tooltip`, `Overlay`, `DataGrid`,
571
+ `Checkbox`, `Switch`, `RadioGroup`, `Select` and `Form` hold state or measure the DOM, so their chunks ship a
572
+ `'use client'` banner.
573
+ A Server Component may still import one — the bundler opens the boundary for you — but it is
574
+ hydrated like any client component, and its CSS is in the server-rendered HTML only if a client
575
+ module of yours has already called `Box.configure({ sink: 'element' })`.
576
+ - **React 19 only.** On React 18 the elements cannot be hoisted and render inline; keep the default
577
+ sink there (a documented App Router fallback for React 18 is still to come).
578
+ - **Cost:** one base element per page (the reset, `:root`, and the layer order — ~1.8 KB gzipped)
579
+ plus one `<style>` element per distinct rule, deduped across every Box that uses it.
580
+
581
+ A whole Next.js App Router app built this way — streamed Suspense boundary, client island, theme
582
+ toggle, and a page of pre-built components rendered on the server — is in
583
+ [`examples/next-app`](examples/next-app): `npm run build:next-app` and `npm run smoke:next-app`,
584
+ which starts the production server and asserts on the HTML it serves.
585
+
586
+ ## Behaviour primitives for your own components
587
+
588
+ Every accessible widget needs the same mechanics, and none of them are visible in the markup:
589
+ return focus to whatever opened a layer, move through a list with the arrow keys, close on Escape
590
+ or a press outside, hold a value the consumer may or may not own, generate ids that wire one
591
+ element to another. They are what this library's components are built from, and they ship on their
592
+ own for the patterns it does not cover:
593
+
594
+ ```tsx
595
+ import { useControllableState, useDismiss, useFocusReturn, useIdentifier, useRovingFocus } from '@box-kite/react/a11y';
596
+ import VisuallyHidden from '@box-kite/react/components/visuallyHidden';
597
+
598
+ function Menu({ open, onOpenChange, items }) {
599
+ const trigger = useRef(null);
600
+ const popup = useRef(null);
601
+
602
+ const roving = useRovingFocus({ count: items.length, textOf: (index) => items[index].label });
603
+
604
+ useDismiss({ enabled: open, inside: [trigger, popup], onDismiss: (reason, event) => onOpenChange(false, { reason, event }) });
605
+ useFocusReturn({ enabled: open, returnTo: trigger });
606
+ }
607
+ ```
608
+
609
+ 2.2 KB gzipped for all five hooks, and they pull in no styling engine — you can use them next to
610
+ any CSS you like. Changes are **event-sourced**: `onChange(value, { reason, event })` tells you
611
+ _why_ something closed (`'escape'`, `'outside-pointer'`, a reason of your own), which is what makes
612
+ a controlled component controllable in practice. Roles and ARIA are deliberately left to you: a
613
+ listbox and a menu navigate identically and are named completely differently.
614
+
615
+ Full reference: [docs/a11y-primitives.md](docs/a11y-primitives.md).
616
+
617
+ ## Using the core without React
618
+
619
+ The engine that turns props into CSS has no framework in it, and it is published on its own:
620
+
621
+ ```js
622
+ import { createStyleEngine } from '@box-kite/core';
623
+
624
+ const engine = createStyleEngine();
625
+
626
+ document.querySelector('#card').className = engine.classNames({
627
+ p: 4,
628
+ bgColor: 'blue-500',
629
+ borderRadius: 2,
630
+ hover: { bgColor: 'blue-600' },
631
+ md: { p: 8 },
632
+ });
633
+ ```
634
+
635
+ That is the whole runtime. The props are the ones `<Box>` takes — pseudo-classes, breakpoints,
636
+ themes, `component`/`variant` — and the CSS behind them is written to a `<style>` element the
637
+ engine creates, on its own microtask. Nothing has to be flushed, mounted or provided.
638
+
639
+ ```js
640
+ const engine = createStyleEngine();
641
+
642
+ // Custom variables and props, the vanilla form of Box.extend()
643
+ engine.extend(
644
+ { brand: '#6d28d9' },
645
+ {},
646
+ { bgColor: [{ styleName: 'background-color', values: ['brand'], valueFormat: (v, get) => get(v) }] },
647
+ );
648
+
649
+ // Component defaults with variants, the vanilla form of Box.components()
650
+ engine.components({ panel: { styles: { p: 6, borderRadius: 3 }, variants: { accent: { b: 2, borderColor: 'brand' } } } });
651
+
652
+ engine.classNames({ component: 'panel', variant: 'accent' });
653
+ engine.flushSync(); // only when computed styles are read in the same tick
654
+ engine.getStyles(); // the stylesheet as text, for static output
655
+ ```
656
+
657
+ Theming is the same state machine `<Box.Theme>` runs, as a plain object:
658
+
659
+ ```js
660
+ import { createThemeController } from '@box-kite/core';
661
+
662
+ // Reads prefers-color-scheme, restores a stored choice, writes the theme onto <html>, and follows
663
+ // the system preference until something overrides it. Theme rules are ancestor-scoped (`.dark .p-4`),
664
+ // so that one class name restyles everything inside.
665
+ const theme = createThemeController({ storageKey: 'theme' });
666
+
667
+ theme.subscribe((name) => console.log('theme is now', name));
668
+ theme.set('dark'); // theme.set(null) hands control back to the system
669
+ ```
670
+
671
+ Notes:
672
+
673
+ - **The engine is explicit here.** The React entries share one lazily-created default instance so
674
+ `Box` and `Box.extend()` agree with no configuration; a vanilla app has no such ambient thing to
675
+ agree with, and separate instances are what keeps a widget independent of its host page.
676
+ - **No React reaches this entry**, in the sources or in the bundle — both are checked in CI. It is
677
+ 14 KB gzipped, all engine.
678
+ - `sink: 'string'` collects CSS in memory with no DOM (`getStyles()` reads it back), which is how
679
+ server rendering works; `sink: 'element'` is the React 19 mode above and has no meaning here, so
680
+ `classNames()` refuses to run in it.
681
+
682
+ A complete page — props, pseudo-classes, breakpoints, themes, `extend`, `components`, no framework
683
+ — is in [`examples/vanilla`](examples/vanilla): `npm run dev:vanilla`.
684
+
685
+ ## Architecture
686
+
687
+ The styling engine is framework-free. Everything that generates CSS — the 221 prop definitions,
688
+ the value formatters, class-name generation, rule ordering, the style sinks (CSSOM, `textContent`,
689
+ string for SSR, style elements for React 19), the flush scheduler, CSS variables and the theme
690
+ runtime — lives in `src/core/` and imports no React at all. CI fails the build if it ever does
691
+ (`npm run check:boundaries`), and it ships on its own as
692
+ [`@box-kite/core`](#using-the-core-without-react).
693
+
694
+ React is a thin adapter on top of it:
695
+
696
+ | | Files | Lines | Share |
697
+ | ---------------------------------------------------------- | ----- | ----- | -------- |
698
+ | Core engine (`src/core/`, `core.ts`) | 23 | 5,702 | 90.9% |
699
+ | React binding (`src/react/`, `box.ts`, `rsc.ts`, `ssg.ts`) | 14 | 573 | **9.1%** |
700
+
701
+ The binding is the whole React-specific surface: resolve class names during render, flush the
702
+ pending rules from `useInsertionEffect`, render the style elements of the Server-Component path,
703
+ and hold the theme state. React feature code the components share sits alongside it and is counted
704
+ separately — three helper hooks (`useVisibility`, `usePortalContainer`, `useVirtualization`), the
705
+ modules behind [`@box-kite/react/a11y`](#behaviour-primitives-for-your-own-components), the
706
+ 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
707
+ would need its own components, which says nothing about how much of the styling engine is
708
+ React-specific.
709
+
710
+ That ratio is not theoretical: the same engine is driven straight from the DOM in
711
+ [`examples/vanilla`](examples/vanilla) with no framework loaded, and from a server with no
712
+ `document` in `ssg.ts`. Numbers are printed by `npm run check:boundaries` and refreshed with the
713
+ code.
714
+
715
+ See [CONTRIBUTING.md](CONTRIBUTING.md#the-core-boundary) for the boundary rules and where new code
716
+ belongs.
717
+
718
+ ## AI editors and agents
719
+
720
+ The package ships its own instructions, because an agent has priors about a library this new and they
721
+ are wrong. `AGENTS.md` at the package root is the file agents look for — the rules, the dividers, one
722
+ worked example — and `docs/` sits beside it: every prop with the CSS it writes (`docs/props.md`),
723
+ every component with its import and whether it renders on a server (`docs/components.md`), and the
724
+ behaviour hooks (`docs/a11y.md`). All of it is generated during the build from the prop registry, the
725
+ rules file and the built chunks' own exports, so none of it can drift from the library it describes.
726
+
727
+ ```shell
728
+ cp node_modules/@box-kite/react/AGENTS.md ./AGENTS.md
729
+ ```
730
+
731
+ Codex, Cursor, Copilot, VS Code, Windsurf, Cline and Zed read a root `AGENTS.md` natively; Claude
732
+ Code reads `CLAUDE.md`, so a file holding the one line `@AGENTS.md` points it at the same place. The
733
+ long-form [BOX_KITE_AI_CONTEXT.md](src/BOX_KITE_AI_CONTEXT.md) ships in the tarball too.
734
+
735
+ The same rules are also a skill, which roughly forty-five coding agents install with one command:
736
+
737
+ ```shell
738
+ npx skills add box-kite/box-kite # Claude Code, Cursor, Codex, Copilot, Gemini CLI, Zed…
739
+ ```
740
+
741
+ In Claude Code this repository is also a plugin marketplace — `/plugin marketplace add box-kite/box-kite`
742
+ then `/plugin install box-kite@box-kite` — and Cursor takes the rules as a rule file of its own:
743
+
744
+ ```shell
745
+ mkdir -p .cursor/rules && cp node_modules/@box-kite/react/.cursor/rules/box-kite.mdc .cursor/rules/
11
746
  ```
12
747
 
13
- Follow the project at [github.com/box-kite/box-kite](https://github.com/box-kite/box-kite).
748
+ Both are generated from the same rules file as `AGENTS.md`, and both are served by the docs site
749
+ ([skill.md](https://www.box-kite.dev/skill.md), [box-kite.mdc](https://www.box-kite.dev/box-kite.mdc))
750
+ for an agent that can fetch a URL but not run a command.