@box-kite/react 2.3.1 → 2.4.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 (118) hide show
  1. package/.claude/rules/box-kite-rules.md +58 -94
  2. package/.claude/skills/box-kite/SKILL.md +69 -104
  3. package/.claude/skills/box-kite/references/components.md +8 -1
  4. package/.claude/skills/box-kite/references/patterns.md +1 -1
  5. package/.claude/skills/box-kite/references/rules.md +115 -0
  6. package/.claude/skills/box-kite/references/styling.md +5 -3
  7. package/.cursor/rules/box-kite.mdc +60 -96
  8. package/AGENTS.md +61 -97
  9. package/BOX_AI_CONTEXT.md +111 -40
  10. package/BOX_KITE_AI_CONTEXT.md +111 -40
  11. package/README.md +200 -721
  12. package/box.cjs +1 -1
  13. package/box.d.ts +5 -0
  14. package/box.mjs +5 -5
  15. package/catalog.cjs +1 -1
  16. package/catalog.mjs +53 -18
  17. package/client.cjs +1 -1
  18. package/client.mjs +85 -85
  19. package/components/accordion.cjs +1 -1
  20. package/components/accordion.mjs +1 -1
  21. package/components/button.cjs +1 -1
  22. package/components/button.mjs +1 -1
  23. package/components/calendar.cjs +1 -1
  24. package/components/calendar.mjs +1 -1
  25. package/components/chart.cjs +1 -1
  26. package/components/chart.mjs +3 -3
  27. package/components/checkbox.cjs +1 -1
  28. package/components/checkbox.mjs +1 -1
  29. package/components/combobox/comboboxListbox.d.ts +2 -1
  30. package/components/combobox.cjs +1 -1
  31. package/components/combobox.d.ts +1 -1
  32. package/components/combobox.mjs +200 -189
  33. package/components/dashboard.cjs +1 -1
  34. package/components/dashboard.mjs +1 -1
  35. package/components/dataGrid/contracts/dataGridContract.d.ts +2 -1
  36. package/components/dataGrid/useListLayout.d.ts +1 -1
  37. package/components/dataGrid.cjs +3 -3
  38. package/components/dataGrid.mjs +453 -453
  39. package/components/dateField.cjs +1 -1
  40. package/components/dateField.mjs +16 -16
  41. package/components/datePicker.cjs +1 -1
  42. package/components/datePicker.mjs +1 -1
  43. package/components/dateTime.cjs +1 -1
  44. package/components/dateTime.mjs +1 -1
  45. package/components/dialog.cjs +1 -1
  46. package/components/dialog.mjs +1 -1
  47. package/components/flex.cjs +1 -1
  48. package/components/flex.mjs +1 -1
  49. package/components/flow.cjs +1 -0
  50. package/components/flow.d.ts +56 -0
  51. package/components/flow.mjs +98 -0
  52. package/components/form.cjs +1 -1
  53. package/components/form.mjs +1 -1
  54. package/components/grid.cjs +1 -1
  55. package/components/grid.mjs +1 -1
  56. package/components/icon.cjs +1 -1
  57. package/components/icon.mjs +1 -1
  58. package/components/list.cjs +1 -1
  59. package/components/list.mjs +1 -1
  60. package/components/menu.cjs +1 -1
  61. package/components/menu.mjs +1 -1
  62. package/components/popover.cjs +1 -1
  63. package/components/popover.mjs +1 -1
  64. package/components/radioButton.cjs +1 -1
  65. package/components/radioButton.mjs +1 -1
  66. package/components/radioGroup.cjs +1 -1
  67. package/components/radioGroup.mjs +2 -1
  68. package/components/semantics.cjs +1 -1
  69. package/components/semantics.d.ts +32 -30
  70. package/components/semantics.mjs +3 -3
  71. package/components/skeleton.cjs +1 -1
  72. package/components/skeleton.d.ts +1 -1
  73. package/components/skeleton.mjs +3 -1
  74. package/components/slider.cjs +1 -1
  75. package/components/slider.mjs +1 -1
  76. package/components/sortableList.cjs +1 -1
  77. package/components/sortableList.mjs +1 -1
  78. package/components/svg.cjs +1 -1
  79. package/components/svg.mjs +14 -14
  80. package/components/swipeItem.cjs +1 -1
  81. package/components/swipeItem.mjs +1 -1
  82. package/components/tabs.cjs +1 -1
  83. package/components/tabs.mjs +1 -1
  84. package/components/textarea.cjs +1 -1
  85. package/components/textarea.d.ts +1 -1
  86. package/components/textarea.mjs +1 -1
  87. package/components/textbox.cjs +1 -1
  88. package/components/textbox.d.ts +1 -1
  89. package/components/textbox.mjs +1 -1
  90. package/components/timeZoneSelect.d.ts +1 -1
  91. package/components/toaster.cjs +1 -1
  92. package/components/toaster.mjs +14 -15
  93. package/components/tooltip.cjs +1 -1
  94. package/components/tooltip.mjs +1 -1
  95. package/components/virtualList.cjs +1 -1
  96. package/components/virtualList.mjs +1 -1
  97. package/components/visuallyHidden.cjs +1 -1
  98. package/components/visuallyHidden.mjs +1 -1
  99. package/docs/a11y.md +1 -2
  100. package/docs/components.md +1 -0
  101. package/docs/index.md +1 -1
  102. package/docs/props.md +1 -1
  103. package/effects.cjs +1 -1
  104. package/effects.mjs +2 -2
  105. package/identity.cjs +1 -1
  106. package/identity.mjs +5 -7
  107. package/package.json +50 -36
  108. package/react/boxClassNames.d.ts +2 -2
  109. package/react/effects.d.ts +5 -6
  110. package/react/identity/useIdentifier.d.ts +0 -3
  111. package/react-shared.cjs +1 -1
  112. package/react-shared.mjs +42 -3
  113. package/rsc.cjs +1 -1
  114. package/rsc.d.ts +1 -0
  115. package/rsc.mjs +2 -2
  116. package/utils/catalog/catalogContracts.d.ts +0 -10
  117. package/utils/flow/flowPalette.d.ts +17 -0
  118. package/utils/flow/flowUtils.d.ts +47 -0
