@box-kite/react 2.1.0 → 2.2.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 (86) hide show
  1. package/.claude/rules/box-kite-rules.md +8 -2
  2. package/.claude/skills/box-kite/SKILL.md +11 -5
  3. package/.claude/skills/box-kite/references/components.md +2 -2
  4. package/.claude/skills/box-kite/references/patterns.md +1 -1
  5. package/.claude/skills/box-kite/references/styling.md +23 -0
  6. package/.cursor/rules/box-kite.mdc +9 -3
  7. package/AGENTS.md +9 -3
  8. package/BOX_AI_CONTEXT.md +250 -87
  9. package/BOX_KITE_AI_CONTEXT.md +250 -87
  10. package/README.md +3 -3
  11. package/box.cjs +1 -1
  12. package/box.d.ts +13 -2
  13. package/box.mjs +25 -18
  14. package/catalog.cjs +1 -1
  15. package/catalog.mjs +15 -1
  16. package/client.cjs +1 -1
  17. package/client.mjs +59 -51
  18. package/components/button.cjs +1 -1
  19. package/components/button.mjs +10 -14
  20. package/components/dataGrid/components/dataGridBottomBarSelection.d.ts +7 -0
  21. package/components/dataGrid/components/dataGridDetailRow.d.ts +3 -3
  22. package/components/dataGrid/components/dataGridGroupRow.d.ts +3 -3
  23. package/components/dataGrid/components/dataGridHeader.d.ts +6 -2
  24. package/components/dataGrid/components/dataGridRow.d.ts +3 -3
  25. package/components/dataGrid/contracts/dataGridContract.d.ts +13 -0
  26. package/components/dataGrid/gridNavigationContext.d.ts +7 -2
  27. package/components/dataGrid/models/editModel.d.ts +6 -4
  28. package/components/dataGrid/models/exportModel.d.ts +1 -1
  29. package/components/dataGrid/models/gridModel.d.ts +55 -2
  30. package/components/dataGrid/models/headerCellModel.d.ts +5 -0
  31. package/components/dataGrid/models/localeText.d.ts +102 -0
  32. package/components/dataGrid/models/rangeModel.d.ts +2 -0
  33. package/components/dataGrid/useFilterDraft.d.ts +9 -0
  34. package/components/dataGrid.cjs +3 -3
  35. package/components/dataGrid.d.ts +1 -0
  36. package/components/dataGrid.mjs +1394 -990
  37. package/components/flex.cjs +1 -1
  38. package/components/flex.mjs +10 -13
  39. package/components/form.cjs +1 -1
  40. package/components/form.mjs +7 -7
  41. package/components/grid.cjs +1 -1
  42. package/components/grid.mjs +10 -13
  43. package/components/menu.cjs +1 -1
  44. package/components/menu.mjs +16 -16
  45. package/components/overlay.cjs +1 -1
  46. package/components/overlay.mjs +35 -34
  47. package/components/popover.cjs +1 -1
  48. package/components/popover.mjs +4 -4
  49. package/components/semantics.cjs +1 -1
  50. package/components/semantics.d.ts +29 -29
  51. package/components/semantics.mjs +9 -13
  52. package/components/svg.cjs +1 -1
  53. package/components/svg.mjs +38 -44
  54. package/components/textarea.cjs +1 -1
  55. package/components/textarea.mjs +12 -16
  56. package/components/textbox.cjs +1 -1
  57. package/components/textbox.mjs +12 -16
  58. package/components/toaster.cjs +1 -1
  59. package/components/toaster.mjs +42 -43
  60. package/components/visuallyHidden.cjs +1 -1
  61. package/components/visuallyHidden.mjs +10 -13
  62. package/docs/components.md +1 -1
  63. package/docs/mcp.md +6 -5
  64. package/docs/props.md +132 -20
  65. package/layer.cjs +1 -1
  66. package/layer.mjs +61 -59
  67. package/mcp/src/data.d.ts +1 -1
  68. package/mcp/src/styles.d.ts +2 -2
  69. package/mcp/src/vocabulary.d.ts +1 -1
  70. package/package.json +81 -49
  71. package/react/boxComponent.d.ts +10 -0
  72. package/react/popover/useTopLayer.d.ts +17 -0
  73. package/react/theme/theme.d.ts +10 -0
  74. package/react/useStyles.d.ts +2 -0
  75. package/react-shared.cjs +1 -1
  76. package/react-shared.mjs +44 -37
  77. package/rsc.cjs +1 -1
  78. package/rsc.d.ts +11 -1
  79. package/rsc.mjs +15 -12
  80. package/ssg.cjs +1 -1
  81. package/ssg.d.ts +8 -2
  82. package/ssg.mjs +13 -9
  83. package/utils/dataGrid/activeCellStore.d.ts +14 -0
  84. package/utils/dataGrid/filterDraftUtils.d.ts +19 -0
  85. package/utils/dataGrid/headerCellUtils.d.ts +50 -0
  86. package/utils/environment/environmentUtils.d.ts +5 -0
