@box-kite/react 1.0.0 → 2.0.1

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 (179) hide show
  1. package/.claude/rules/box-kite-rules.md +23 -1
  2. package/.claude/skills/box-kite/SKILL.md +134 -427
  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 +1124 -153
  10. package/BOX_KITE_AI_CONTEXT.md +1124 -153
  11. package/README.md +36 -2
  12. package/a11y.cjs +1 -1
  13. package/a11y.d.ts +1 -1
  14. package/a11y.mjs +3 -2
  15. package/anchor.cjs +1 -0
  16. package/anchor.d.ts +14 -0
  17. package/anchor.mjs +3 -0
  18. package/behavior.cjs +1 -1
  19. package/behavior.mjs +118 -136
  20. package/box.cjs +1 -1
  21. package/box.mjs +1 -1
  22. package/client.cjs +1 -1
  23. package/client.mjs +15 -14
  24. package/components/accordion.cjs +1 -0
  25. package/components/accordion.d.ts +114 -0
  26. package/components/accordion.mjs +251 -0
  27. package/components/button.cjs +1 -1
  28. package/components/button.d.ts +3 -0
  29. package/components/button.mjs +1 -1
  30. package/components/chart.cjs +1 -1
  31. package/components/chart.d.ts +2 -1
  32. package/components/chart.mjs +159 -184
  33. package/components/checkbox.cjs +1 -1
  34. package/components/checkbox.d.ts +8 -0
  35. package/components/checkbox.mjs +1 -1
  36. package/components/combobox/comboboxChips.d.ts +21 -0
  37. package/components/combobox/comboboxListbox.d.ts +47 -0
  38. package/components/combobox.cjs +1 -0
  39. package/components/combobox.d.ts +121 -0
  40. package/components/combobox.mjs +563 -0
  41. package/components/dataGrid/components/dataGridAggregateValue.d.ts +10 -0
  42. package/components/dataGrid/components/dataGridCell.d.ts +2 -1
  43. package/components/dataGrid/components/dataGridCellEditor.d.ts +15 -0
  44. package/components/dataGrid/components/dataGridCellPlaceholder.d.ts +14 -0
  45. package/components/dataGrid/components/dataGridCellTree.d.ts +16 -0
  46. package/components/dataGrid/components/dataGridDataSourceError.d.ts +12 -0
  47. package/components/dataGrid/components/dataGridExport.d.ts +10 -0
  48. package/components/dataGrid/components/dataGridFooter.d.ts +11 -0
  49. package/components/dataGrid/contracts/dataGridContract.d.ts +474 -16
  50. package/components/dataGrid/export/gridExport.d.ts +10 -0
  51. package/components/dataGrid/gridNavigationContext.d.ts +9 -1
  52. package/components/dataGrid/models/aggregateCellModel.d.ts +34 -0
  53. package/components/dataGrid/models/aggregationModel.d.ts +32 -0
  54. package/components/dataGrid/models/cellKeys.d.ts +11 -0
  55. package/components/dataGrid/models/cellModel.d.ts +27 -1
  56. package/components/dataGrid/models/columnModel.d.ts +12 -2
  57. package/components/dataGrid/models/dataSourceModel.d.ts +177 -0
  58. package/components/dataGrid/models/editModel.d.ts +77 -0
  59. package/components/dataGrid/models/exportModel.d.ts +51 -0
  60. package/components/dataGrid/models/gridModel.d.ts +108 -21
  61. package/components/dataGrid/models/groupRowCellModel.d.ts +11 -3
  62. package/components/dataGrid/models/groupRowModel.d.ts +22 -2
  63. package/components/dataGrid/models/rowModel.d.ts +13 -1
  64. package/components/dataGrid/models/sourceGroupRowModel.d.ts +45 -0
  65. package/components/dataGrid/models/sourceLevel.d.ts +71 -0
  66. package/components/dataGrid/models/sourceRowModel.d.ts +29 -0
  67. package/components/dataGrid/models/sourceTreeRowModel.d.ts +23 -0
  68. package/components/dataGrid/models/treeModel.d.ts +94 -0
  69. package/components/dataGrid/models/treeRow.d.ts +22 -0
  70. package/components/dataGrid/models/treeRowModel.d.ts +50 -0
  71. package/components/dataGrid.cjs +1 -1
  72. package/components/dataGrid.d.ts +4 -2
  73. package/components/dataGrid.mjs +4484 -2755
  74. package/components/dialog.cjs +1 -0
  75. package/components/dialog.d.ts +151 -0
  76. package/components/dialog.mjs +223 -0
  77. package/components/dropdown.cjs +1 -1
  78. package/components/dropdown.d.ts +30 -0
  79. package/components/dropdown.mjs +256 -244
  80. package/components/icon.cjs +1 -1
  81. package/components/icon.d.ts +2 -1
  82. package/components/icon.mjs +3 -3
  83. package/components/menu.cjs +1 -0
  84. package/components/menu.d.ts +234 -0
  85. package/components/menu.mjs +422 -0
  86. package/components/overlay.cjs +1 -1
  87. package/components/overlay.d.ts +30 -26
  88. package/components/overlay.mjs +45 -75
  89. package/components/popover.cjs +1 -0
  90. package/components/popover.d.ts +100 -0
  91. package/components/popover.mjs +69 -0
  92. package/components/presence.d.ts +6 -0
  93. package/components/progress.cjs +1 -0
  94. package/components/progress.d.ts +47 -0
  95. package/components/progress.mjs +28 -0
  96. package/components/radioButton.cjs +1 -1
  97. package/components/radioButton.d.ts +8 -0
  98. package/components/radioButton.mjs +1 -1
  99. package/components/radioGroup.cjs +1 -1
  100. package/components/radioGroup.d.ts +15 -2
  101. package/components/radioGroup.mjs +34 -30
  102. package/components/select.d.ts +9 -0
  103. package/components/semantics.cjs +1 -1
  104. package/components/semantics.d.ts +33 -28
  105. package/components/semantics.mjs +3 -3
  106. package/components/slider.cjs +1 -0
  107. package/components/slider.d.ts +98 -0
  108. package/components/slider.mjs +250 -0
  109. package/components/svg.cjs +1 -1
  110. package/components/svg.d.ts +25 -23
  111. package/components/svg.mjs +3 -3
  112. package/components/switch.cjs +1 -1
  113. package/components/switch.d.ts +9 -0
  114. package/components/switch.mjs +1 -1
  115. package/components/tabs.cjs +1 -0
  116. package/components/tabs.d.ts +87 -0
  117. package/components/tabs.mjs +324 -0
  118. package/components/textarea.cjs +1 -1
  119. package/components/textarea.d.ts +14 -0
  120. package/components/textarea.mjs +1 -1
  121. package/components/textbox.cjs +1 -1
  122. package/components/textbox.d.ts +12 -0
  123. package/components/textbox.mjs +1 -1
  124. package/components/toaster.cjs +1 -0
  125. package/components/toaster.d.ts +106 -0
  126. package/components/toaster.mjs +295 -0
  127. package/components/tooltip.cjs +1 -1
  128. package/components/tooltip.d.ts +39 -4
  129. package/components/tooltip.mjs +56 -54
  130. package/docs/a11y.md +199 -0
  131. package/docs/anchor.md +139 -0
  132. package/docs/components.md +48 -0
  133. package/docs/index.md +11 -0
  134. package/docs/props.md +1777 -0
  135. package/effects.cjs +1 -1
  136. package/effects.mjs +1 -1
  137. package/gridExport.cjs +2 -0
  138. package/gridExport.mjs +244 -0
  139. package/identity.cjs +1 -0
  140. package/identity.mjs +12 -0
  141. package/keyboard.cjs +1 -0
  142. package/keyboard.mjs +30 -0
  143. package/layer.cjs +1 -0
  144. package/layer.mjs +127 -0
  145. package/motion.cjs +1 -1
  146. package/motion.mjs +1 -1
  147. package/package.json +134 -10
  148. package/placement.cjs +1 -0
  149. package/placement.mjs +180 -0
  150. package/platform.cjs +1 -1
  151. package/platform.mjs +18 -5
  152. package/react/anchor/useAnchorPosition.d.ts +79 -0
  153. package/react/dialog/useScrollLock.d.ts +12 -0
  154. package/react/hooks/usePortalContainer.d.ts +6 -1
  155. package/react/hooks/useVirtualization.d.ts +24 -35
  156. package/react/popover/usePopoverLayer.d.ts +87 -0
  157. package/react-shared.cjs +1 -1
  158. package/react-shared.mjs +19 -2
  159. package/rsc.cjs +1 -1
  160. package/rsc.mjs +1 -1
  161. package/utils/accordion/accordionUtils.d.ts +48 -0
  162. package/utils/anchor/anchorUtils.d.ts +79 -0
  163. package/utils/array/lazyList.d.ts +10 -0
  164. package/utils/chart/chartPalette.d.ts +15 -0
  165. package/utils/chart/chartUtils.d.ts +2 -8
  166. package/utils/combobox/comboboxUtils.d.ts +89 -0
  167. package/utils/dom/domUtils.d.ts +6 -0
  168. package/utils/environment/environmentUtils.d.ts +10 -0
  169. package/utils/export/csvUtils.d.ts +23 -0
  170. package/utils/export/downloadUtils.d.ts +12 -0
  171. package/utils/export/xlsxUtils.d.ts +56 -0
  172. package/utils/export/zipUtils.d.ts +26 -0
  173. package/utils/keyboard/typeaheadUtils.d.ts +31 -0
  174. package/utils/menu/menuUtils.d.ts +25 -0
  175. package/utils/slider/sliderUtils.d.ts +67 -0
  176. package/utils/tabs/tabsUtils.d.ts +75 -0
  177. package/utils/toast/toastStore.d.ts +73 -0
  178. package/utils/virtual/virtualUtils.d.ts +47 -0
  179. /package/react/{a11y → identity}/useIdentifier.d.ts +0 -0