package/BOX_AI_CONTEXT.md CHANGED
@@ -660,8 +660,15 @@ const wobble = Box.spring({ stiffness: 120, damping: 8 });
660
660
  the same commit, with nothing to configure. `Tooltip`, the `Dropdown` popup and the DataGrid column
661
661
  menu are all built on it, and each expresses its exit as a `closed` variant in its component styles — plus `up`/`closedUp` on `dropdown.items`, since a popup that opened upward has to collapse upward.
662
662
  - **`Box.configure({ transition })`** changes what the base class transitions for the whole engine:
663
- a group name, or `false` to declare nothing at all and leave transitions entirely to the props.
663
+ a group name, a comma list of groups and property names (`'colors, opacity'`), or `false` to declare
664
+ nothing at all and leave transitions entirely to the props. Anything else transitions `all` and warns.
664
665
  Call it before the first render.
666
+ - **An inline phrasing element is `inline-block`**, where every other Box is `block`: `a`, `span`, `label`, `time`, `mark`, `abbr`, `code`, `kbd`, `samp`, `var`, `em`,
667
+ `strong`, `small`, `b`, `i`, `u`, `s`, `sub`, `sup`, `q`, `cite`, `dfn`, `data`, `output`, `bdi`, `bdo`. It is
668
+ selected on the rendered element at the base class's specificity, so `<Link>`, `<Box tag="a">` and a
669
+ `useClassNames` class on a router's `<a>` all stay in their sentence, margin and padding still apply, and a
670
+ `display` prop or component still wins. Media (`img`, `svg`, `video`, `canvas`, `iframe`) stays block. An `inline-block` never breaks across lines, so a
671
+ prose link or `<Mark>` that has to wrap with the text sets `display="inline"`.
665
672
 
666
673
  ---
667
674
 
@@ -1263,7 +1270,7 @@ import Textbox from '@box-kite/react/components/textbox';
1263
1270
  <Flex d="column" gap={2} md={{ d: 'row', gap: 4 }}>{children}</Flex>
1264
1271
 
1265
1272
  // Grid
1266
- <Grid gridCols={3} gap={4}>{items.map(i => <Box key={i.id}>{i.content}</Box>)}</Grid>
1273
+ <Grid gridTemplateColumns={3} gap={4}>{items.map(i => <Box key={i.id}>{i.content}</Box>)}</Grid>
1267
1274
 
1268
1275
  // Card
1269
1276
  <Box p={4} bgColor="white" borderRadius={8} shadow="medium">{content}</Box>
@@ -1320,6 +1327,11 @@ undefined and rendering it costs nothing, so write the line either way. Pass `{
1320
1327
  second argument for an element inside an `<svg>`. `<Icon>` is this hook plus that flag — reach for
1321
1328
  `Icon` for an icon and this only for everything else.
1322
1329
 
1330
+ The class carries the base reset, which is `display: block` except on an `<a>`, `<span>`, `<label>` and the
1331
+ other phrasing elements, which are `inline-block`: a `NavLink` styled this way stays in its sentence.
1332
+ `{ base: false }` leaves the reset out — for a library's own root (`<ReactFlow>`, `<MiniMap>`, a `BaseEdge`)
1333
+ whose margin, display and transitions are its own.
1334
+
1323
1335
  ### Group and Peer
1324
1336
 
1325
1337
  ```tsx
@@ -1456,6 +1468,19 @@ Full reference: `docs/a11y-primitives.md`.
1456
1468
 
1457
1469
  ---
1458
1470
 
1471
+ ## Field ids, external labels and the invalid state
1472
+
1473
+ - **A top-level `id` lands on the control, once.** `<Combobox id="owner">` puts it on the `<input role="combobox">`
1474
+ and nowhere else, as `Textbox`, `Textarea`, `Select` (its button), `Checkbox`, `Switch`, `RadioButton` and
1475
+ `TimeZoneSelect` do, so `<label htmlFor="owner">` names the control. `DateField`, `DatePicker`, `Slider` and
1476
+ `RadioGroup` are groups no `<label>` can name: the `id` goes on the group, and `labelledBy` points it at an
1477
+ external label.
1478
+ - **`aria-invalid` draws the error border on every field.** `props={{ 'aria-invalid': true }}` turns the frame of a
1479
+ `Textbox`, `Textarea`, `Select` or `Combobox` `red-500`, through hover and focus, in both themes. A combobox's
1480
+ attribute is on its input and its border on the shell, so the shell matches `:has([aria-invalid="true"])` —
1481
+ restyle it as `combobox: { styles: { has: { '[aria-invalid="true"]': { … } } } }`, the others with
1482
+ `ariaAttr: { invalid: { … } }`. `DateField` and `DatePicker` take an `invalid` prop that does both.
1483
+
1459
1484
  ## Sizes: compact, normal, large
1460
1485
 
1461
1486
  Every built-in component is drawn at one of three sizes, and a size is two CSS variables —
@@ -1521,16 +1546,16 @@ the association needs no `htmlFor`/`id` pair and the whole row is a click target
1521
1546
  nothing wraps the input — the markup is what it always was. `labelProps` styles that `<label>`;
1522
1547
  every other Box prop styles the control itself.
1523
1548
 