package/BOX_AI_CONTEXT.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @box-kite/react - AI Assistant Context
2
2
 
3
- Runtime CSS-in-JS library. `Box` component accepts 221 CSS props and generates CSS classes at runtime. Same prop values share a single class.
3
+ Runtime CSS-in-JS library. `Box` component accepts 235 CSS props and generates CSS classes at runtime. Same prop values share a single class.
4
4
 
5
5
  ---
6
6
 
@@ -282,9 +282,13 @@ const { css, anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', ali
282
282
  The palette is **Tailwind 4.3's, in OKLCH**: twenty-six families of eleven steps (`50`–`950`) — the five
283
283
  neutrals (`slate`, `gray`, `zinc`, `neutral`, `stone`), the four Tailwind 4.3 added (`mauve`, `mist`,
284
284
  `olive`, `taupe`) and seventeen hues (`red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`,
285
- `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`). Each token is a
286
- CSS variable declared in `:root` the first time something uses it, so a colour you never write costs
287
- nothing.
285
+ `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`). A token is a
286
+ CSS variable written **with its own value as the fallback** — `bgColor="sky-500"` is
287
+ `background-color: var(--sky-500,oklch(68.5% .169 237.3))` — so nothing is declared in `:root` (a late
288
+ `:root` insert recalculates every element on the page) and declaring `--sky-500` yourself still overrides
289
+ it. `var(--sky-500)` elsewhere in this file is that, shortened. Your own CSS cannot count on `--sky-500`
290
+ existing: hand it a variable of yours with `vars={{ brand: 'violet-500' }}` on an ancestor. A hand-written
291
+ `var(--sky-500)` _inside_ a Box value — `fill`, `vars`, `css`, a `Box.extend()` variable — gets the fallback too.
288
292
 
289
293
  Any colour value takes a slash and a percentage:
290
294
 
@@ -642,6 +646,131 @@ const wobble = Box.spring({ stiffness: 120, damping: 8 });
642
646
 
643
647
  ---
644
648
 