@@ -0,0 +1,235 @@
1
+ # Components reference
2
+
3
+ Which component replaces which `<Box tag>`, and the three that carry a pattern of their own. Read this when choosing a component or wiring a Dropdown, a Select or a DataGrid.
4
+
5
+ ## Component Shortcuts
6
+
7
+ | Instead of… | Use | Import |
8
+ | ------------------------------------ | -------------------------------------- | ---------------------------------------------------------------- |
9
+ | `<Box display="flex/grid">` | `<Flex>`/`<Grid>` | `components/flex`, `components/grid` |
10
+ | `<Box tag="button/input/textarea">` | `<Button>`/`<Textbox>`/`<Textarea>` | `components/button`, `components/textbox`, `components/textarea` |
11
+ | `<Box tag="a/img/label">` | `<Link>`/`<Img>`/`<Label>` | `components/semantics` |
12
+ | `<Box tag="h1..h6/p/span">` | `<H1>..<H6>`/`<P>`/`<Span>` | `components/semantics` |
13
+ | `<Box tag="ul/ol/li">` | `<Ul>`/`<Ol>`/`<Li>` | `components/semantics` |
14
+ | `<Box tag="nav/header/footer/main">` | `<Nav>`/`<Header>`/`<Footer>`/`<Main>` | `components/semantics` |
15
+ | `<Box tag="section/article/aside">` | `<Section>`/`<Article>`/`<Aside>` | `components/semantics` |
16
+ | `<Box tag="form">` | `<Form>` | `components/form` |
17
+ | `<Box tag="svg/path/circle/rect">` | `<Svg>`/`<Path>`/`<Circle>`/`<Rect>` | `components/svg` |
18
+ | a menu button and its menu | `<Menu>` | `components/menu` |
19
+ | sections that open one at a time | `<Accordion>` | `components/accordion` |
20
+ | one "show more" disclosure | `<Collapsible>` | `components/accordion` |
21
+ | a value dragged along a track | `<Slider>` | `components/slider` |
22
+ | how far a task has got | `<Progress>` | `components/progress` |
23
+ | a message sent from anywhere | `<Toaster>` + `toast()` | `components/toaster` |
24
+ | a lucide/Tabler icon, styled | `<Icon>` | `components/icon` |
25
+ | a sparkline, ring, gauge or donut | `<Sparkline>`/`<ProgressRing>`/… | `components/chart` |
26
+ | a themed Recharts (or any) chart | `<ChartContainer>` | `components/chart` |
27
+
28
+ Also: `Mark`, `Figure`, `Figcaption`, `Details`, `Summary`, `MenuList` (the semantic `<menu>`; the menu **button** is `Menu` from `components/menu`), `Time`. All from `@box-kite/react/components/...`.
29
+
30
+ `<Form<T> onSubmit={(values, e) => …}>` reads its own named fields on submit (after `preventDefault()`): a value per
31
+ named input, a boolean for a lone checkbox/radio, an array for a repeated name, and `name="a.b"` nests. `T` is the shape you
32
+ expect, not a check against the fields.
33
+
34
+ ## Accordion and Collapsible
35
+
36
+ ```tsx
37
+ import Accordion, { Collapsible } from '@box-kite/react/components/accordion';
38
+
39
+ <Accordion defaultValue={['shipping']} multiple onValueChange={(open, { reason }) => log(open, reason)}>
40
+ <Accordion.Item value="shipping">
41
+ <Accordion.Trigger>Shipping</Accordion.Trigger>
42
+ <Accordion.Panel>Two to four working days.</Accordion.Panel>
43
+ </Accordion.Item>
44
+ <Accordion.Item value="audit" disabled>
45
+ <Accordion.Trigger>Audit log</Accordion.Trigger>
46
+ <Accordion.Panel>On the Team plan.</Accordion.Panel>
47
+ </Accordion.Item>
48
+ </Accordion>;
49
+
50
+ <Collapsible trigger={(trigger) => <Button {...trigger}>What is in the box?</Button>}>A kite.</Collapsible>;
51
+ ```
52
+
53
+ `Accordion`: `value`/`defaultValue` (`string[]`), `onValueChange(open, { reason })`, `multiple` (default
54
+ `false`), `loop` (default `true`), `level` (`1`–`6`, default `3`). `Accordion.Item`: `value`, `disabled`.
55
+ `Accordion.Trigger`: `level` to override one header, `arrow` (default `true`) for the built-in chevron.
56
+ `Accordion.Panel`: every Box prop. `Collapsible`: `trigger`, `open`/`defaultOpen`, `onOpenChange`.
57
+
58
+ The height animation is a shared class — a one-row grid whose track runs `1fr` to `0fr` — so nothing is
59
+ measured and nothing is written per instance. A closed panel is hidden with `visibility` and stays in the
60
+ DOM, which is what keeps its state and what gives the exit a value to animate from. Every header is its
61
+ own tab stop; Down/Up/Home/End are a shortcut. Style the open state with `ariaAttr={{ expanded: … }}`,
62
+ never a variant, and leave `accordion.track`/`accordion.clip` alone: padding on a grid item cannot be
63
+ squeezed, so it floors the `0fr` track — which is why your padded panel sits inside the clip (#142).
64
+
65
+ ## Slider and Progress
66
+
67
+ ```tsx
68
+ import Slider from '@box-kite/react/components/slider';
69
+ import Progress from '@box-kite/react/components/progress';
70
+
71
+ <Slider label="Volume" defaultValue={40} onValueChange={(value) => setVolume(value)} />;
72
+ <Slider label="Price" defaultValue={[20, 80]} thumbLabels={['Lowest', 'Highest']} format={(v) => `${v} lei`} />;
73
+ <Progress label="Upload" value={62} />;
74
+ <Progress label="Preparing" />;
75
+ ```
76
+
77
+ **A number in is a number out.** `defaultValue={40}` is one thumb and `onValueChange` hands back a
78
+ `number`; `defaultValue={[20, 80]}` is a range and it hands back a `number[]`. The value's own shape is
79
+ how many thumbs there are — there is no second prop for it, and no narrowing at the call site.
80
+
81
+ `Slider`: `value`/`defaultValue`, `onValueChange(value, { reason })` on every step and
82
+ `onValueCommit(value, { reason })` once at the end of the interaction (`'pointer'` or `'keyboard'`),
83
+ `min`/`max`/`step`/`largeStep`, `orientation`, `disabled`, `label`/`labelledBy`/`thumbLabels`, `format`
84
+ (writes `aria-valuetext`) and `name`, which submits one hidden input per thumb. Parts:
85
+ `slider`/`slider.track`/`slider.fill`/`slider.thumb`.
86
+
87
+ `Progress`: `value` (leave it out for indeterminate), `min`/`max`, `label`/`labelledBy`, `format`. Parts:
88
+ `progress` (the track, and it carries the role) and `progress.fill`, whose `indeterminate` variant sweeps.
89
+ It renders on a **server** — no state, no effect, no measurement.
90
+
91
+ Four things to know. **Neither is an `<input type="range">` or a `<progress>`**: a range input holds one
92
+ thumb and both draw themselves with vendor pseudo-elements no typed prop reaches — `name` is what keeps
93
+ the form working. **The position is an inline style**, the one thing here that is not a shared class: a
94
+ thumb's offset is per frame of a drag, so a class for it would be a rule per frame, never freed — where a
95
+ `ProgressRing` rounds its fraction into a class because nobody drags a ring. **No `value` is a state, not
96
+ a zero**: an indeterminate bar omits `aria-valuenow` rather than reporting `0`. And a `role="slider"`
97
+ **has to be named** — `label` names one thumb, and on a range it names the `role="group"` while
98
+ `thumbLabels` names the thumbs. The geometry is logical, so a right-to-left page mirrors for free; only
99
+ the sideways arrows swap, and Up/Down never do.
100
+
101
+ ## Toaster
102
+
103
+ ```tsx
104
+ import Toaster, { toast } from '@box-kite/react/components/toaster';
105
+
106
+ // once, near the root of the app
107
+ <Toaster position="bottom-end" limit={3} />;
108
+
109
+ // then from anywhere at all — no React needed
110
+ toast.success('Saved');
111
+ toast.error('Could not save', { action: { label: 'Retry', onClick: save } });
112
+ toast('Row deleted', { description: 'Ada Lovelace, added in March.', action: { label: 'Undo', onClick: restore } });
113
+ toast.promise(save(), { loading: 'Saving…', success: (saved) => `Saved as ${saved.name}`, error: 'Could not save' });
114
+ ```
115
+
116
+ **It is called, not rendered.** The store behind `toast()` has no React in it, so a message can come from
117
+ an event handler, a fetch, a router guard or a module that has never heard of a component — and a call
118
+ made _before_ the viewport mounts is queued rather than lost.
119
+
120
+ `toast(message, options)` returns the id, with `success`/`error`/`warning`/`info`/`loading` beside it,
121
+ plus `toast.promise`, `toast.update(id, message, options)` and `toast.dismiss(id?)`. An `options.id`
122
+ already on screen is an **update**, which is what makes a promise one toast rather than two. Options:
123
+ `description`, `action` (`{ label, onClick, closeOnClick }` — it dismisses the toast it answered unless
124
+ told otherwise), `duration` in **milliseconds** (`Infinity` waits; `loading` already does), `dismissible`
125
+ and `onDismiss(reason)` with `'timeout'`/`'close'`/`'action'`/`'imperative'`.
126
+
127
+ `Toaster`: `position` (six corners, the inline half logical — default `'bottom-end'`), `limit` (3),
128
+ `duration` (5000), `label`, `closeLabel`, `overflowLabel(count)`, `hotkey` (`'F6'`, `'alt+t'`, or
129
+ `false`) and `store`, for a second independent stack or a test that must not share state
130
+ (`createToastStore()` is exported from the same module). Parts: `toaster`, `toaster.toast` (the kind is a
131
+ variant on it), `toaster.message`, `toaster.description`, `toaster.action`, `toaster.close`,
132
+ `toaster.overflow`.
133
+
134
+ **The limit is a queue, not a cap.** Past it a toast waits _with its timer unstarted_, so nothing expires
135
+ that was never on screen, and a counter at the far end of the stack says how many are waiting.
136
+
137
+ Four things to know. The viewport is a **polite live region that exists before there is anything in it** —
138
+ a region inserted together with its content is not reliably announced — and an error toast is
139
+ `role="alert"`, which is assertive; nothing else carries a region of its own, because the nearest one to a
140
+ change is the one that speaks. It is in the **top layer with no portal**, so it inherits the theme and the
141
+ direction around it, and it takes **no pointer events** while the toasts take them back, or a corner-sized
142
+ fixed strip would swallow every press in its gaps. **Timers stop on hover, on focus and in a background
143
+ tab** and resume where they left off (WCAG 2.2.1 — the only reason a toast may carry a control). And a
144
+ toast **never takes focus**: `F6` moves focus to the stack, Tab walks it, and Escape dismisses the toast
145
+ focus is in, handing focus back once the stack is empty.
146
+
147
+ ## Dropdown
148
+
149
+ The APG combobox — select-only by default, the editable one with `isSearchable`. **Always pass `label`** (or your own `aria-label` via `props`) — a combobox is
150
+ not named by its content, so without one it has no accessible name. Never add `role="combobox"`/`role="option"`
151
+ by hand; the component supplies the whole pattern.
152
+
153
+ ```tsx
154
+ import Dropdown from '@box-kite/react/components/dropdown';
155
+ <Dropdown<string> label="Fruit" defaultValue="a" onValueChange={(fruit, { reason }) => {}}>
156
+ <Dropdown.Unselect>Pick...</Dropdown.Unselect>
157
+ <Dropdown.Item value="a">Alpha</Dropdown.Item>
158
+ </Dropdown>;
159
+ // Multiple: <Dropdown label="Fruit" multiple showCheckbox isSearchable searchPlaceholder="Search...">
160
+ // <Dropdown.SelectAll>All</Dropdown.SelectAll> <Dropdown.EmptyItem>No results</Dropdown.EmptyItem>
161
+ // <Dropdown.Display>{(values) => `${values.length} selected`}</Dropdown.Display>
162
+ ```
163
+
164
+ **Props**: `label`/`labelProps`, `value`/`defaultValue`, `multiple`, `isSearchable`, `searchPlaceholder`, `hideIcon`, `showCheckbox`, `name`, `onValueChange` (`(selection, { reason })` — `select`, `deselect`, `select-all`, `clear`; the selection is an array in `multiple` mode), `itemsProps`, `iconProps`, `variant` (propagates to children). All BoxProps. `onChange` is the deprecated `(value, values)` spelling and still fires.
165
+ **Sub-components**: `Item<T>` (requires `value`; `disabled` is skipped by the arrows), `Unselect`, `SelectAll`, `EmptyItem`, `Display` (static or `(values, isOpen) => ReactNode`).
166
+ **Keyboard (select-only)**: closed — Down/Up/Enter/Space/Home/End open, a printable character opens at the first match. Open — arrows move, Home/End jump, typing searches, Enter/Space choose, Escape closes unchanged, Tab chooses then leaves. DOM focus stays on the trigger throughout (`aria-activedescendant`).
167
+ **Keyboard (`isSearchable`)**: the `<input>` is the combobox, so printable keys type (no typeahead), Space types a space, Home/End and Left/Right move the caret and drop the highlight, only Down/Up reach the listbox, Enter chooses the highlighted option, and Escape closes before a second Escape clears the field. The field shows the selection as its value, unless a `Dropdown.Display` is drawing it.
168
+ **Style tree**: `dropdown` > `items`, `item` (variants: compact, multiple, highlighted), `unselect`, `selectAll`, `emptyItem`, `icon`.
169
+
170
+ ## Select
171
+
172
+ Data-driven dropdown — `data` + `def` instead of children. Shares `dropdown.*` style tree.
173
+
174
+ ```tsx
175
+ import Select from '@box-kite/react/components/select';
176
+ <Select<User, number>
177
+ label="User"
178
+ data={users}
179
+ def={{ valueKey: 'id', displayKey: 'name', placeholder: 'Pick...' }}
180
+ value={selected}
181
+ onValueChange={(value) => setSelected(value as number)}
182
+ />;
183
+ ```
184
+
185
+ **SelectDef**: `valueKey` (required), `displayKey`, `display` (`(row) => ReactNode`), `selectedDisplay` (`(rows, isOpen) => ReactNode`), `placeholder`, `selectAllText`, `emptyText`.
186
+ Also: `data`, `label`/`labelProps`, `value`/`defaultValue`, `multiple`, `isSearchable`, `searchPlaceholder`, `showCheckbox`, `hideIcon`, `name`, `onValueChange` (and the deprecated `onChange`), `itemsProps`, `iconProps`, `variant`, BoxProps. Same combobox pattern as Dropdown — including needing a name, and `isSearchable` switching it to the editable one.
187
+
188
+ ## DataGrid
189
+
190
+ ```tsx
191
+ import DataGrid from '@box-kite/react/components/dataGrid';
192
+ <DataGrid
193
+ data={users}
194
+ def={{
195
+ rowKey: 'id',
196
+ topBar: true,
197
+ bottomBar: true,
198
+ globalFilter: true,
199
+ rowSelection: { pinned: true },
200
+ showRowNumber: { pinned: true },
201
+ rowHeight: 40,
202
+ visibleRowsCount: 15,
203
+ columns: [
204
+ { key: 'name', header: 'Name', filterable: true },
205
+ { key: 'age', header: 'Age', width: 80, align: 'right', filterable: { type: 'number' } },
206
+ { key: 'status', header: 'Status', filterable: { type: 'multiselect' } },
207
+ { key: 'country', header: 'Country', pin: 'END' },
208
+ {
209
+ key: 'actions',
210
+ header: '',
211
+ width: 80,
212
+ sortable: false,
213
+ contextMenu: false,
214
+ Cell: ({ cell }) => <Button onClick={() => edit(cell.row.data)}>Edit</Button>,
215
+ },
216
+ ],
217
+ rowDetail: { content: (row) => <Details row={row} />, expandOnRowClick: true, expandColumnHeader: 'Details' },
218
+ contextMenu: { sort: true, pin: true, group: false },
219
+ resizerStyle: 'hover',
220
+ }}
221
+ onSelectionChange={(e) => console.log(e.selectedRowKeys)}
222
+ />;
223
+ ```
224
+
225
+ **DataGridProps**: `data`, `def`, `component` (default `'datagrid'`), `loading`, `filters` (predicate[]), `page`/`onPageChange`, `onSortChange`, `onServerStateChange` (`{ page, pageSize, sortColumn, sortDirection, columnFilters, globalFilterValue }`), `onSelectionChange` (`{ action, selectedRowKeys, affectedRowKeys, isAllSelected }`), `expandedRowKeys`/`onExpandedRowKeysChange`, `globalFilterValue`/`onGlobalFilterChange`, `columnFilters`/`onColumnFiltersChange`.
226
+
227
+ **GridDefinition**: `columns` (required), `rowKey`, `rowHeight` (px, default 48), `visibleRowsCount` (number/`'all'`), `showRowNumber` (bool/`{ pinned?, width? }`), `rowSelection` (bool/`{ pinned? }`), `rowDetail` (`{ content, height?, expandOnRowClick?, pinned?, expandColumnWidth?, expandColumnHeader? }`), `pagination` (`{ totalCount, pageSize? }`), `topBar`/`bottomBar`, `title`/`topBarContent`, `globalFilter`, `globalFilterKeys`, `sortable`/`resizable` (default true), `contextMenu` (bool/`{ sort?, pin?, group? }`, default true), `resizerStyle` (`'visible'`/`'hover'`/`'hidden'`), `noDataComponent`.
228
+
229
+ **ColumnType**: `key`, `header`, `width` (px, default 200), `align` (`'start'`/`'end'`/`'center'`, or the physical `'left'`/`'right'`), `pin` (`'START'`/`'END'` — an edge of the **inline axis**, so a pinned column holds under either `dir`; `'LEFT'`/`'RIGHT'` are the older spelling of the same two), `columns` (grouped headers), `Cell` (`({ cell }) => ReactNode`), `sortable`/`resizable` (override grid), `flexible`, `filterable` (`true`=text, `{ type: 'number' }`, `{ type: 'multiselect' }`), `contextMenu` (override grid).
230
+
231
+ **Server-side**: `def={{ pagination: { totalCount }, bottomBar: true }}` + `page={page}` + `onServerStateChange={fetchData}`.
232
+
233
+ **Accessibility (A7, A10)**: the APG grid pattern, over a virtualized body — supplied whole, so add no roles and no `tabIndex`. The scrolling element is `role="grid"` (header and body are `rowgroup`s, rows `role="row"`, cells `role="gridcell"`/`columnheader`, scroll spacers `presentation`). `aria-rowcount`/`-colcount` describe the _whole_ grid and `aria-rowindex`/`-colindex` place each rendered cell in it, header rows first. Also `aria-sort` on sortable headers, `aria-selected` on rows only with `rowSelection`, `aria-busy` while `loading`, `aria-expanded` on group and expanded rows, and a live region announcing the selection count. Keyboard: one cell in the tab order; arrows move it, Home/End along the row, Ctrl+Home/End to the corners (scrolling to unrendered rows), PageUp/Down by a screenful, Enter/Space sorts a header or steps into the cell’s control, F2 always steps in, Escape steps back out. Down/Up keep the _column_ rather than the cell ordinal, so they land under where they started even through a grouped header. The column resizer is APG's window splitter — `role="separator"` with `aria-valuenow`/`-valuemin`/`-valuemax` in pixels; Tab or F2 reaches it, the arrows move it 16px, Home/End go to the narrowest the grid allows and to the grid's own width. Every control the grid draws names itself after what it acts on ("Select row 4", "Column options for Age"); the column menu is the APG menu button. **Give it `def.title`** — a grid is not named by its rows. Known gap: Tab does not yet stay inside the grid.
234
+
235
+ **Style tree**: `datagrid` > `content`, `topBar` > (`globalFilter` > `stats`, `columnGroups` > `icon`|`separator`|`item` > `icon`, `columnVisibility` > `badge`), `filter` > `cell` > `input`, `header` > `cell` > (`contextMenu` > `icon`|`tooltip` > `item` > `icon`|`separator`, `resizer`), `body` > (`cell` > `text`|`rowDetail`, `row`, `groupRow` > `expandButton`, `detailRow`, `empty`), `emptyColumns`, `bottomBar` > (`info`, `clearFilters`, `pagination` > `button`|`info`).
@@ -0,0 +1,42 @@
1
+ # Extending Box
2
+
3
+ The two registries a consumer adds to: component styles with variants (`Box.components()`) and new props, new values or new variables (`Box.extend()`). Read this when a value is used twice, or when the prop needed does not exist.
4
+
5
+ ## Component System
6
+
7
+ ```tsx
8
+ <Box component="card" variant="bordered">
9
+ <Box component="card.header">Title</Box>
10
+ </Box>;
11
+
12
+ export const components = Box.components({
13
+ card: {
14
+ styles: { display: 'flex', d: 'column', p: 4, bgColor: 'white', borderRadius: 8, shadow: 'medium' },
15
+ variants: { bordered: { b: 1, borderColor: 'gray-200', shadow: 'none' } },
16
+ children: { header: { styles: { fontSize: 18, fontWeight: 600 } } },
17
+ },
18
+ subgrid: { extends: 'datagrid', styles: { b: 0, shadow: 'none' } },
19
+ });
20
+ ```
21
+
22
+ ## Extension System
23
+
24
+ ```tsx
25
+ export const { extendedProps, extendedPropTypes } = Box.extend(
26
+ { 'brand-primary': '#ff6600' }, // CSS variables
27
+ { imageRendering: [{ values: ['pixelated'] as const, styleName: 'image-rendering' }] }, // New props (`aspectRatio` is built in)
28
+ {
29
+ bgColor: [
30
+ {
31
+ values: ['brand-primary'] as const,
32
+ styleName: 'background-color', // Extend existing
33
+ valueFormat: (v, getVar) => getVar(v),
34
+ },
35
+ ],
36
+ },
37
+ );
38
+ // TypeScript: declare module '@box-kite/core/types' { namespace Augmented {
39
+ // interface BoxProps extends ExtractBoxStyles<typeof extendedProps> {}
40
+ // interface BoxPropTypes extends ExtractBoxStyles<typeof extendedPropTypes> {}
41
+ // interface ComponentsTypes extends ExtractComponentsAndVariants<typeof components> {} }}
42
+ ```
@@ -0,0 +1,27 @@
1
+ # Patterns and the rest of the surface
2
+
3
+ The recipes, plus the parts of the library that are not props: server rendering, React Server Components, the behaviour hooks, floating layers, form controls and the tooltip. Read this when the question is how a piece fits together rather than which prop to write.
4
+
5
+ ## Common Patterns
6
+
7
+ ```tsx
8
+ <Flex d="column" gap={4} ai="center" jc="between">{children}</Flex>
9
+ <Flex d="column" gap={2} md={{ d: 'row', gap: 4 }}>{children}</Flex>
10
+ <Grid gridCols={3} gap={4}>{items}</Grid>
11
+ <Box p={4} bgColor="white" borderRadius={8} shadow="medium" />
12
+ <Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" />
13
+ ```
14
+
15
+ **Group hover**: `<Box group={{ 'parent-class/hover': { opacity: 1 } }}>`
16
+ **SSR**: `import { getStyles, resetStyles } from '@box-kite/react/ssg'` — render, read `getStyles()` into `<style id="box-kite-styles">`, then `resetStyles()`. Needs no DOM.
17
+ **Server Components (React 19)**: works with no `'use client'` and no setup — the `react-server` entry renders Box hook-free and its CSS ships as `<style href precedence>` elements React hoists into `<head>`. Client components in the same app: `Box.configure({ sink: 'element' })` once in a `'use client'` module. Hook-free components (`Flex`, `Grid`, `Button`, `Textbox`, `Textarea`, `RadioButton`, `Icon`, the SVG elements, semantic tags) render on the server too — import them straight into a Server Component; `Dropdown`/`Tooltip`/`Overlay`/`Popover`/`Dialog`/`DataGrid`/`Checkbox`/`Switch`/`RadioGroup`/`Select`/`Form` ship a `'use client'` banner, so importing one just opens a client boundary. Client-only: hover-callback children, `Box.Theme` — but `theme={{ dark: {...} }}` styles work server-side (set the theme class on `<html>`). Element-mode rules are in `@layer`, so your own unlayered CSS wins. React 18: keep the default sink.
18
+ **Accessible behaviour** (`@box-kite/react/a11y`, 2.2 KB gz, client-only): `useControllableState` (controlled/uncontrolled with `onChange(value, { reason, event })` — `'escape'`, `'outside-pointer'`, yours), `useDismiss({ enabled, inside: [triggerRef, popupRef], onDismiss })` (Escape reaches the innermost layer only; pass the trigger or it dismisses and reopens in one press), `useFocusReturn({ enabled, returnTo })`, `useRovingFocus({ count, orientation, textOf, onSelect })` → `{ activeIndex, onKeyDown, itemProps(i), activeItem() }` (arrows, Home/End, typeahead, disabled skipped; `focusItems: false` for `aria-activedescendant`), `useIdentifier('select')` (React's `useId`, usable as a selector). They supply mechanics, never roles or ARIA. `VisuallyHidden` (`@box-kite/react/components/visuallyHidden`) is a Box that clips itself away instead of hiding, so it stays in the accessibility tree — and it renders on a server.
19
+ **Config**: `Box.configure({ sink: 'cssom' | 'textContent' | 'string' | 'element', classNames: 'hashed' | 'readable' | 'stable' })` — before the first render.
20
+ **Floating layers**: `Overlay` (`components/overlay`) renders its children where it is declared, in the browser's **top layer** (`popover="manual"`, shown on mount) — so it beats every stacking context, escapes every clipped and transformed ancestor, and still inherits the theme, the custom properties and the direction around it. `manual` because it owns no dismissal, no ARIA and no open state. `Tooltip`, the `Dropdown` popup and the DataGrid column menu are all this. **Never declare one inside its trigger** — a listbox or a menu inside a `<button>` is unreachable content, and the portal used to hide that by moving it. Where the browser has no Popover API it is portalled into `#box-kite-portal` instead, which is the old behaviour. Reach for it only when you want positioning with no pattern on it; a panel with content in it is `Popover`.
21
+ **Form controls** (`components/checkbox`, `components/switch`, `components/radioGroup`, `components/radioButton`): real native inputs, so focus, the checked state and form submission come from the platform. `label` is the prop that matters — it renders the text inside a `<label>` wrapping the input, so there is no `htmlFor`/`id` pair and the whole row is clickable; `labelProps` styles that label, every other Box prop styles the control. `<Checkbox label="Accept the terms" name="terms" />`, `<Checkbox label="Select all" indeterminate />` (sets the DOM property **and** `aria-checked="mixed"`), `<Switch label="Email notifications" name="notify" />` (`role="switch"`; Space and Enter both toggle, Enter does not submit the form). `<RadioGroup label="Plan" name="plan" defaultValue="free" onValueChange={(v, { reason }) => …}>` with `<RadioGroup.Item value="free" label="Free" />` children: `role="radiogroup"` named by the label, one shared `name` (generated if omitted), `orientation` `vertical`|`horizontal`, arrows move **and** select with wrapping and disabled items skipped, and Tab enters/leaves the set once — the platform's own tab order is left alone. Style tree `radioGroup` (a `horizontal` variant) > `label`. `RadioButton` alone is a plain radio and still renders on a server. `checked`/`disabled`/`indeterminate` are each a state _and_ a pseudo-class style prop: pass a boolean, or `[state, styles]` for both.
22
+ **Popover** (`components/popover`): a panel on the platform's Popover API, and the answer whenever a layer has content a user interacts with. `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`. **No portal**: the panel is in the browser's top layer, so it beats every stacking context and clipped ancestor while keeping the theme, custom properties, direction and tab order it was declared in. Light dismiss (Escape, outside press) and focus return are the browser's; the component adds `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus into the panel (`autoFocus`), the `side`/`align`/`offset`/`flip`/`matchWidth` placement props, and `onOpenChange(open, { reason })` with `trigger`|`escape`|`outside-pointer`|`imperative`. Traps: the trigger must be a **button** (the toggle is the browser's `popovertarget`) and must never carry an `onClick` toggle of your own — light dismiss closes on `pointerdown`, so the `click` after it reopens the panel; the panel is **always rendered** (closed is `display: none`), so the exit is `transitionBehavior="allow-discrete"` and never `<Presence>`, and expensive children want `{open ? … : null}`; and a **close cannot be refused**, since `beforetoggle` is cancelable opening only. Without the Popover API it falls back to a portalled `Overlay` + `useDismiss` + `useFocusReturn`.
23
+ **Dialog / AlertDialog** (`components/dialog`): a real `<dialog>` shown with `showModal()`, so **the top layer, the `::backdrop`, an inert page, Escape, focus containment and focus return are the platform's** — no focus trap, no `aria-hidden` sweep, no `z-index`. `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>`. Rendering a `Dialog.Title` writes `aria-labelledby` and a `Dialog.Description` writes `aria-describedby`; neither attribute exists when the part is absent, so a dialog with no visible heading wants `label`/`labelledBy`/`describedBy`. Props: `trigger` (optional — open it from anywhere), `open`/`defaultOpen`/`onOpenChange(open, { reason })` with `trigger`|`escape`|`outside-pointer`|`imperative`, `modal` (default `true`; `false` is `show()` — no backdrop, nothing inert, positioned against its containing block), `dismissible`, `lockScroll` (default: `modal`) and `initialFocus`. `AlertDialog` is a named export of the same module: `role="alertdialog"`, always modal, never dismissed by a press outside, and `initialFocus` on the least destructive action (APG). Traps, all measured: the dialog is **always rendered** (closed is `display: none`), so the exit is a CSS transition and never `<Presence>`; a **close cannot be refused** (`cancel` fires after the fact — use `dismissible={false}`); a press outside is `closedby="any"`, and on a browser without it the press is measured against the dialog's box, because **for a modal dialog the backdrop is the dialog element**; and the platform does not stop the page scrolling, which is what `lockScroll` is for — an `overflow: hidden` class on `<html>`, counted so nested dialogs cannot unlock it early.
24
+
25
+ **Menu** (`components/menu`): APG's menu button on the same Popover API, and a submenu is a popover nested inside it. `<Menu trigger={(t) => <Button {...t}>Actions</Button>}><Menu.Item onSelect={duplicate}>Duplicate</Menu.Item><Menu.Separator /><Menu.Sub label="Share"><Menu.Item>Copy link</Menu.Item></Menu.Sub></Menu>`. Parts: `Menu.Item`, `Menu.CheckboxItem` (`checked`/`defaultChecked`/`onCheckedChange`), `Menu.RadioGroup` (`value`/`defaultValue`/`onValueChange`) + `Menu.RadioItem value`, `Menu.Group label` (which names the group through `aria-labelledby`), `Menu.Separator`, and `Menu.Sub label`, which renders its own `menuitem` with `aria-haspopup="menu"` and `aria-expanded` plus the menu beside it. Props on the menu: `trigger`, `open`/`defaultOpen`/`onOpenChange(open, { reason })` with `select`|`tab` beside `trigger`|`escape`|`outside-pointer`|`imperative`, `label`/`labelledBy` (left out, the menu takes its trigger's name — APG's rule), and the four placement props `side`/`align`/`offset`/`flip`, which `Menu.Sub` takes too (`side="end"`, `offset={0}` by default, so a submenu abuts its menu). The platform supplies the top layer, light dismiss — Escape closing the innermost menu first — and the toggle; the component supplies the roles, the keys (Down/Up wrapping, Home/End, typeahead where a letter again cycles, Right/Left for a submenu in **reading order**, so they swap in a right-to-left menu) and the focus moves the browser does not make: into the first item on open, and back onto a submenu's own item when it closes. Four things to know: a `disabled` item is `aria-disabled` and **stays focusable**, as APG asks; `Menu.Item` closes the menu on select while a checkbox or radio item does not, and `closeOnSelect` swaps either default; the menu is **always rendered** (closed is `display: none`), so the exit is a CSS transition and never `<Presence>`, and expensive items want gating with `{open ? … : null}`; and it inherits the top layer's one cost, keeping the side it opened on (see `Overlay`). The semantic `<menu>` element is `MenuList` from `components/semantics` now, since two exports named `Menu` are one import away from the wrong component.
26
+
27
+ **Tooltip** (`components/tooltip`): the APG pattern on that layer. `<Tooltip content="Deletes the row">{(trigger) => <Button {...trigger}>Delete</Button>}</Tooltip>` — the child is a render prop because `aria-describedby` has to land on the control itself; the bag is `{ ref, props }`, so one spread on a Box component, or `<button ref={trigger.ref} {...trigger.props}>` on a plain one. The `ref` is what positions the bubble under the trigger. Gives you `role="tooltip"`, the describedby wiring while open, hover **and** focus opening (`openDelay` 300 ms, focus ignores it), Escape with focus left in place, and WCAG 1.4.13: the pointer can travel onto the bubble (`closeDelay` 150 ms) and nothing hides it on a timer. `open`/`defaultOpen`/`onOpenChange(open, { reason })` with reasons `hover`|`focus`|`pointer-leave`|`blur`|`escape`; other Box props style the bubble.
@@ -0,0 +1,222 @@
1
+ # Styling reference
2
+
3
+ Every prop by category, the six kinds of nesting, and the theme system. Read this when writing props you have not written before; `props.md` in this package (or the `/props.md` address on the docs site) is the exhaustive, measured version.
4
+
5
+ ## Props
6
+
7
+ **Spacing**: `p`/`px`/`py`/`pt`/`pr`/`pb`/`pl`/`ps`/`pe`, `m`/`mx`/`my`/`mt`/`mr`/`mb`/`ml`/`ms`/`me`, `gap` — the axis pair is already logical (`px` is `padding-inline`) and `ps`/`pe`, `ms`/`me` are its two sides
8
+ **Layout**: `display`, `d` (flex-direction), `wrap`, `ai` (align-items), `jc` (justify-content), `justifyItems`/`placeItems`/`placeContent`/`alignContent`/`alignSelf`/`justifySelf`, `flex`/`grow`/`shrink`, `container`/`containerName`/`containerType` (a query container — see `cq`). Every alignment prop also takes the **overflow-safe** forms — `'safe center'`, `'safe start'`, `'safe end'` and the `'unsafe …'` twins: `safe center` centres until the content stops fitting and then aligns to `start`, where plain `center` overflows both edges and the half above the scrollable origin cannot be reached at all. `aspectRatio` is `'square'` (1/1), `'video'` (16/9), a compact ratio (`'4/3'`), a number, or `'auto'`
9
+ **Sizing**: `width`/`height` — number (÷4=rem), `'auto'`, `'fit'` (100%), `'fit-screen'` (100vw/vh), fractions (`'1/2'`…), `'33%'`. `minWidth`/`maxWidth`/`minHeight`/`maxHeight` same. All accept `"5%"`.
10
+ **Colors**: `bgColor`/`color`/`borderColor`/`outlineColor`/`fill`/`stroke` — Tailwind's OKLCH palette, 26 families × 11 steps `'50'`..`'950'` (slate/gray/zinc/neutral/stone/mauve/mist/olive/taupe/red/orange/amber/yellow/lime/green/emerald/teal/cyan/sky/blue/indigo/violet/purple/fuchsia/pink/rose), plus `'white'`/`'black'`/`'transparent'`/`'currentColor'`. **Opacity modifier on any colour value**: `bgColor="blue-500/40"` → `color-mix(in oklab, var(--blue-500) 40%, transparent)` — the mix wraps the _variable_, so it stays themed and shared; unlike `opacity` it fades one declaration, not the element. Works in `vars` and on a `Box.extend()` variable (`"brand/30"`); an unknown token or a percentage outside 0–100 emits nothing
11
+ **Borders**: `b`/`bx`/`by`/`bt`/`br`/`bb`/`bl`/`bs`/`be` (px — `bs`/`be` are the logical sides), `borderRadius` (÷4) and its nine physical corners plus six logical ones (`borderRadiusStart`/`borderRadiusEnd`, and `borderRadiusStartStart`/`StartEnd`/`EndEnd`/`EndStart` — **block axis first, inline axis second**), `borderStyle`
12
+ **Text**: `fontSize` (÷16), `fontWeight`, `lineHeight` (px), `textAlign` (`'start'`/`'end'` follow the text direction where `'left'`/`'right'` do not)/`textDecoration`/`textTransform`/`whiteSpace`/`textOverflow`, `textWrap`
13
+ **Position**: `position`, `top`/`right`/`bottom`/`left`/`inset`/`insetX`/`insetY`/`insetStart`/`insetEnd`, `zIndex` — number (÷4), `'auto'`, fractions (`'1/2'`, `'-1/3'`) or a percentage; `insetX`/`insetY` are `inset-inline`/`inset-block`, the way `mx`/`my` are `margin-inline`/`margin-block`, and `insetStart`/`insetEnd` are that axis's two sides
14
+ **SVG paint & stroke**: `fill`/`stroke` (a colour variable, or a reference CSS resolves itself — `url(#sky)` for a gradient, pattern or `<ClipPath>`, `var(--chart-1)` for somebody else's variable; `clipPath` takes `url(#…)` the same way, and anything else emits no rule at all), `fillOpacity`/`strokeOpacity` (0–1 in tenths), `fillRule`, `strokeWidth`/`strokeDasharray`/`strokeDashoffset`/`strokeMiterlimit` (**user units — no divider**; a dash pattern with a gap is a string, `"12 4"`), `strokeLinecap`, `strokeLinejoin`, `paintOrder`, `vectorEffect`, `shapeRendering`. All inherited except `vectorEffect`, so set them on the `<svg>` (`Svg`) rather than on every shape.
15
+
16
+ **SVG text**: `textAnchor` (`start`/`middle`/`end` — which part of a label sits on its `x`) and `dominantBaseline` (`alphabetic`/`central`/`hanging`/… — which part sits on its `y`). `textAnchor` is inherited; `dominantBaseline` is not, so like `vectorEffect` its rule targets the element _and_ its descendants and still works on the `<svg>`.
17
+
18
+ **SVG geometry** (SVG 2, all **user units — no divider**, or a percentage): `cx`/`cy` (`<circle>`, `<ellipse>`), `r` (`<circle>`), `rx`/`ry` (`<ellipse>` radii, `<rect>` corners; also `auto`), `x`/`y` (`<rect>`, `<image>`, `<use>`, `<foreignObject>`, nested `<svg>` — **not** `<text>`). They are real CSS, so they transition: `<Circle r={38} hover={{ r: 40 }} />` is a whole animation. Not inherited — put them on the shape. A `<rect>`'s `width`/`height` are NOT in this family (those prop names are the ÷4 layout scale), which is why `<Rect>` claims them back as its own attributes — `<Rect width={40} height={40} />`. A path's `d` stays an attribute too (no Safari support), and `<Path d="M…" />` is how you write it.
19
+
20
+ **SVG elements** (`@box-kite/react/components/svg`, 20 components, server-safe): `Svg`, `G`, `Defs`, `Path`, `Circle`, `Ellipse`, `Rect`, `Line`, `Polyline`, `Polygon`, `SvgText`, `TSpan`, `LinearGradient`, `RadialGradient`, `Stop`, `ClipPath`, `Mask`, `Use`, `SvgSymbol`, `Marker` — so a drawing never writes `tag`. Each one is a Box and takes every prop above. **Each also settles the names SVG and Box both use, for its own element**: `Path`'s `d` is path data, `Rect`'s `width`/`height` are user units, `SvgText`/`TSpan`'s `x`/`y`/`dx`/`dy` are attributes (CSS geometry does not apply to text), `RadialGradient`'s `cx`/`cy`/`r` are attributes (nor to a gradient) — while `Circle`'s `cx` stays CSS and transitions. `transform` is a prop on every shape and group. `Svg` takes `viewBox`/`preserveAspectRatio`/`width`/`height` as attributes (so no ÷4 layout `width` on it) and a `label` prop: no label means `aria-hidden`, a label means `role="img"` with that name. A paint server is a value, not an attribute: `fill="url(#sky)"`, `clipPath="url(#frame)"` — themed and hoverable like any other paint. A themed gradient stop is `<Stop stopColor="currentColor" color="amber-300" />`. `BaseSvg` is deprecated — it is `Svg` with a 24×24 preset.
21
+
22
+ **Icons** (`@box-kite/react/components/icon`, server-safe): `<Icon size={5} color="amber-500" label="Sunny"><Sun /></Icon>` — Box props on an icon somebody else drew, wrapping exactly one element from lucide, Tabler, react-icons, or a raw `<svg>`. It resolves the props to a class and puts that on the icon's own `<svg>`, so it knows no set's API: `size` is the ÷4 scale (`size={6}` is 24px, the default; an icon set's own `size` counts in pixels, so `size={20}` becomes `size={5}`) and it lands in the _class_, where a CSS declaration outranks the `width`/`height` attributes the set writes for itself. `strokeWidth` is likewise the ordinary Box prop, so it can change on hover or at a breakpoint. No `label` means `aria-hidden`, a `label` means `role="img"` — the same rule `Svg` follows. **Prefer `Svg` over wrapping one in `Icon`**: `Svg` takes these props directly. (Wrapping works — `Icon` asks the child which convention it follows and routes `role`/`aria-label` into `props` for a component of ours — but it is a layer nobody needs.) The hook underneath is public — `useClassNames(props)` from the main entry returns `{ className, styles }` for anything Box cannot render (a `motion.div`, a `NavLink`); render `styles` beside the element (it is defined in element mode only). **Beyond lucide**: Iconify's 300k+ icons in 200+ sets reach the same `<Icon>`, the choice being only when the icon becomes markup — paste one icon's SVG (no dependency, server-renders); `unplugin-icons` for a set at build time (`npm i -D unplugin-icons @iconify-json/<set> @svgr/core @svgr/plugin-jsx`, plugin with `{ compiler: 'jsx', jsx: 'react' }`, `/// <reference types="unplugin-icons/types/react" />` in a `.d.ts`, then `import SiGithub from '~icons/simple-icons/github'`); or `@iconify/react` when the name is data — it fetches in the browser, so it is a client component and the server sends no icon. Turbopack runs no unplugin: under Next.js 16 the build-time recipe needs `next build --webpack`.
23
+
24
+ **Charts** (`@box-kite/react/components/chart`, server-safe): `Sparkline`, `ProgressRing`, `Gauge`, `MiniDonut` — micro-primitives over the SVG components, **not a chart library** (no axes, no legends, no data transformations; theme Recharts for that). Each is an `Svg`, so `width`/`height` are the attributes, the paint is inherited by the shapes inside (default stroke `currentColor`, so `color="sky-500"` recolours one), and every prop, pseudo-class, breakpoint and theme works. `<Sparkline data={[4, 9, 6, 12]} variant="line|area|bar" min={0} max={20} />` — it fills its box (`preserveAspectRatio="none"` + `vectorEffect="non-scaling-stroke"`, so the line stays one width thick at any size), and `min`/`max` fix the axis so a column of rows is comparable. `<ProgressRing value={0.62} thickness={10} trackOpacity={0.2} />` and `<Gauge value={0.4} sweep={270} start={225} />` (degrees clockwise from twelve o'clock; `sweep={360}` is a ring) draw the value as a dash on the arc — a style prop, so it **eases between values with no animation code**; the fraction is rounded to half a percent, because a dash length lands in a class name. `<MiniDonut data={[5, 3, 2]} colors={['sky-500', 'var(--chart-2)']} />` gives each value its share of the circle. `children` is SVG drawn after the chart (an `SvgText` in the middle, a `Defs` with a gradient); `label` names it `role="img"`, and without one it is `aria-hidden` — leave a sparkline beside its own number unnamed, name one that _is_ the data. Cheap in a grid: the shape is the `d` attribute (no CSS at all, so 10,000 rows share every rule) while the paint is a class — and that paint is a style tree, `sparkline` > `line`/`area`/`bar`, `progressRing`/`gauge` > `track`/`arc`, `miniDonut` > `segment`, so one `Box.components()` override restyles every drawing in an app. A DataGrid cell renderer is a component — define it outside the render.
25
+
26
+ **Theming a chart library** — `<ChartContainer>` (same entry, server-safe): the bridge for Recharts and anything
27
+ else that takes a colour. It declares `--chart-1` … `--chart-6` in both themes plus one `--color-<series>` per
28
+ series, so the chart itself names no colour at all: `series={['revenue', 'cost']}` (the palette in order) or
29
+ `series={{ revenue: 'emerald-600' }}` (your own paint), then `<Line stroke="var(--color-revenue)" />` inside. A
30
+ series name becomes part of a custom-property name, so it has to be a CSS identifier — a dot-path `dataKey` is
31
+ skipped. Overriding a slot needs no prop of its own, because the container is a Box:
32
+ `vars={{ 'chart-1': 'teal-600' }}`, `theme={{ dark: { vars: { 'chart-1': 'teal-400' } } }}`, and any other name
33
+ the chart reads (`--chart-grid`, `--chart-label` for axes) is declared the same way. The names are the ones the
34
+ ecosystem already uses, so a chart lifted from shadcn's charts works unchanged. It adds no role and no ARIA, and the palette it declares is its own `chartContainer` style node — override that and every chart on the page follows.
35
+
36
+ **Custom properties**: `vars` — the one prop whose declaration _names_ come from its value.
37
+ `vars={{ 'color-revenue': 'sky-500', 'chart-gap': '4px' }}` declares `--color-revenue: var(--sky-500)` and
38
+ `--chart-gap: 4px` on the element, inherited by everything inside it — including markup this library never
39
+ rendered (a chart library, a third-party widget). A colour token resolves to the variable behind it; every other
40
+ value is written out as it stands. Names may carry a leading `--` or not. It is an ordinary prop, so it nests in
41
+ `theme`/`hover`/a breakpoint and lands in a **class** — two subtrees declaring the same variables share one rule,
42
+ and nothing needs a `<style>` tag or an `id` to scope it. A name that is not a CSS identifier, or a value
43
+ containing `;` or a brace, is skipped (that entry only, not the whole record).
44
+
45
+ **Escape hatch**: `css={{ mixBlendMode: 'multiply', WebkitLineClamp: 2 }}` — a style object for the properties with
46
+ no prop, compiled to a **class** through the same pipeline (shared, nests in `hover`/`md`/`theme`/`dataAttr`/`before`/
47
+ `startingStyle`/a keyframes step, renders on a server), never an inline style. camelCase names typed by csstype (a
48
+ misspelling is a compile error, `WebkitLineClamp` → `-webkit-line-clamp`); values are CSS written as they stand — a
49
+ number too, so `width: 100` is a type error while `zIndex: 3` is fine — and a colour token resolves like a `vars`
50
+ value (`outlineColor: 'sky-500'`). Sorted **last**, so on one element it wins the property a typed prop also names.
51
+ A value with `;` or a brace is dropped (that entry only). Prefer a prop, then `Box.extend()` for anything used
52
+ twice; `css` is the one-off.
53
+
54
+ **Animation & transitions**: every Box already transitions `all` over `--transitionTime` (0.25s), which is why a `hover` colour fades
55
+ unasked. `animation` takes a preset — `'spin'`/`'pulse'`/`'bounce'`/`'ping'`/`'none'`, keyframes included, durations riding
56
+ `--transitionTime` so `prefers-reduced-motion` stops them for free. Longhands (declared after it, so they win): `animationName`,
57
+ `animationDuration`/`animationDelay` (**milliseconds, no divider** — `animationDuration={1100}`), `animationIterationCount` (number or
58
+ `'infinite'`), `animationDirection`, `animationFillMode`, `animationPlayState`, `animationTimingFunction`. Name a duration in ms and you
59
+ own reduced motion: add `motionReduce={{ animationName: 'none' }}`. `transition` narrows what transitions to a group — `'colors'`,
60
+ `'opacity'`, `'shadow'`, `'transform'`, `'size'`, `'filter'` (or `'all'`/`'none'`) — with `transitionDuration`/`transitionDelay` in ms and
61
+ `transitionTimingFunction` taking the keywords plus computed curves (`'cubic-bezier(0.4, 0, 0.6, 1)'`, `'steps(4, end)'`,
62
+ `'linear(0, 0.5, 1)'`; a typo emits no rule). **Springs** are four sampled curves — `'spring'` (540ms), `'spring-gentle'` (660ms),
63
+ `'spring-bouncy'` (880ms, 20% overshoot), `'spring-snappy'` (420ms) — and a spring is a curve _and_ a settling time, so name it on both
64
+ props: `<Box transition="transform" transitionTimingFunction="spring-bouncy" transitionDuration="spring-bouncy" hover={{ scale: 1.1 }} />`.
65
+ The durations ride `--transitionTime`, so reduced motion stops a spring too; `Box.spring({ stiffness, damping, mass, velocity })` returns
66
+ `{ easing, duration }` for one of your own. The curve is fixed once sampled (an interrupted transition restarts rather than carrying its
67
+ velocity — that is framer-motion's job), and every `linear()` is written with an `ease-out` under it for the ~13% of browsers without it. **`Box.keyframes()`** registers sequences whose steps are Box props —
68
+ `Box.keyframes({ 'slide-in': { from: { opacity: 0, translateY: 3 }, to: { opacity: 1, translateY: 0 } } })`, stops keyed `'from'`/`'to'`/`'50%'`
69
+ — written into the stylesheet the first time a rule names one, and exactly once, so an unused sequence costs nothing; it reaches
70
+ `getStyles()` and element mode, so a Server Component animates with no client JS. **Transforms compose**: `translateX`/`translateY` (÷4,
71
+ fractions, percentages) both feed one `translate` and still transition _and_ animate (the base stylesheet registers both axes with
72
+ `@property`, or a keyframe moving them would jump), `rotate={45}` (degrees) and `scale={1.05}` (unitless) are their own
73
+ properties — only `flip` and `scale` collide, both writing `scale`. **`startingStyle` is an entrance with no JavaScript**: a nested block of
74
+ plain props (`startingStyle={{ opacity: 0, translateY: 2 }}`) saying what they start from the first time the element is styled — mounted, or
75
+ shown from `display: none`. It compiles to `@starting-style`, nests inside a breakpoint/pseudo-class/theme rather than around one
76
+ (`md: { startingStyle: … }`), and a browser without the at-rule shows the element finished. Its declarations are emitted `!important` — a
77
+ starting rule at `.x` would otherwise lose on specificity to the value it starts from at `.dark .x` or `.x[data-state="open"]` and nothing
78
+ would transition at all; the importance reaches nothing but the before-change style. `Tooltip` and the `Dropdown` popup already carry
79
+ one. For the way back out React unmounts too fast to animate, so either hide instead — `transitionBehavior="allow-discrete"` lets `display`
80
+ transition, flipping it at the _end_, so `<Box display={open ? 'block' : 'none'} opacity={open ? 1 : 0} transitionBehavior="allow-discrete"
81
+ startingStyle={{ opacity: 0 }} />` animates both directions — or hold the node with **`<Presence present>`**
82
+ (`components/presence`): a render prop handed `{ present, state, ref, props }` that keeps rendering its child with `present: false` until the
83
+ child's own CSS says the transition is over, then lets React remove it. `ref` goes on the element carrying the transition (its computed
84
+ `transition-duration`/`animation-duration` _is_ the wait — not a `transitionend` listener, which fires once per property), `props` is
85
+ `{ 'data-state': 'open' | 'closed' }`. Under `prefers-reduced-motion` the measured wait is `0` and the node leaves in the same commit.
86
+ `Tooltip`, the `Dropdown` popup and the DataGrid column menu are built on it. `interpolateSize="allow-keywords"` on a container is what makes
87
+ `height: auto` animate inside it (inherited; Chromium-only, elsewhere it snaps). `Box.configure({ transition: 'colors' | false })` changes
88
+ what the base class transitions, before the first render.
89
+
90
+ **Gradients**: `bgGradient` — a gradient as a _value_, written as a record, so its stops are palette tokens and it is
91
+ themed, takes the opacity modifier and shares one class. The key names the kind and carries its geometry:
92
+ `{ linear: 'r', colors: ['blue-500', 'pink-500'] }` (a direction `t`/`tr`/`r`/`br`/`b`/`bl`/`l`/`tl`, or a number of
93
+ degrees — `{ linear: 135, … }`), `{ radial: 'circle' | 'ellipse' | true, at: 'top left', … }`, `{ conic: 45 | true, … }`.
94
+ Exactly one kind; `at` centres a radial or conic gradient, never a linear one. A stop is any colour value —
95
+ `'blue-500/40'`, `'transparent'`, `'var(--chart-1)'` — or a `[colour, position]` pair (`['sky-500', '20%']`), **two
96
+ minimum**. `interpolate` names the space: `'srgb'`, `'hsl'`, `'oklab'`, `'oklch'`, `'hsl-longer'`, `'oklch-longer'` —
97
+ `'oklch'` is what keeps two stops out of the grey middle sRGB drags them through, and a `-longer` hue turns two stops
98
+ into a spectrum. The record is judged **whole**: one unknown stop, one misspelt key (`interpolat`), two kinds at once,
99
+ and the value emits no rule and no class name. It writes `background-image`, so `bgGradient` and `bgImage` are the
100
+ same property — use one.
101
+
102
+ **Shadows stack, four at a time**: `box-shadow` is one property, so each layer sets its own custom property and all
103
+ four write the same composed declaration — a ring and an elevation coexist instead of the last rule winning.
104
+ `shadow`: `'xxs'`/`'xs'`/`'sm'`/`'md'`/`'lg'`/`'xl'`/`'xxl'` on Tailwind's elevation scale, plus the three original presets
105
+ `'small'`/`'medium'`/`'large'` (which carry their own colour), plus `'none'`. `insetShadow`: `'xxs'`/`'xs'`/`'sm'`, drawn
106
+ inside the border box. `ring`/`insetRing` are a **width in px**, not a scale — `ring={2}` — and unlike `outline` a ring
107
+ joins the stack, follows `borderRadius` and costs no layout. Each layer takes a colour of its own: `shadowColor`,
108
+ `insetShadowColor`, `ringColor` (`currentColor` by default), `insetRingColor` — every colour value, modifier included
109
+ (`shadowColor="blue-500/40"`). A colour prop **shows nothing on its own**, the way `borderColor` does with no border
110
+ width. `'none'` — or `0` on a ring — clears just that layer. `textShadow` (`'xxs'`…`'lg'`, `'none'`) with
111
+ `textShadowColor` is the text-side pair, and `transition="shadow"` covers `box-shadow` and `text-shadow` both.
112
+
113
+ **Filters stack, nine at a time**, for the reason the shadows do: `filter` is one property whose value is a list, so
114
+ each function sets its own custom property and all nine write the same composed declaration. `blur`, `brightness`,
115
+ `contrast`, `grayscale`, `hueRotate`, `invert`, `saturate`, `sepia`, `dropShadow`. **A number is the function's own
116
+ unit**: a percentage for the six that take one (`brightness={110}`, `grayscale={100}`), degrees for `hueRotate`, px for
117
+ `blur` — which also takes Tailwind's scale, `'xs'` (4px) `'sm'` (8px) `'md'` (12px) `'lg'` (16px) `'xl'` (24px)
118
+ `'xxl'` (40px) `'xxxl'` (64px). `dropShadow` is `'xs'`…`'xxl'` with `dropShadowColor`, and it is cast by the **shape** —
119
+ an SVG path, the opaque part of a transparent PNG — where `shadow` draws a rectangle round the box. `'none'` clears
120
+ just that function. The nine `backdrop*` props (`backdropBlur`, `backdropBrightness`, `backdropContrast`,
121
+ `backdropGrayscale`, `backdropHueRotate`, `backdropInvert`, `backdropOpacity`, `backdropSaturate`, `backdropSepia`) are
122
+ the same functions on `backdrop-filter`, which is the glassmorphism half; there is no `backdropDropShadow` and no
123
+ `opacity` filter, because neither means anything on the other side. The older `backdropFilter` prop writes that
124
+ property directly — it and the nine are the same declaration, so use one. `transition="filter"` covers both properties.
125
+
126
+ **Masks and clipping**: `maskImage` takes the same record `bgGradient` does and reads its alpha channel — where the
127
+ gradient is transparent the element is not painted, so `maskImage={{ linear: 'b', colors: ['black', 'transparent'] }}`
128
+ is the whole edge-fade recipe. It also takes `'url(#id)'`, `'var(--name)'` and `'none'`; one mask, not a stack.
129
+ `bgClip` is `'border'`/`'padding'`/`'content'`/`'text'`, and `bgClip="text"` with `color="transparent"` is how a
130
+ gradient becomes lettering — the pairing is deliberately not automatic.
131
+
132
+ **Effects**: `opacity`, `cursor`, `pointerEvents`, `userSelect`, `overflow`
133
+
134
+ **Native controls**: `accentColor` tints the parts of a checkbox, radio, range or progress bar the page does not draw — the palette, its opacity modifier and the system colours, and it inherits, so a form sets it once instead of every control giving up `appearance`. `caretColor` is the text caret. `colorScheme` (`'light'`, `'dark'`, `'light dark'`, `'only light'`, `'only dark'`, `'normal'`) is what native UI follows: scrollbars, form controls, the spellcheck underline — a dark theme that leaves it alone gets light scrollbars over a dark page. `fieldSizing="content"` is a control that grows with what is typed into it, with nothing measuring anything. `scrollbarGutter="stable"` (or `'stable both-edges'`) reserves the scrollbar's space before there is one, so a panel does not shift when its content overflows; `scrollbarWidth`/`scrollbarColor` style the bar itself. `willChange` (`'transform'`, `'opacity'`, `'filter'`, `'scroll-position'`, `'contents'`) promotes an element before it animates rather than during — a hint with a real cost, so it goes on the few elements that move, never on a list
135
+
136
+ ## Pseudo-Classes, Breakpoints, State Variants & Container Queries
137
+
138
+ ```tsx
139
+ <Box bgColor="blue-500" hover={{ bgColor: 'blue-600' }} disabled={{ opacity: 0.5 }} />
140
+ // Pseudo: hover, focus (:focus-within), focusVisible, hasFocus, active, valid, invalid, optional,
141
+ // disabled, checked, indeterminate, required, selected, hasChecked, hasRequired, hasDisabled,
142
+ // visited (colour props only — the privacy rule), target, open (an [open] element, a popover or a
143
+ // <select>'s picker), placeholderShown, autofill, inRange, outOfRange, inert (the subtree too),
144
+ // rtl / ltr (:dir() — this element's own resolved direction, so ltr matches with no dir set anywhere)
145
+ // Responsive (mobile-first): sm(640) md(768) lg(1024) xl(1280) xxl(1536)
146
+ <Box p={2} md={{ p: 4, hover: { bgColor: 'gray-200' } }} />
147
+ // Device + a11y media keys, same shape as a breakpoint, and they beat every breakpoint in the cascade:
148
+ // pointerCoarse / pointerFine (pointer: coarse|fine — a finger needs a bigger target, and never
149
+ // hovers; ranked below the three preferences), motionReduce (prefers-reduced-motion: reduce),
150
+ // forcedColors (forced-colors: active), contrastMore (prefers-contrast: more).
151
+ // Not nestable in a breakpoint or in each other.
152
+ // The pre-built components come with one dir and no configuration: useRovingFocus reads the element's
153
+ // resolved direction when a sideways arrow arrives, so ArrowLeft is the *next* item in a right-to-left
154
+ // list or grid (APG's rule; Tab/Home/End never flip); a DataGrid column pins to 'START'/'END' of the
155
+ // inline axis; and Overlay stays in the subtree it was declared in, so the layer inherits its direction
156
+ // (on the portal fallback it measures one and writes it on). Physical on purpose: Overlay's coordinates.
157
+ // Reduced motion is already the default — the preference sets --transitionTime to 0s, so every
158
+ // Box stops animating. Declare motionReduce only to replace a movement or keep a safe one.
159
+ <Box transitionDuration={150} motionReduce={{ transition: 'none' }} forcedColors={{ b: 1 }} />
160
+ // Forced colours keep only the system colours, so they are values on every colour prop: Canvas,
161
+ // CanvasText, ButtonFace, ButtonText, Highlight, HighlightText, GrayText, LinkText (keywords, not tokens).
162
+ <Box forcedColors={{ bgColor: 'ButtonFace', color: 'ButtonText' }} />
163
+ // State variants — a selector fragment on the element's own class, so a state your code sets is CSS,
164
+ // not a ternary. Record key = the selector; a key the grammar rejects drops its block, like a bad value.
165
+ // dataAttr {'state=open'} → [data-state="open"], {'loading'} → [data-loading]
166
+ // ariaAttr {'selected'} → [aria-selected="true"] (bare key means ="true"), {'sort=ascending'}
167
+ // has {':checked'} → :has(:checked) · not {hover} / {'data-loading'} → :not(:hover) / :not([data-loading])
168
+ // nth {first|last|only|odd|even|'3'|'2n+1'|'-n+3'|'last 2'} → :first-child … :nth-last-child(2)
169
+ // The attribute itself still goes in `props`. Everything else nests around them, either direction.
170
+ <Box props={{ 'data-state': state }} dataAttr={{ 'state=busy': { bgColor: 'amber-500' } }}
171
+ md={{ hover: { ariaAttr: { selected: { color: 'white' } } } }} />
172
+ <Box bb={1} nth={{ odd: { bgColor: 'slate-500/10' }, 'last 1': { bb: 0 } }} />
173
+ // Somebody else's state: group = an ancestor, peer = a preceding sibling. The key is a state, on the
174
+ // default class (group/peer, Tailwind's names) or on one you name — 'card/hover', 'row/data-state=open'.
175
+ // The ancestor carries the class through `className`; the element's own states stay on the element.
176
+ // hoverGroup/focusGroup/activeGroup/disabledGroup/selectedGroup are the older spelling of the same
177
+ // rule (hoverGroup={{ card }} === group={{ 'card/hover' }}) and share its class. A name that is not a
178
+ // CSS identifier drops the block; a group means nothing in addGlobalStyles.
179
+ <Flex className="card">
180
+ <Box opacity={0} group={{ 'card/hover': { opacity: 1 } }} />
181
+ </Flex>
182
+ <Checkbox className="agree" label={<Box peer={{ 'agree/checked': { color: 'emerald-500' } }}>I agree</Box>} />
183
+ // Pseudo-elements — CSS allows ONE per selector, so it is a slot: a second nested one is a type error,
184
+ // and it lands last on the element's own compound (on the target, not on a group's ancestor).
185
+ // before, after (both come with content: '' — a generated element with none renders nothing),
186
+ // placeholder, selection, marker, firstLine, firstLetter, backdrop, fileButton
187
+ // marker/selection also name descendants, so they go on the <Ul> or the <P>
188
+ // content: 'empty' → '' · 'New' → "New" (text is quoted for you) · 'attr(data-x)'/'counter(s)'/
189
+ // 'url(...)'/'var(--x)'/'"Step " counter(step)' written out as CSS · 'none' turns it off.
190
+ // A value that cannot be quoted or parsed produces no rule and no class name.
191
+ <Box position="relative" before={{ position: 'absolute', inset: 0, bgColor: 'indigo-500' }}
192
+ hover={{ before: { width: 'fit' } }} after={{ content: 'attr(data-count)' }} />
193
+ // On Textbox/Textarea the name means both: a string is the attribute, an object the styles; both at
194
+ // once puts the text in `props`. `placeholderStyles` is the old name for `placeholder` and still works.
195
+ <Textbox props={{ placeholder: 'Search…' }} placeholder={{ color: 'slate-400' }} />
196
+ // Container queries — the same question, asked of the element's own container. `container` makes one
197
+ // (true → container-type: inline-size; a name → container: <name> / inline-size), containerName and
198
+ // containerType ('inline-size' | 'size' | 'normal') are the longhands. `cq` queries the NEAREST one,
199
+ // keyed by size: xs(20rem) sm(24) md(28) lg(32) xl(36) xxl(42), each with a complement maxXs…maxXxl
200
+ // (`not (min-width: …)`, so md and maxMd never both match), and `'name/md'` for a named container.
201
+ // Ranked after every breakpoint and before every preference; one at-rule block per rule, so cq and a
202
+ // breakpoint do not nest in each other. A name that is not a CSS ident drops its block.
203
+ <Flex container="card">
204
+ <Box cq={{ sm: { d: 'row' }, maxSm: { display: 'none' }, 'card/xl': { p: 6 } }} />
205
+ </Flex>
206
+ ```
207
+
208
+ ## Theme System
209
+
210
+ ```tsx
211
+ <Box.Theme> {/* auto-detect system */}
212
+ <Box.Theme theme="dark" use="global"> {/* explicit + applies to <html> */}
213
+ <Box.Theme storageKey="app-theme"> {/* persists to localStorage */}
214
+ // Hook: const [theme, setTheme] = Box.useTheme();
215
+ // setTheme('dark') | setTheme(null) resets to auto
216
+ <Box bgColor="white" theme={{ dark: { bgColor: 'gray-900', hover: { bgColor: 'gray-700' } } }} />
217
+ // App-wide styles on <html> (only with use="global"). Use for inheritable CSS (scrollbarColor, fontFamily, color):
218
+ <Box.Theme use="global" globalStyles={{ scrollbarColor: ['violet-500','transparent'],
219
+ theme: { dark: { scrollbarColor: ['violet-700','gray-900'] } } }} />
220
+ ```
221
+
222
+ **Props**: `theme?` (string), `use?` (`'global'`|`'local'`), `storageKey?`, `globalStyles?` (BoxStyleProps — `<html>`, requires `use="global"`). Sets `data-theme` attr + class.