1524
- | Component | Prop | What it does |
1525
- | ------------ | ------------------------ | -------------------------------------------------------------------------------------------- |
1526
- | all three | `label` / `labelProps` | The wrapping `<label>` and its styles. |
1527
- | `Checkbox` | `indeterminate` | Sets the DOM property **and** `aria-checked="mixed"`, which is what is announced. |
1528
- | `Switch` | — | `role="switch"` over the same input; Space **and** Enter toggle, Enter does not submit. |
1529
- | `RadioGroup` | `label` | `role="radiogroup"` named by it — a set of radios with nothing over it is a flat list. |
1530
- | `RadioGroup` | `name` | The field every item submits under. Generated when left out. |
1531
- | `RadioGroup` | `value` / `defaultValue` | The selected item's value (a string — the platform's own model for a radio). |
1532
- | `RadioGroup` | `onValueChange` | `(value, { reason, event })` — `click` or `keyboard`. `onChange` is the deprecated spelling. |
1533
- | `RadioGroup` | `orientation` | `vertical` (default) or `horizontal`. Both arrow pairs navigate either way. |
1549
+ | Component | Prop | What it does |
1550
+ | ------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
1551
+ | all three | `label` / `labelProps` | The wrapping `<label>` and its styles. |
1552
+ | `Checkbox` | `indeterminate` | Sets the DOM property **and** `aria-checked="mixed"`, which is what is announced. |
1553
+ | `Switch` | — | `role="switch"` over the same input; Space **and** Enter toggle, Enter does not submit. |
1554
+ | `RadioGroup` | `label` | `role="radiogroup"` named by it — a set of radios with nothing over it is a flat list. |
1555
+ | `RadioGroup` | `name` | The field every item submits under. Generated when left out. |
1556
+ | `RadioGroup` | `value` / `defaultValue` | The selected item's value (a string — the platform's own model for a radio). |
1557
+ | `RadioGroup` | `onValueChange` | `(value, { reason, event })` — `click` or `keyboard`. `onChange` is the deprecated spelling. |
1558
+ | `RadioGroup` | `orientation` | `vertical` (default) or `horizontal`, which wraps and keeps the label on a line of its own above the radios. Both arrow pairs navigate either way. |
1534
1559
 
1535
1560
  Arrow keys move **and select** inside a group, wrapping at both ends and skipping disabled items;
1536
1561
  Tab enters the group once and leaves it once, because a native radio set is already a single tab
@@ -2660,6 +2685,51 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
2660
2685
  <DataGrid data={rows} def={{ rowKey: 'id', columns: [{ key: 'trend', header: 'Last 12 months', width: 180, Cell: TrendCell }] }} />;