649
+ ### Scroll-driven animations
650
+
651
+ An animation's progress can come from a scroll position rather than a clock. There is no listener, no
652
+ `requestAnimationFrame` and no state: the browser runs it off the compositor, and the whole thing is props.
653
+
654
+ | Prop | CSS Property | Notes |
655
+ | ------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
656
+ | `animationTimeline` | animation-timeline | `'none'`, `'auto'`, an anonymous timeline (`'scroll()'`, `'scroll(root block)'`, `'view()'`, `'view(inline 20%)'`) or a name a declaration below gave |
657
+ | `animationRange` | animation-range | One end or both: `'entry'`, `'cover 20%'`, `'entry 0% entry 100%'`, `'normal'` |
658
+ | `animationRangeStart` / `animationRangeEnd` | animation-range-start / -end | One end at a time. A named part of the pass with an optional offset, a bare length, or `'normal'` |
659
+ | `scrollTimeline` | scroll-timeline | Declares a named timeline on a **scroller**: `'page'` or `'page block'`. The `--` is optional and added for you |
660
+ | `scrollTimelineName` / `scrollTimelineAxis` | scroll-timeline-name / -axis | The two halves of that shorthand. Axis: `'block'`, `'inline'`, `'x'`, `'y'` |
661
+ | `viewTimeline` | view-timeline | Declares a named timeline on a **subject** — its own pass across the scrollport: `'card'` or `'card block'` |
662
+ | `viewTimelineName` / `viewTimelineAxis` | view-timeline-name / -axis | The two halves of that one |
663
+ | `viewTimelineInset` | view-timeline-inset | `'auto'`, or one or two lengths, shrinking the scrollport the pass is measured against |
664
+ | `timelineScope` | timeline-scope | Makes named timelines below this element reachable from anywhere else below it. Comma-separated |
665
+
666
+ The two anonymous timelines cover most of it and need nothing declared elsewhere:
667
+
668
+ ```tsx
669
+ // A reading-progress bar: the page's own scroll, driving a scaleX from 0 to 1.
670
+ Box.keyframes({ progress: { from: { scale: 0 }, to: { scale: 1 } } });
671
+
672
+ <Box
673
+ position="fixed"
674
+ top={0}
675
+ insetX={0}
676
+ height={1}
677
+ bgColor="sky-500"
678
+ css={{ transformOrigin: 'left' }}
679
+ animationName="progress"
680
+ animationTimeline="scroll(root)"
681
+ animationFillMode="both"
682
+ motionReduce={{ animation: 'none' }}
683
+ />;
684
+
685
+ // Reveal on scroll: this element's own pass, over the first 60% of its entry.
686
+ Box.keyframes({ 'fade-up': { from: { opacity: 0, translateY: 4 }, to: { opacity: 1, translateY: 0 } } });
687
+
688
+ <Box animationName="fade-up" animationTimeline="view()" animationRange="entry 0% entry 60%" motionReduce={{ animation: 'none' }}>
689
+ …
690
+ </Box>;
691
+ ```
692
+
693
+ A **named** timeline is the two-sided form, for when the animated element is not inside the scroller:
694
+
695
+ ```tsx
696
+ <Box timelineScope="page">
697
+ <Box height="fit-screen" overflow="auto" scrollTimeline="page block">
698
+ …
699
+ </Box>
700
+ <Box animationName="progress" animationTimeline="page" animationFillMode="both" />
701
+ </Box>
702
+ ```
703
+
704
+ **Three things to know, and all three are traps:**
705
+
706
+ - **The CSS `animation` shorthand resets `animation-timeline`.** A timeline declared before it is
707
+ silently undone. The registry declares `animationTimeline` after `animation`, so writing both props is
708
+ safe in either order — but an `animation` written inside `css` sorts last and will undo it.
709
+ - **A scroll-driven animation has no duration**, so `--transitionTime` cannot zero it. It is the one kind
710
+ of motion in this library that does **not** stop itself under `prefers-reduced-motion`: pair it with
711
+ `motionReduce={{ animation: 'none' }}`, every time.
712
+ - **Where the browser has none, the declaration is dropped rather than the animation.** It then runs on the
713
+ document timeline over whatever duration it was given. Make the end state the resting state — with
714
+ `animationFillMode="both"` and a sequence that ends where the element belongs, a missing timeline degrades
715
+ to "already arrived" rather than to a loop. Chrome/Edge 115+, Safari 26+; Firefox is behind a flag.
716
+
717
+ ---
718
+
719
+ ### View transitions
720
+
721
+ The browser screenshots the page, runs the update, screenshots again, and cross-fades between the two —
722
+ so a change that was a jump becomes a transition with nothing animated by hand.
723
+
724
+ | Prop / API | What it does |
725
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
726
+ | `viewTransitionName` | Names this element, so it animates _from where it was to where it is_ instead of being cross-faded with the page. A `<custom-ident>`, **not** dashed |
727
+ | `viewTransitionClass` | A name shared by several elements, so `::view-transition-group(.card)` styles them together. Space-separated for more than one |
728
+ | `Box.viewTransition(update, options)` | Runs `update` inside `document.startViewTransition` where the browser has one, and plainly where it has not |
729
+ | `<Box.Theme viewTransition>` | That call around a theme change, `flushSync` included |
730
+
731
+ ```tsx
732
+ const handle = Box.viewTransition(() => flushSync(() => setTab(next)), { types: ['forward'] });
733
+ await handle.finished;
734
+ ```
735
+
736
+ ```ts
737
+ interface ViewTransitionOptions {
738
+ /** `'skip'` (default) runs the update with no transition under reduced motion; `'play'` transitions anyway. */
739
+ reducedMotion?: 'skip' | 'play';
740
+ /** Types `:active-view-transition-type(name)` selects on, so forward and back can differ. */
741
+ types?: string[];
742
+ }
743
+
744
+ interface ViewTransitionHandle {
745
+ ready: Promise<void>;
746
+ finished: Promise<void>;
747
+ updateCallbackDone: Promise<void>;
748
+ skip(): void;
749
+ /** False when the browser had no API, or reduced motion skipped it. */
750
+ transitioned: boolean;
751
+ }
752
+ ```
753
+
754
+ **Four things to know:**
755
+
756
+ - **In React the update has to be flushed.** The browser takes its second screenshot the moment the
757
+ callback returns, and a `setState` has not rendered by then — so a hand-rolled version captures the old
758
+ state twice and nothing appears to move. `flushSync` inside the callback is the fix, and
759
+ `<Box.Theme viewTransition>` is it already done for the one change every app has.
760
+ - **A name has to be unique in the document while the transition runs.** Two elements sharing one is how a
761
+ transition silently does nothing. The prop is for the handful of names a layout has (`header`, `main`);
762
+ a name **per list item** is a rule per item that is never freed, so that one goes in
763
+ `props={{ style: { viewTransitionName: id } }}` — the exception `useAnchorPosition`'s anchor name and a
764
+ `Slider` thumb already take.
765
+ - **Reduced motion skips the transition and keeps the update.** A whole-page cross-fade is exactly the
766
+ motion the preference is about. `{ reducedMotion: 'play' }` is the opt-out, for a change that is
767
+ unreadable without one.
768
+ - **The handle is the same shape either way**, so there is one code path: with no API, `finished` is an
769
+ already-resolved promise, `skip()` a no-op and `transitioned` false. `ready` rejects when a transition is
770
+ skipped, which is not an error — branch on `finished`.
771
+
772
+ ---
773
+
645
774
  ## Pseudo-Classes, Breakpoints & Themes
646
775
 
647
776
  ```tsx
@@ -1119,24 +1248,41 @@ second argument for an element inside an `<svg>`. `<Icon>` is this hook plus tha
1119
1248
  ### Server-Side Rendering
1120
1249
 
1121
1250
  ```tsx
1122
- import { getStyles, resetStyles } from '@box-kite/react/ssg';
1251
+ import Box from '@box-kite/react';
1252
+ import { getStyleTag, resetStyles } from '@box-kite/react/ssg';
1253
+
1254
+ Box.configure({ classNames: 'stable' }); // in the server AND in the browser entry
1123
1255
 
1124
1256
  const html = renderToString(<App />); // any React server renderer
1125
- const cssString = getStyles(); // the CSS for what was just rendered
1126
- // <style id="box-kite-styles">{cssString}</style> in the document head
1257
+ const styleTag = getStyleTag(); // <style id="box-kite-styles" data-box-kite="…">…</style>, for the head
1127
1258
  resetStyles(); // reset before the next request
1128
1259
  ```
1129
1260
 
1130
- Or in one call, which injects the styles into the rendered `<head>` and resets afterwards:
1261
+ Or in one call, which injects the tag into the rendered `<head>` and resets afterwards:
1131
1262
 
1132
1263
  ```tsx
1133
1264
  import { renderToStaticMarkup } from '@box-kite/react/ssg';
1134
1265
 