2661
2686
  ```
2662
2687
 
2688
+ ### Theming a React Flow canvas — `FlowContainer`
2689
+
2690
+ `ChartContainer`'s move for diagrams. `@box-kite/react/components/flow` (server-safe, imports nothing from xyflow) is a Box
2691
+ that declares React Flow's `--xy-*` variables from the palette in both themes, so the canvas — background, pattern, edges,
2692
+ handles, built-in nodes, minimap, controls, selection box, edge labels — names no colour and follows the page:
2693
+
2694
+ ```tsx
2695
+ import { Background, Controls, MiniMap, ReactFlow } from '@xyflow/react';
2696
+ import '@xyflow/react/dist/style.css';
2697
+ import { FlowContainer, flowColorMode } from '@box-kite/react/components/flow';
2698
+
2699
+ function Canvas() {
2700
+ const [theme] = Box.useTheme();
2701
+
2702
+ return (
2703
+ <FlowContainer height={120}>
2704
+ <ReactFlow nodes={nodes} edges={edges} colorMode={flowColorMode(theme)} fitView>
2705
+ <Background />
2706
+ <Controls />
2707
+ <MiniMap />
2708
+ </ReactFlow>
2709
+ </FlowContainer>
2710
+ );
2711
+ }
2712
+ ```
2713
+
2714
+ | Prop | What it is |
2715
+ | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
2716
+ | `tokens` | Repaints a role: `{ selection: 'emerald-500' }` for both themes, `{ surface: { light: 'base-50', dark: 'base-950' } }` per theme |
2717
+ | `motion` | `'static'` zeroes every Box transition inside the canvas, for nodes moved or resized every frame |
2718
+
2719
+ - **Eight roles**, each a custom property a custom node can read: `--flow-surface`, `--flow-panel`, `--flow-text`, `--flow-border`,
2720
+ `--flow-grid`, `--flow-edge`, `--flow-selection`, `--flow-handle`. Defaults are `base`/`accent`, so `Box.palette()` reaches them.
2721
+ - **The defaults are the `flowContainer` style node**, so one override repaints every canvas in an app:
2722
+ `Box.components({ flowContainer: { styles: { vars: { 'flow-selection': 'emerald-500' } } } })`; `tokens` is the per-canvas answer.
2723
+ - **One `--xy-*` variable is `vars`** (`vars={{ 'xy-edge-stroke-width': '2' }}`); a prop on `<MiniMap>`/`<Background>` still wins.
2724
+ - **`colorMode={flowColorMode(theme)}`** keeps React Flow's own `.dark` class in step with the page. A canvas in another theme sits
2725
+ in `<Box.Theme use="local">`; never `colorMode="light"` under a dark page — `.light` is no theme boundary, so the two mix.
2726
+ - **A custom node is invisible to the keyboard** — xyflow removes every node's focus outline. Spread `flowNodeStyles` on its
2727
+ root (an outline while xyflow's wrapper is `:focus-visible`) and `flowNodeSelectedStyles` when its `selected` prop is true;
2728
+ both are outlines in the `selection` role, so they survive forced colours. A custom edge on `BaseEdge` takes `flowEdgeStyles`
2729
+ through `useClassNames(flowEdgeStyles, { svg: true, base: false })`.
2730
+ - Colour props refuse `var()`, so a custom node reads a role through `css`:
2731
+ `css={{ backgroundColor: 'var(--flow-panel)', borderColor: 'var(--flow-border)', color: 'var(--flow-text)' }}`.
2732
+
2663
2733
  ---
2664
2734
 
2665
2735
  ## Key Reminders for AI Assistants
@@ -2677,7 +2747,7 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
2677
2747
  11. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
2678
2748
  12. **Box is memoized** with `React.memo`, and so are `Flex`, `H1`, `Button` and the rest. A component of your own that is a Box with defaults calls `useBoxElement({ p: 4, ...props }, ref)` from its own `forwardRef` rather than returning `<Box>` — one fiber instead of two
2679
2749
  13. **`style` is top-level only** — never inside breakpoints (`sm={{ style: ... }}`), pseudo-classes, or theme objects. `css` is the one that nests: `sm={{ css: { objectPosition: 'top' } }}` is a rule
2680
- 14. **A CSS variable is a prop**: `vars={{ 'color-x': 'sky-500' }}` declares `--color-x` for everything inside — and a third-party chart goes in `<ChartContainer series={['revenue']}>` so it names no colour at all
2750
+ 14. **A CSS variable is a prop**: `vars={{ 'color-x': 'sky-500' }}` declares `--color-x` for everything inside — and a third-party chart goes in `<ChartContainer series={['revenue']}>` so it names no colour at all, and a React Flow canvas goes in `<FlowContainer>` with `colorMode={flowColorMode(theme)}`
2681
2751
  15. **All sizing props use divider 4** — `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight` are NOT direct pixels. `height={10}` = 2.5rem = 40px
2682
2752
  16. **A state your own code sets is a nested prop too**: `dataAttr={{ 'state=open': { … } }}` → `[data-state="open"]`, `ariaAttr={{ selected: { … } }}` → `[aria-selected="true"]`, `has={{ ':checked': { … } }}`, `not={{ hover: { … } }}`, `nth={{ odd: { … } }}`. The attribute itself still goes in `props`
2683
2753
  17. **Somebody else's state is `group`/`peer`**: `group={{ 'card/hover': { … } }}` → `.card:hover .x`, `peer={{ 'agree/checked': { … } }}` → `.agree:checked ~ .x`. The ancestor or sibling carries the class via `className`; `hoverGroup` and its four siblings are the older spelling
@@ -2790,27 +2860,27 @@ named ones carry the reason.
2790
2860
 
2791
2861
  ### ColumnType
2792
2862
 
2793
- | Prop | Type | Default | Description |
2794
- | ------------------------ | ------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
2795
- | `key` | `Key` | required | Column identifier (maps to TRow property) |
2796
- | `header` | `string` | — | Header text |
2797
- | `width` | `number` | `200` | Base width in px |
2798
- | `align` | `'start' \| 'end' \| 'center' \| 'left' \| 'right'` | `'left'` | Cell alignment. `start`/`end` follow the reading order; `left`/`right` stay on the screen side they name |
2799
- | `pin` | `'START' \| 'END'` (also `'LEFT'`/`'RIGHT'`) | — | Pin to an edge of the **inline axis** (sticky on scroll), so it holds under either `dir`. `LEFT`/`RIGHT` are the older spelling of the same two |
2800
- | `columns` | `ColumnType[]` | — | Nested columns (grouped header) |
2801
- | `Cell` | `({ cell }) => ReactNode` | — | Custom renderer. `cell`: `{ value, row, column, grid }`. It redraws when its row changes or the grid does — not when another row is selected |
2802
- | `sortable` / `resizable` | `boolean` | inherits | Override grid-level setting |
2803
- | `flexible` | `boolean` | `true` | Participate in flex width distribution |
2804
- | `filterable` | `boolean \| FilterConfig` | — | `true` (text), `{ type: 'number', min?, max? }`, `{ type: 'multiselect', options? }` |
2805
- | `contextMenu` | `boolean \| ContextMenuConfig` | inherits | Override grid-level context menu. `false` hides entirely. `{ sort?, pin?, group? }` controls sections |
2806
- | `aggregate` | `AggregateName \| (values, rows) => value` | — | Total this column over each group row, and over the footer. See below |
2807
- | `AggregateCell` | `({ cell }) => ReactNode` | — | Renders the aggregate. `cell`: `{ value, column, rows, scope }`, `scope` being `'group'` or `'footer'` |
2808
- | `editable` | `boolean \| (row) => boolean` | inherits | Whether this column’s cells can be edited — every row, or the rows a predicate says |
2809
- | `editor` | `CellEditorType \| CellEditorConfig` | inferred | `text`/`number`/`checkbox`/`select`. Without one it is read off the value in the cell |
2810
- | `EditCell` | `({ cell }) => ReactNode` | — | An editor of your own, bound to `cell.draft` with `cell.commitEdit()`/`cell.cancelEdit()` |
2811
- | `exportValue` | `(row: TRow) => value` | the field | What this column writes to an exported file — an export runs no React, so a `Cell` renderer needs it |
2812
- | `list` | `'title' \| 'kicker' \| 'subtitle' \| 'end' \| 'field' \| 'detail' \| 'hidden'` | inferred | Where the column goes in a list item, on a phone |
2813
- | `exportFormat` | `string` | — | The number format its cells wear in a workbook, `'#,##0.00'` or `'yyyy-mm-dd'` |
2863
+ | Prop | Type | Default | Description |
2864
+ | ------------------------ | ------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2865
+ | `key` | `Key` | required | Column identifier (maps to TRow property) |
2866
+ | `header` | `string` | — | Header text |
2867
+ | `width` | `number` | `200` | Base width in px |
2868
+ | `align` | `'start' \| 'end' \| 'center' \| 'left' \| 'right'` | `'left'` | Cell alignment. `start`/`end` follow the reading order; `left`/`right` stay on the screen side they name |
2869
+ | `pin` | `'START' \| 'END'` (also `'LEFT'`/`'RIGHT'`) | — | Pin to an edge of the **inline axis** (sticky on scroll), so it holds under either `dir`. `LEFT`/`RIGHT` are the older spelling of the same two |
2870
+ | `columns` | `ColumnType[]` | — | Nested columns (grouped header) |
2871
+ | `Cell` | `({ cell }) => ReactNode` | — | Custom renderer. `cell`: `{ value, row, column, grid }`. It fills the cell **edge to edge, with no padding** — the built-in text pads itself `px={3}`, so text of your own does the same. It redraws when its row changes or the grid does — not when another row is selected |
2872
+ | `sortable` / `resizable` | `boolean` | inherits | Override grid-level setting |
2873
+ | `flexible` | `boolean` | `true` | Participate in flex width distribution |
2874
+ | `filterable` | `boolean \| FilterConfig` | — | `true` (text), `{ type: 'number', min?, max? }`, `{ type: 'multiselect', options? }` |
2875
+ | `contextMenu` | `boolean \| ContextMenuConfig` | inherits | Override grid-level context menu. `false` hides entirely. `{ sort?, pin?, group? }` controls sections |
2876
+ | `aggregate` | `AggregateName \| (values, rows) => value` | — | Total this column over each group row, and over the footer. See below |
2877
+ | `AggregateCell` | `({ cell }) => ReactNode` | — | Renders the aggregate. `cell`: `{ value, column, rows, scope }`, `scope` being `'group'` or `'footer'` |
2878
+ | `editable` | `boolean \| (row) => boolean` | inherits | Whether this column’s cells can be edited — every row, or the rows a predicate says |
2879
+ | `editor` | `CellEditorType \| CellEditorConfig` | inferred | `text`/`number`/`checkbox`/`select`. Without one it is read off the value in the cell |
2880
+ | `EditCell` | `({ cell }) => ReactNode` | — | An editor of your own, bound to `cell.draft` with `cell.commitEdit()`/`cell.cancelEdit()` |
2881
+ | `exportValue` | `(row: TRow) => value` | the field | What this column writes to an exported file — an export runs no React, so a `Cell` renderer needs it |
2882
+ | `list` | `'title' \| 'kicker' \| 'subtitle' \| 'end' \| 'field' \| 'detail' \| 'hidden'` | inferred | Where the column goes in a list item, on a phone |
2883
+ | `exportFormat` | `string` | — | The number format its cells wear in a workbook, `'#,##0.00'` or `'yyyy-mm-dd'` |
2814
2884
 
2815
2885
  ### Aggregation
2816
2886
 
@@ -3786,15 +3856,16 @@ with `language` and `code`, `rule`, `image`, `tableWrap`, `table`, `row`, `cell`
3786
3856
 
3787
3857
  Where content will be, while it is being fetched.
3788
3858
 
3789
- | Prop | Type | What it does |
3790
- | -------- | ----------- | ---------------------------------------------------------------------------- |
3791
- | `lines` | `number` | How many bars. Default 1; past that the last one is short. |
3792
- | `circle` | `boolean` | One round bar — an avatar. `width` sets its size and the height follows. |
3793
- | `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`. |
3859
+ | Prop | Type | What it does |
3860
+ | -------- | ----------- | ---------------------------------------------------------------------------------------- |
3861
+ | `lines` | `number` | How many bars. Default 1, which fills the skeleton; past that the last one is short. |
3862
+ | `circle` | `boolean` | One round bar — an avatar. `width` sets its size (40px without one), the height follows. |
3863
+ | `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`. |
3794
3864
 
3795
3865
  ```tsx