1135
- const { html, styles } = renderToStaticMarkup(<App />);
1266
+ const { html, styles, styleTag } = renderToStaticMarkup(<App />);
1136
1267
  ```
1137
1268
 
1138
1269
  No DOM is needed: with no `document` in the process the engine collects CSS in memory. Sequential
1139
- requests are independent and identical markup gets identical class names.
1270
+ requests are independent and identical markup gets identical class names. `getStyles()` is the bare CSS.
1271
+
1272
+ **Adoption.** The `data-box-kite` attribute is a manifest — each rule's cascade position and a checksum
1273
+ of the classes it styles. On the first render the browser engine checks it against the sheet it parsed
1274
+ and takes the sheet over: every rule in it counts as generated, so hydration builds and inserts nothing
1275
+ for what the HTML already styled, and only rules the client adds afterwards are inserted, each in its
1276
+ cascade position. Two conditions:
1277
+
1278
+ | Condition | Why | If it does not hold |
1279
+ | --------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
1280
+ | `classNames: 'stable'` on both sides | the content-hashed names are how the browser recognises the server's rules | the sheet is left in place under no id and the engine writes its own after it |
1281
+ | the browser parsed every generated rule | an index is only trustworthy if nothing in between was dropped | same fallback |
1282
+
1283
+ Do **not** remove the server's `<style>` after hydration — it is the engine's own sheet now. Register
1284
+ the same `Box.extend()` props and `Box.keyframes()` sequences on both sides. `engine.getStyleTag()` is
1285
+ the same thing for `@box-kite/core`.
1140
1286
 
1141
1287
  ---
1142
1288
 
@@ -2139,7 +2285,7 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
2139
2285
  9. **Theme styles nest**: `theme={{ dark: { hover: { ... } } }}`
2140
2286
  10. **HTML attributes go in `props` prop**: `<Link props={{ href: '/about' }}>` not `<Link href>`. `data-*` and `aria-*` go there too — a `data-state` written at the top level typechecks and is then dropped
2141
2287
  11. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
2142
- 12. **Box is memoized** with `React.memo`
2288
+ 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
2143
2289
  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
2144
2290
  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
2145
2291
  15. **All sizing props use divider 4** — `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight` are NOT direct pixels. `height={10}` = 2.5rem = 40px
@@ -2219,34 +2365,36 @@ named ones carry the reason.
2219
2365
 
2220
2366
  ### GridDefinition
2221
2367
 
2222
- | Prop | Type | Default | Description |
2223
- | ------------------------- | ------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------- |
2224
- | `columns` | `ColumnType[]` | required | Column definitions |
2225
- | `rowKey` | `keyof TRow \| (row) => Key` | auto | Unique row identifier |
2226
- | `rowHeight` | `number` | `48` | Row height in px |
2227
- | `visibleRowsCount` | `number \| 'all'` | `10` | Visible rows. `'all'` disables virtualization |
2228
- | `showRowNumber` | `boolean \| { pinned?, width? }` | `false` | Row number column |
2229
- | `rowSelection` | `boolean \| { pinned? }` | `false` | Checkbox selection column |
2230
- | `rowDetail` | `RowDetailConfig` | — | Expandable detail panel (see below) |
2231
- | `treeData` | `TreeDataConfig` | — | Rows that hold rows: a `treegrid` with a chevron column (see below) |
2232
- | `groupBy` | `Key[]` | — | The columns the grid starts grouped by, outermost first. The column menu takes it from there |
2233
- | `groupDefaultExpanded` | `boolean \| number` | `false` | Which group rows start open: `true` for all, a number for how many levels down |
2234
- | `editable` | `boolean` | `false` | Whether every column can be edited. A column’s own `editable` takes priority |
2235
- | `onCellEdit` | `(edit) => void \| boolean \| string` | — | Judges an edit and is told about it: nothing accepts, a string refuses. May be async |
2236
- | `dataSource` | `DataSource` | — | The grid fetches its own rows a block at a time; `data` is not read at all |
2237
- | `pagination` | `{ totalCount, pageSize? }` | — | Server-side pagination. Bypasses client-side filtering |
2238
- | `topBar` / `bottomBar` | `boolean` | `false` | Show top/bottom bars |
2239
- | `title` / `topBarContent` | `ReactNode` | — | Top bar content |
2240
- | `globalFilter` | `boolean` | `false` | Enable global fuzzy search |
2241
- | `globalFilterKeys` | `(keyof TRow)[]` | all | Limit global filter columns |
2242
- | `sortable` / `resizable` | `boolean` | `true` | Enable sorting/resizing for all columns |
2243
- | `contextMenu` | `boolean \| ContextMenuConfig` | `true` | Control column header context menu. `false` hides it. Object: `{ sort?, pin?, group? }` |
2244
- | `resizerStyle` | `'visible' \| 'hover' \| 'hidden'` | `'visible'` | Resizer handle visibility. `'hover'`: shows on header cell hover. `'hidden'`: invisible but resize still works |
2245
- | `noDataComponent` | `ReactNode` | `'empty'` | Custom empty state |
2246
- | `footer` | `boolean \| { label? }` | `false` | A pinned row of grand totals over every column with an `aggregate`. `label` replaces the default `Total` |
2247
- | `export` | `boolean \| ExportConfig` | `false` | Export buttons in the top bar: `{ csv?, xlsx?, fileName? }`. The writers load on the first press |
2248
- | `tabNavigation` | `'none' \| 'cells'` | `'none'` | What Tab does. `'none'` is APG's single tab stop; `'cells'` is the spreadsheet reading |
2249
- | `rangeSelection` | `boolean` | `false` | A drag or Shift+arrow marks a block of cells, and `Ctrl+C`/`Ctrl+V` copy and paste it as TSV (see below) |
2368
+ | Prop | Type | Default | Description |
2369
+ | ------------------------- | ------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2370
+ | `columns` | `ColumnType[]` | required | Column definitions |
2371
+ | `rowKey` | `keyof TRow \| (row) => Key` | auto | Unique row identifier |
2372
+ | `rowHeight` | `number` | `48` | Row height in px |
2373
+ | `visibleRowsCount` | `number \| 'all'` | `10` | Visible rows. `'all'` disables virtualization |
2374
+ | `showRowNumber` | `boolean \| { pinned?, width? }` | `false` | Row number column |
2375
+ | `rowSelection` | `boolean \| { pinned? }` | `false` | Checkbox selection column. The header checkbox acts on the rows the filters leave on screen; a filter never drops a selection, and the bottom bar counts what it hides, with **Show selected** and **Clear selection** |
2376
+ | `rowDetail` | `RowDetailConfig` | — | Expandable detail panel (see below) |
2377
+ | `treeData` | `TreeDataConfig` | — | Rows that hold rows: a `treegrid` with a chevron column (see below) |
2378
+ | `groupBy` | `Key[]` | — | The columns the grid starts grouped by, outermost first. The column menu takes it from there |
2379
+ | `groupDefaultExpanded` | `boolean \| number` | `false` | Which group rows start open: `true` for all, a number for how many levels down |
2380
+ | `editable` | `boolean` | `false` | Whether every column can be edited. A column’s own `editable` takes priority |
2381
+ | `onCellEdit` | `(edit) => void \| boolean \| string` | — | Judges an edit and is told about it: nothing accepts, a string refuses. May be async |
2382
+ | `dataSource` | `DataSource` | — | The grid fetches its own rows a block at a time; `data` is not read at all |
2383
+ | `pagination` | `{ totalCount, pageSize? }` | — | Server-side pagination. Bypasses client-side filtering |
2384
+ | `topBar` / `bottomBar` | `boolean` | `false` | Show top/bottom bars |
2385
+ | `title` / `topBarContent` | `ReactNode` | — | Top bar content |
2386
+ | `globalFilter` | `boolean` | `false` | Enable global fuzzy search |
2387
+ | `globalFilterKeys` | `(keyof TRow)[]` | all | Limit global filter columns |
2388
+ | `sortable` / `resizable` | `boolean` | `true` | Enable sorting/resizing for all columns |
2389
+ | `contextMenu` | `boolean \| ContextMenuConfig` | `true` | Control column header context menu. `false` hides it. Object: `{ sort?, pin?, group? }` |
2390
+ | `resizerStyle` | `'visible' \| 'hover' \| 'hidden'` | `'visible'` | Resizer handle visibility. `'hover'`: shows on header cell hover. `'hidden'`: invisible but resize still works |
2391
+ | `headerHover` | `'static' \| 'sliding'` | `'static'` | Header hover: an inset pill per sortable cell, or one pill that flows from cell to cell. Whole cell sorts on press |
2392
+ | `localeText` | `Partial<DataGridLocaleText>` | English | Every word the grid writes — labels, accessible names, announcements, messages. Values with a number or a name in them are functions; spread `DATA_GRID_LOCALE_TEXT` to start a translation |
2393
+ | `noDataComponent` | `ReactNode` | `'empty'` | Custom empty state |
2394
+ | `footer` | `boolean \| { label? }` | `false` | A pinned row of grand totals over every column with an `aggregate`. `label` replaces the default `Total` |
2395
+ | `export` | `boolean \| ExportConfig` | `false` | Export buttons in the top bar: `{ csv?, xlsx?, fileName? }`. The writers load on the first press |
2396
+ | `tabNavigation` | `'none' \| 'cells'` | `'none'` | What Tab does. `'none'` is APG's single tab stop; `'cells'` is the spreadsheet reading |
2397
+ | `rangeSelection` | `boolean` | `false` | A drag or Shift+arrow marks a block of cells, and `Ctrl+C`/`Ctrl+V` copy and paste it as TSV (see below) |
2250
2398
 
2251
2399
  ### ColumnType
2252
2400
 
@@ -2258,7 +2406,7 @@ named ones carry the reason.
2258
2406
  | `align` | `'start' \| 'end' \| 'center' \| 'left' \| 'right'` | `'left'` | Cell alignment. `start`/`end` follow the reading order; `left`/`right` stay on the screen side they name |
2259
2407
  | `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 |
2260
2408
  | `columns` | `ColumnType[]` | — | Nested columns (grouped header) |
2261
- | `Cell` | `({ cell }) => ReactNode` | — | Custom renderer. `cell`: `{ value, row, column, grid }` |
2409
+ | `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 |
2262
2410
  | `sortable` / `resizable` | `boolean` | inherits | Override grid-level setting |
2263
2411
  | `flexible` | `boolean` | `true` | Participate in flex width distribution |
2264
2412
  | `filterable` | `boolean \| FilterConfig` | — | `true` (text), `{ type: 'number', min?, max? }`, `{ type: 'multiselect', options? }` |
@@ -2641,52 +2789,54 @@ stops, as they were before A7.
2641
2789
 
2642
2790
  ### Component Style Tree
2643
2791
 
2644
- | Component Name | Description | Variants |
2645
- | --------------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2646
- | `datagrid` | Root container | — |
2647
- | `datagrid.content` | Scroll container for header + body | — |
2648
- | `datagrid.topBar` | Top bar (title, filters, column groups) | — |
2649
- | `datagrid.topBar.globalFilter` | Global search wrapper | — |
2650
- | `datagrid.topBar.globalFilter.stats` | Filtered rows count badge | — |
2651
- | `datagrid.topBar.columnGroups` | Column group chips container | — |
2652
- | `datagrid.topBar.columnGroups.icon` | Column group icon | — |
2653
- | `datagrid.topBar.columnGroups.separator` | Separator between groups | — |
2654
- | `datagrid.topBar.columnGroups.item` | Column group chip | — |
2655
- | `datagrid.topBar.columnGroups.item.icon` | Remove icon on chip | — |
2656
- | `datagrid.topBar.columnVisibility` | Column visibility dropdown | — |
2657
- | `datagrid.topBar.columnVisibility.badge` | Hidden columns count badge | — |
2658
- | `datagrid.filter.cell` | Filter row cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned` |
2659
- | `datagrid.filter.cell.input` | Filter input container (text/number/multiselect) | — |
2660
- | `datagrid.header` | Header grid container (sticky) | — |
2661
- | `datagrid.header.cell` | Header cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned`, `isSortable`, `isRowSelection`, `isRowNumber`, `isFirstLeaf`, `isLastLeaf`, `isEmptyCell` |
2662
- | `datagrid.header.cell.contextMenu` | Column context menu button | — |
2663
- | `datagrid.header.cell.contextMenu.icon` | Context menu icon | — |
2664
- | `datagrid.header.cell.contextMenu.tooltip` | Context menu popup | — |
2665
- | `datagrid.header.cell.contextMenu.tooltip.item` | Context menu action | — |
2666
- | `datagrid.header.cell.contextMenu.tooltip.item.icon` | Action icon | — |
2667
- | `datagrid.header.cell.contextMenu.tooltip.item.separator` | Menu separator line | — |
2668
- | `datagrid.header.cell.resizer` | Column resize handle | — |
2669
- | `datagrid.body` | Body grid container (virtualized rows) | — |
2670
- | `datagrid.body.cell` | Body cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned`, `isRowNumber`, `isRowSelection`, `isRowSelected`, `isFirstLeaf`, `isLastLeaf`, `isEmptyCell`, `isEditing`, `isInvalid` |
2671
- | `datagrid.body.cell.text` | Default cell text renderer | — |
2672
- | `datagrid.body.cell.editor` | The open cell editor | `isPending` |
2673
- | `datagrid.body.cell.error` | What a refused value says, in the top layer | — |
2674
- | `datagrid.body.cell.rowDetail` | Row detail expand/collapse button | — |
2675
- | `datagrid.body.cell.tree` | Tree cell: the indent, the chevron and the value | — |
2676
- | `datagrid.body.cell.tree.toggle` | Tree expand/collapse chevron | `isExpanded` |
2677
- | `datagrid.body.cell.tree.spacer` | What a leaf puts where its chevron would be | — |
2678
- | `datagrid.body.row` | Data row (display: contents) | — |
2679
- | `datagrid.body.groupRow` | Group row (display: contents) | — |
2680
- | `datagrid.body.groupRow.expandButton` | Group expand/collapse button | — |
2681
- | `datagrid.body.detailRow` | Expanded detail row | — |
2682
- | `datagrid.body.empty` | Empty state container | — |
2683
- | `datagrid.emptyColumns` | No columns selected placeholder | — |
2684
- | `datagrid.bottomBar` | Bottom bar (row count, pagination) | — |
2685
- | `datagrid.bottomBar.info` | Status text ("Rows: ...", "Selected: ...") | — |
2686
- | `datagrid.bottomBar.clearFilters` | "Clear filters" link | — |
2687
- | `datagrid.bottomBar.pagination` | Pagination controls wrapper | — |
2688
- | `datagrid.bottomBar.pagination.button` | Pagination nav button | — |
2689
- | `datagrid.bottomBar.pagination.info` | Page info text ("1 of 5") | — |
2792
+ | Component Name | Description | Variants |
2793
+ | --------------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2794
+ | `datagrid` | Root container | — |
2795
+ | `datagrid.content` | Scroll container for header + body | — |
2796
+ | `datagrid.topBar` | Top bar (title, filters, column groups) | — |
2797
+ | `datagrid.topBar.globalFilter` | Global search wrapper | — |
2798
+ | `datagrid.topBar.globalFilter.stats` | Filtered rows count badge (only without a bottom bar) | — |
2799
+ | `datagrid.topBar.columnGroups` | Column group chips container | — |
2800
+ | `datagrid.topBar.columnGroups.icon` | Column group icon | — |
2801
+ | `datagrid.topBar.columnGroups.separator` | Separator between groups | — |
2802
+ | `datagrid.topBar.columnGroups.item` | Column group chip | — |
2803
+ | `datagrid.topBar.columnGroups.item.icon` | Remove icon on chip | — |
2804
+ | `datagrid.topBar.columnVisibility` | Column visibility dropdown | — |
2805
+ | `datagrid.topBar.columnVisibility.badge` | Hidden columns count badge | — |
2806
+ | `datagrid.filter.cell` | Filter row cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned` |
2807
+ | `datagrid.filter.cell.input` | Filter input container (text/number/multiselect) | — |
2808
+ | `datagrid.header` | Header grid container (sticky) | — |
2809
+ | `datagrid.header.cell` | Header cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned`, `isSortable`, `isRowSelection`, `isRowNumber`, `isFirstLeaf`, `isLastLeaf`, `isEmptyCell` |
2810
+ | `datagrid.header.cell.contextMenu` | Column context menu button | — |
2811
+ | `datagrid.header.cell.contextMenu.icon` | Context menu icon | — |
2812
+ | `datagrid.header.cell.contextMenu.tooltip` | Context menu popup | — |
2813
+ | `datagrid.header.cell.contextMenu.tooltip.item` | Context menu action | — |
2814
+ | `datagrid.header.cell.contextMenu.tooltip.item.icon` | Action icon | — |
2815
+ | `datagrid.header.cell.contextMenu.tooltip.item.separator` | Menu separator line | — |
2816
+ | `datagrid.header.cell.resizer` | Column resize handle | — |
2817
+ | `datagrid.body` | Body grid container (virtualized rows) | — |
2818
+ | `datagrid.body.cell` | Body cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned`, `isRowNumber`, `isRowSelection`, `isRowSelected`, `isFirstLeaf`, `isLastLeaf`, `isEmptyCell`, `isEditing`, `isInvalid` |
2819
+ | `datagrid.body.cell.text` | Default cell text renderer | — |
2820
+ | `datagrid.body.cell.editor` | The open cell editor | `isPending` |
2821
+ | `datagrid.body.cell.error` | What a refused value says, in the top layer | — |
2822
+ | `datagrid.body.cell.rowDetail` | Row detail expand/collapse button | — |
2823
+ | `datagrid.body.cell.tree` | Tree cell: the indent, the chevron and the value | — |
2824
+ | `datagrid.body.cell.tree.toggle` | Tree expand/collapse chevron | `isExpanded` |
2825
+ | `datagrid.body.cell.tree.spacer` | What a leaf puts where its chevron would be | — |
2826
+ | `datagrid.body.row` | Data row (display: contents) | — |
2827
+ | `datagrid.body.groupRow` | Group row (display: contents) | — |
2828
+ | `datagrid.body.groupRow.expandButton` | Group expand/collapse button | — |
2829
+ | `datagrid.body.detailRow` | Expanded detail row | — |
2830
+ | `datagrid.body.empty` | Empty state container | — |
2831
+ | `datagrid.emptyColumns` | No columns selected placeholder | — |
2832
+ | `datagrid.bottomBar` | Bottom bar (row count, pagination) | — |
2833
+ | `datagrid.bottomBar.info` | Status text ("Rows: ...", "Selected: ...") | — |
2834
+ | `datagrid.bottomBar.clearFilters` | "Clear filters" button | — |
2835
+ | `datagrid.bottomBar.selection` | The selection count and its buttons, at the bar's far end | — |
2836
+ | `datagrid.bottomBar.action` | "Show selected" and "Clear selection" | `pressed` (Show selected while it is on) |
2837
+ | `datagrid.bottomBar.pagination` | Pagination controls wrapper | — |
2838
+ | `datagrid.bottomBar.pagination.button` | Pagination nav button | — |
2839
+ | `datagrid.bottomBar.pagination.info` | Page info text ("1 of 5") | — |
2690
2840
 
2691
2841
  ---
2692
2842
 
@@ -2896,6 +3046,19 @@ Also accepts: `data` (TRow[]), `value`/`defaultValue`, `label`/`labelProps`, `mu
2896
3046
  3. **CSS variables**: In `:root` rules