3796
3866
  <Skeleton lines={3} label="Loading orders" />
3797
3867
  <Skeleton circle width={10} />
3868
+ <Skeleton height={40} /> {/* one bar fills it: an image or a chart placeholder */}
3798
3869
  ```
3799
3870
 
3800
3871
  A `label` belongs on the one skeleton standing for a region and not on each bar — a screen of
@@ -3802,7 +3873,7 @@ placeholders each announcing itself is a screen nobody can listen to, and a regi
3802
3873
  with its text is not reliably announced anyway, so what it really buys is words for somebody who lands
3803
3874
  there. The gloss is a named duration in milliseconds, so it sits outside what `--transitionTime` zeroes
3804
3875
  and stops itself under `prefers-reduced-motion`. It renders on a server. Tree: `skeleton` (+ `bar` with
3805
- `short` and `circle` variants, `gloss`).
3876
+ `short`, `circle` and `fill` variants, `gloss`).
3806
3877
 
3807
3878
  ## The catalog (`@box-kite/react/catalog`)
3808
3879
 
@@ -660,8 +660,15 @@ const wobble = Box.spring({ stiffness: 120, damping: 8 });
660
660
  the same commit, with nothing to configure. `Tooltip`, the `Dropdown` popup and the DataGrid column
661
661
  menu are all built on it, and each expresses its exit as a `closed` variant in its component styles — plus `up`/`closedUp` on `dropdown.items`, since a popup that opened upward has to collapse upward.
662
662
  - **`Box.configure({ transition })`** changes what the base class transitions for the whole engine:
663
- a group name, or `false` to declare nothing at all and leave transitions entirely to the props.
663
+ a group name, a comma list of groups and property names (`'colors, opacity'`), or `false` to declare
664
+ nothing at all and leave transitions entirely to the props. Anything else transitions `all` and warns.
664
665
  Call it before the first render.
666
+ - **An inline phrasing element is `inline-block`**, where every other Box is `block`: `a`, `span`, `label`, `time`, `mark`, `abbr`, `code`, `kbd`, `samp`, `var`, `em`,
667
+ `strong`, `small`, `b`, `i`, `u`, `s`, `sub`, `sup`, `q`, `cite`, `dfn`, `data`, `output`, `bdi`, `bdo`. It is
668
+ selected on the rendered element at the base class's specificity, so `<Link>`, `<Box tag="a">` and a
669
+ `useClassNames` class on a router's `<a>` all stay in their sentence, margin and padding still apply, and a
670
+ `display` prop or component still wins. Media (`img`, `svg`, `video`, `canvas`, `iframe`) stays block. An `inline-block` never breaks across lines, so a
671
+ prose link or `<Mark>` that has to wrap with the text sets `display="inline"`.
665
672
 
666
673
  ---
667
674
 
@@ -1263,7 +1270,7 @@ import Textbox from '@box-kite/react/components/textbox';
1263
1270
  <Flex d="column" gap={2} md={{ d: 'row', gap: 4 }}>{children}</Flex>
1264
1271
 
1265
1272
  // Grid
1266
- <Grid gridCols={3} gap={4}>{items.map(i => <Box key={i.id}>{i.content}</Box>)}</Grid>
1273
+ <Grid gridTemplateColumns={3} gap={4}>{items.map(i => <Box key={i.id}>{i.content}</Box>)}</Grid>
1267
1274
 
1268
1275
  // Card
1269
1276
  <Box p={4} bgColor="white" borderRadius={8} shadow="medium">{content}</Box>
@@ -1320,6 +1327,11 @@ undefined and rendering it costs nothing, so write the line either way. Pass `{
1320
1327
  second argument for an element inside an `<svg>`. `<Icon>` is this hook plus that flag — reach for
1321
1328
  `Icon` for an icon and this only for everything else.
1322
1329
 
1330
+ The class carries the base reset, which is `display: block` except on an `<a>`, `<span>`, `<label>` and the
1331
+ other phrasing elements, which are `inline-block`: a `NavLink` styled this way stays in its sentence.
1332
+ `{ base: false }` leaves the reset out — for a library's own root (`<ReactFlow>`, `<MiniMap>`, a `BaseEdge`)
1333
+ whose margin, display and transitions are its own.
1334
+
1323
1335
  ### Group and Peer
1324
1336
 
1325
1337
  ```tsx
@@ -1456,6 +1468,19 @@ Full reference: `docs/a11y-primitives.md`.
1456
1468
 
1457
1469
  ---
1458
1470
 
1471
+ ## Field ids, external labels and the invalid state
1472
+
1473
+ - **A top-level `id` lands on the control, once.** `<Combobox id="owner">` puts it on the `<input role="combobox">`
1474
+ and nowhere else, as `Textbox`, `Textarea`, `Select` (its button), `Checkbox`, `Switch`, `RadioButton` and
1475
+ `TimeZoneSelect` do, so `<label htmlFor="owner">` names the control. `DateField`, `DatePicker`, `Slider` and
1476
+ `RadioGroup` are groups no `<label>` can name: the `id` goes on the group, and `labelledBy` points it at an
1477
+ external label.
1478
+ - **`aria-invalid` draws the error border on every field.** `props={{ 'aria-invalid': true }}` turns the frame of a
1479
+ `Textbox`, `Textarea`, `Select` or `Combobox` `red-500`, through hover and focus, in both themes. A combobox's
1480
+ attribute is on its input and its border on the shell, so the shell matches `:has([aria-invalid="true"])` —
1481
+ restyle it as `combobox: { styles: { has: { '[aria-invalid="true"]': { … } } } }`, the others with
1482
+ `ariaAttr: { invalid: { … } }`. `DateField` and `DatePicker` take an `invalid` prop that does both.
1483
+
1459
1484
  ## Sizes: compact, normal, large
1460
1485
 
1461
1486
  Every built-in component is drawn at one of three sizes, and a size is two CSS variables —
@@ -1521,16 +1546,16 @@ the association needs no `htmlFor`/`id` pair and the whole row is a click target
1521
1546
  nothing wraps the input — the markup is what it always was. `labelProps` styles that `<label>`;
1522
1547
  every other Box prop styles the control itself.
1523
1548
 
1524
- | Component | Prop | What it does |
1525
- | ------------ | ------------------------ | -------------------------------------------------------------------------------------------- |
1526
- | all three | `label` / `labelProps` | The wrapping `<label>` and its styles. |
1527
- | `Checkbox` | `indeterminate` | Sets the DOM property **and** `aria-checked="mixed"`, which is what is announced. |
1528
- | `Switch` | — | `role="switch"` over the same input; Space **and** Enter toggle, Enter does not submit. |
1529
- | `RadioGroup` | `label` | `role="radiogroup"` named by it — a set of radios with nothing over it is a flat list. |
1530
- | `RadioGroup` | `name` | The field every item submits under. Generated when left out. |
1531
- | `RadioGroup` | `value` / `defaultValue` | The selected item's value (a string — the platform's own model for a radio). |
1532
- | `RadioGroup` | `onValueChange` | `(value, { reason, event })` — `click` or `keyboard`. `onChange` is the deprecated spelling. |
1533
- | `RadioGroup` | `orientation` | `vertical` (default) or `horizontal`. Both arrow pairs navigate either way. |
1549
+ | Component | Prop | What it does |
1550
+ | ------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
1551
+ | all three | `label` / `labelProps` | The wrapping `<label>` and its styles. |
1552
+ | `Checkbox` | `indeterminate` | Sets the DOM property **and** `aria-checked="mixed"`, which is what is announced. |
1553
+ | `Switch` | — | `role="switch"` over the same input; Space **and** Enter toggle, Enter does not submit. |
1554
+ | `RadioGroup` | `label` | `role="radiogroup"` named by it — a set of radios with nothing over it is a flat list. |
1555
+ | `RadioGroup` | `name` | The field every item submits under. Generated when left out. |
1556
+ | `RadioGroup` | `value` / `defaultValue` | The selected item's value (a string — the platform's own model for a radio). |
1557
+ | `RadioGroup` | `onValueChange` | `(value, { reason, event })` — `click` or `keyboard`. `onChange` is the deprecated spelling. |
1558
+ | `RadioGroup` | `orientation` | `vertical` (default) or `horizontal`, which wraps and keeps the label on a line of its own above the radios. Both arrow pairs navigate either way. |
1534
1559
 
1535
1560
  Arrow keys move **and select** inside a group, wrapping at both ends and skipping disabled items;
1536
1561
  Tab enters the group once and leaves it once, because a native radio set is already a single tab
@@ -2660,6 +2685,51 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
2660
2685
  <DataGrid data={rows} def={{ rowKey: 'id', columns: [{ key: 'trend', header: 'Last 12 months', width: 180, Cell: TrendCell }] }} />;
2661
2686
  ```
2662
2687
 
2688
+ ### Theming a React Flow canvas — `FlowContainer`
2689
+
2690
+ `ChartContainer`'s move for diagrams. `@box-kite/react/components/flow` (server-safe, imports nothing from xyflow) is a Box
2691
+ that declares React Flow's `--xy-*` variables from the palette in both themes, so the canvas — background, pattern, edges,
2692
+ handles, built-in nodes, minimap, controls, selection box, edge labels — names no colour and follows the page:
2693
+
2694
+ ```tsx
2695
+ import { Background, Controls, MiniMap, ReactFlow } from '@xyflow/react';
2696
+ import '@xyflow/react/dist/style.css';
2697
+ import { FlowContainer, flowColorMode } from '@box-kite/react/components/flow';
2698
+
2699
+ function Canvas() {
2700
+ const [theme] = Box.useTheme();
2701
+
2702
+ return (
2703
+ <FlowContainer height={120}>
2704
+ <ReactFlow nodes={nodes} edges={edges} colorMode={flowColorMode(theme)} fitView>
2705
+ <Background />
2706
+ <Controls />
2707
+ <MiniMap />
2708
+ </ReactFlow>
2709
+ </FlowContainer>
2710
+ );
2711
+ }
2712
+ ```
2713
+
2714
+ | Prop | What it is |
2715
+ | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
2716
+ | `tokens` | Repaints a role: `{ selection: 'emerald-500' }` for both themes, `{ surface: { light: 'base-50', dark: 'base-950' } }` per theme |
2717
+ | `motion` | `'static'` zeroes every Box transition inside the canvas, for nodes moved or resized every frame |
2718
+
2719
+ - **Eight roles**, each a custom property a custom node can read: `--flow-surface`, `--flow-panel`, `--flow-text`, `--flow-border`,
2720
+ `--flow-grid`, `--flow-edge`, `--flow-selection`, `--flow-handle`. Defaults are `base`/`accent`, so `Box.palette()` reaches them.
2721
+ - **The defaults are the `flowContainer` style node**, so one override repaints every canvas in an app:
2722
+ `Box.components({ flowContainer: { styles: { vars: { 'flow-selection': 'emerald-500' } } } })`; `tokens` is the per-canvas answer.
2723
+ - **One `--xy-*` variable is `vars`** (`vars={{ 'xy-edge-stroke-width': '2' }}`); a prop on `<MiniMap>`/`<Background>` still wins.
2724
+ - **`colorMode={flowColorMode(theme)}`** keeps React Flow's own `.dark` class in step with the page. A canvas in another theme sits
2725
+ in `<Box.Theme use="local">`; never `colorMode="light"` under a dark page — `.light` is no theme boundary, so the two mix.
2726
+ - **A custom node is invisible to the keyboard** — xyflow removes every node's focus outline. Spread `flowNodeStyles` on its
2727
+ root (an outline while xyflow's wrapper is `:focus-visible`) and `flowNodeSelectedStyles` when its `selected` prop is true;
2728
+ both are outlines in the `selection` role, so they survive forced colours. A custom edge on `BaseEdge` takes `flowEdgeStyles`
2729
+ through `useClassNames(flowEdgeStyles, { svg: true, base: false })`.
2730
+ - Colour props refuse `var()`, so a custom node reads a role through `css`:
2731
+ `css={{ backgroundColor: 'var(--flow-panel)', borderColor: 'var(--flow-border)', color: 'var(--flow-text)' }}`.
2732
+
2663
2733
  ---
2664
2734
 
2665
2735
  ## Key Reminders for AI Assistants
@@ -2677,7 +2747,7 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
2677
2747
  11. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
2678
2748
  12. **Box is memoized** with `React.memo`, and so are `Flex`, `H1`, `Button` and the rest. A component of your own that is a Box with defaults calls `useBoxElement({ p: 4, ...props }, ref)` from its own `forwardRef` rather than returning `<Box>` — one fiber instead of two
2679
2749
  13. **`style` is top-level only** — never inside breakpoints (`sm={{ style: ... }}`), pseudo-classes, or theme objects. `css` is the one that nests: `sm={{ css: { objectPosition: 'top' } }}` is a rule
2680
- 14. **A CSS variable is a prop**: `vars={{ 'color-x': 'sky-500' }}` declares `--color-x` for everything inside — and a third-party chart goes in `<ChartContainer series={['revenue']}>` so it names no colour at all
2750
+ 14. **A CSS variable is a prop**: `vars={{ 'color-x': 'sky-500' }}` declares `--color-x` for everything inside — and a third-party chart goes in `<ChartContainer series={['revenue']}>` so it names no colour at all, and a React Flow canvas goes in `<FlowContainer>` with `colorMode={flowColorMode(theme)}`
2681
2751
  15. **All sizing props use divider 4** — `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight` are NOT direct pixels. `height={10}` = 2.5rem = 40px
2682
2752
  16. **A state your own code sets is a nested prop too**: `dataAttr={{ 'state=open': { … } }}` → `[data-state="open"]`, `ariaAttr={{ selected: { … } }}` → `[aria-selected="true"]`, `has={{ ':checked': { … } }}`, `not={{ hover: { … } }}`, `nth={{ odd: { … } }}`. The attribute itself still goes in `props`
2683
2753
  17. **Somebody else's state is `group`/`peer`**: `group={{ 'card/hover': { … } }}` → `.card:hover .x`, `peer={{ 'agree/checked': { … } }}` → `.agree:checked ~ .x`. The ancestor or sibling carries the class via `className`; `hoverGroup` and its four siblings are the older spelling
@@ -2790,27 +2860,27 @@ named ones carry the reason.
2790
2860
 
2791
2861
  ### ColumnType
2792
2862
 
2793
- | Prop | Type | Default | Description |
2794
- | ------------------------ | ------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
2795
- | `key` | `Key` | required | Column identifier (maps to TRow property) |
2796
- | `header` | `string` | — | Header text |
2797
- | `width` | `number` | `200` | Base width in px |
2798
- | `align` | `'start' \| 'end' \| 'center' \| 'left' \| 'right'` | `'left'` | Cell alignment. `start`/`end` follow the reading order; `left`/`right` stay on the screen side they name |
2799
- | `pin` | `'START' \| 'END'` (also `'LEFT'`/`'RIGHT'`) | — | Pin to an edge of the **inline axis** (sticky on scroll), so it holds under either `dir`. `LEFT`/`RIGHT` are the older spelling of the same two |
2800
- | `columns` | `ColumnType[]` | — | Nested columns (grouped header) |
2801
- | `Cell` | `({ cell }) => ReactNode` | — | Custom renderer. `cell`: `{ value, row, column, grid }`. It redraws when its row changes or the grid does — not when another row is selected |
2802
- | `sortable` / `resizable` | `boolean` | inherits | Override grid-level setting |
2803
- | `flexible` | `boolean` | `true` | Participate in flex width distribution |
2804
- | `filterable` | `boolean \| FilterConfig` | — | `true` (text), `{ type: 'number', min?, max? }`, `{ type: 'multiselect', options? }` |
2805
- | `contextMenu` | `boolean \| ContextMenuConfig` | inherits | Override grid-level context menu. `false` hides entirely. `{ sort?, pin?, group? }` controls sections |
2806
- | `aggregate` | `AggregateName \| (values, rows) => value` | — | Total this column over each group row, and over the footer. See below |
2807
- | `AggregateCell` | `({ cell }) => ReactNode` | — | Renders the aggregate. `cell`: `{ value, column, rows, scope }`, `scope` being `'group'` or `'footer'` |
2808
- | `editable` | `boolean \| (row) => boolean` | inherits | Whether this column’s cells can be edited — every row, or the rows a predicate says |
2809
- | `editor` | `CellEditorType \| CellEditorConfig` | inferred | `text`/`number`/`checkbox`/`select`. Without one it is read off the value in the cell |
2810
- | `EditCell` | `({ cell }) => ReactNode` | — | An editor of your own, bound to `cell.draft` with `cell.commitEdit()`/`cell.cancelEdit()` |
2811
- | `exportValue` | `(row: TRow) => value` | the field | What this column writes to an exported file — an export runs no React, so a `Cell` renderer needs it |
2812
- | `list` | `'title' \| 'kicker' \| 'subtitle' \| 'end' \| 'field' \| 'detail' \| 'hidden'` | inferred | Where the column goes in a list item, on a phone |
2813
- | `exportFormat` | `string` | — | The number format its cells wear in a workbook, `'#,##0.00'` or `'yyyy-mm-dd'` |
2863
+ | Prop | Type | Default | Description |
2864
+ | ------------------------ | ------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2865
+ | `key` | `Key` | required | Column identifier (maps to TRow property) |
2866
+ | `header` | `string` | — | Header text |
2867
+ | `width` | `number` | `200` | Base width in px |
2868
+ | `align` | `'start' \| 'end' \| 'center' \| 'left' \| 'right'` | `'left'` | Cell alignment. `start`/`end` follow the reading order; `left`/`right` stay on the screen side they name |
2869
+ | `pin` | `'START' \| 'END'` (also `'LEFT'`/`'RIGHT'`) | — | Pin to an edge of the **inline axis** (sticky on scroll), so it holds under either `dir`. `LEFT`/`RIGHT` are the older spelling of the same two |
2870
+ | `columns` | `ColumnType[]` | — | Nested columns (grouped header) |
2871
+ | `Cell` | `({ cell }) => ReactNode` | — | Custom renderer. `cell`: `{ value, row, column, grid }`. It fills the cell **edge to edge, with no padding** — the built-in text pads itself `px={3}`, so text of your own does the same. It redraws when its row changes or the grid does — not when another row is selected |
2872
+ | `sortable` / `resizable` | `boolean` | inherits | Override grid-level setting |
2873
+ | `flexible` | `boolean` | `true` | Participate in flex width distribution |
2874
+ | `filterable` | `boolean \| FilterConfig` | — | `true` (text), `{ type: 'number', min?, max? }`, `{ type: 'multiselect', options? }` |
2875
+ | `contextMenu` | `boolean \| ContextMenuConfig` | inherits | Override grid-level context menu. `false` hides entirely. `{ sort?, pin?, group? }` controls sections |
2876
+ | `aggregate` | `AggregateName \| (values, rows) => value` | — | Total this column over each group row, and over the footer. See below |
2877
+ | `AggregateCell` | `({ cell }) => ReactNode` | — | Renders the aggregate. `cell`: `{ value, column, rows, scope }`, `scope` being `'group'` or `'footer'` |
2878
+ | `editable` | `boolean \| (row) => boolean` | inherits | Whether this column’s cells can be edited — every row, or the rows a predicate says |
2879
+ | `editor` | `CellEditorType \| CellEditorConfig` | inferred | `text`/`number`/`checkbox`/`select`. Without one it is read off the value in the cell |
2880
+ | `EditCell` | `({ cell }) => ReactNode` | — | An editor of your own, bound to `cell.draft` with `cell.commitEdit()`/`cell.cancelEdit()` |
2881
+ | `exportValue` | `(row: TRow) => value` | the field | What this column writes to an exported file — an export runs no React, so a `Cell` renderer needs it |
2882
+ | `list` | `'title' \| 'kicker' \| 'subtitle' \| 'end' \| 'field' \| 'detail' \| 'hidden'` | inferred | Where the column goes in a list item, on a phone |
2883
+ | `exportFormat` | `string` | — | The number format its cells wear in a workbook, `'#,##0.00'` or `'yyyy-mm-dd'` |
2814
2884
 
2815
2885
  ### Aggregation
2816
2886
 
@@ -3786,15 +3856,16 @@ with `language` and `code`, `rule`, `image`, `tableWrap`, `table`, `row`, `cell`
3786
3856
 
3787
3857
  Where content will be, while it is being fetched.
3788
3858
 
3789
- | Prop | Type | What it does |
3790
- | -------- | ----------- | ---------------------------------------------------------------------------- |
3791
- | `lines` | `number` | How many bars. Default 1; past that the last one is short. |
3792
- | `circle` | `boolean` | One round bar — an avatar. `width` sets its size and the height follows. |
3793
- | `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`. |
3859
+ | Prop | Type | What it does |
3860
+ | -------- | ----------- | ---------------------------------------------------------------------------------------- |
3861
+ | `lines` | `number` | How many bars. Default 1, which fills the skeleton; past that the last one is short. |
3862
+ | `circle` | `boolean` | One round bar — an avatar. `width` sets its size (40px without one), the height follows. |
3863
+ | `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`. |
3794
3864
 
3795
3865
  ```tsx
3796
3866
  <Skeleton lines={3} label="Loading orders" />
3797
3867
  <Skeleton circle width={10} />
3868
+ <Skeleton height={40} /> {/* one bar fills it: an image or a chart placeholder */}
3798
3869
  ```
3799
3870
 
3800
3871
  A `label` belongs on the one skeleton standing for a region and not on each bar — a screen of
@@ -3802,7 +3873,7 @@ placeholders each announcing itself is a screen nobody can listen to, and a regi
3802
3873
  with its text is not reliably announced anyway, so what it really buys is words for somebody who lands
3803
3874
  there. The gloss is a named duration in milliseconds, so it sits outside what `--transitionTime` zeroes
3804
3875
  and stops itself under `prefers-reduced-motion`. It renders on a server. Tree: `skeleton` (+ `bar` with
3805
- `short` and `circle` variants, `gloss`).
3876
+ `short`, `circle` and `fill` variants, `gloss`).
3806
3877
 
3807
3878
  ## The catalog (`@box-kite/react/catalog`)
3808
3879