2897
3047
  4. **Theme issues**: Ensure `<Box.Theme>` wraps your app
2898
3048
  5. **Layer theming**: a tooltip or dropdown popup stays where it was declared and inherits the theme; only the no-Popover-API fallback uses the `#box-kite-portal` container
3049
+ 6. **Read the console first**: outside a production build the engine prints one `[box-kite]` warning per mistake that would otherwise be silent, each naming the fix:
3050
+
3051
+ | You wrote | The warning says |
3052
+ | -------------------------- | -------------------------------------------------------------------------- |
3053
+ | `bgColor="bleu-500"` | wrote no CSS … Did you mean `"blue-500"`? |
3054
+ | `opacity={50}` | wrote no CSS … It takes one of 0, 0.1, … 1 |
3055
+ | `<Link href="/about">` | `href` is an HTML attribute … write `props={{ href: ... }}` |
3056
+ | `fontSize={4}` | is 4px text … For 16px write `fontSize={16}` |
3057
+ | `lineHeight={1.5}` | is a 1.5px line … `lineHeight="font-size"`, or `css={{ lineHeight: 1.5 }}` |
3058
+ | `transitionDuration={0.3}` | is 0.3ms … For 0.3s write `transitionDuration={300}` |
3059
+ | `brightness={1.1}` | is 1.1% … For 110% write `brightness={110}` |
3060
+
3061
+ Once per value, and `Box.configure({ warnings: false })` silences them. In production a rejected value is dropped with nothing said, which is why `check_styles` (`@box-kite/mcp`) exists
2899
3062
 
2900
3063
  ---
2901
3064
 
@@ -3204,7 +3367,7 @@ allowed.tokens; // the colours, the @keyframes names and the style-tree nodes a
3204
3367
  `DataGrid`'s `def` (required — the columns and the grid-wide flags) and `data`, `DashboardGrid`'s
3205
3368
  `layout`/`defaultLayout`/`columns`, `Widget`'s `empty`, `ChartContainer`'s `series`. A column's nested
3206
3369
  `columns`, and `dataSource`/`onCellEdit`/`rowDetail`/`treeData`, stay out — they are the app's to pass.
3207
- - **The allow-list is the app's.** `catalog()` with no options is all 78 components and all 221 props —
3370
+ - **The allow-list is the app's.** `catalog()` with no options is all 78 components and all 235 props —
3208
3371
  ~4.6 MB serialized. `include`, `exclude` and `styleProps` are how an app narrows it.
3209
3372
 
3210
3373
  Full reference: `docs/catalog.md`.