@box-kite/react 2.2.1 → 2.2.3

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.
@@ -80,7 +80,7 @@ globs: '**/*.{ts,tsx,jsx}'
80
80
 
81
81
  46. **A scroll-driven animation is a timeline prop, and it costs no JavaScript at all.** `animationTimeline="scroll()"` runs an animation off the nearest scrollport's progress and `"view()"` off this element's own pass across it — no listener, no rAF, no state, and the browser drives it on the compositor. Where the scroller is not an ancestor, name one: `scrollTimeline="page block"` on the scroller (or `viewTimeline="card block"` on the subject), `animationTimeline="page"` on the animated element, and `timelineScope="page"` on their common ancestor when the two are not nested — the `--` on a name is optional and added for you, the axis is `block`/`inline`/`x`/`y` (logical, so it mirrors), and the longhands `scrollTimelineName`/`Axis` and `viewTimelineName`/`Axis` exist to override half a shorthand. **`animationRange` is which part of the pass the animation occupies**: `cover`, `contain`, `entry`, `exit`, `entry-crossing`, `exit-crossing`, each taking an offset — `animationRange="entry 0% entry 50%"`, or the `animationRangeStart`/`animationRangeEnd` pair — and `viewTimelineInset` shrinks the scrollport the pass is measured against, which is how a reveal fires before the element really reaches the edge. **Three traps.** The CSS `animation` shorthand **resets `animation-timeline`**, so a timeline declared before it is silently undone; the registry's order settles that for the props, but not for an `animation` written inside `css`. A scroll-driven animation has **no duration**, so `--transitionTime` cannot zero it and it is the one motion here that does _not_ stop itself under `prefers-reduced-motion` — `motionReduce={{ animation: 'none' }}` is the opt-in, and it is not optional. And where the browser has none (Safari, and Firefox behind a flag) the declaration is dropped rather than the animation: it runs on the **document timeline** over whatever duration it was given, so make the end state the resting state and a missing timeline degrades to "already arrived" instead of a loop.
82
82
 
83
- 47. **A view transition is two props and one call.** `Box.viewTransition(() => applyChange())` runs a DOM change inside `document.startViewTransition` where the browser has one and plainly where it has not — the same `ready`/`finished`/`updateCallbackDone` promises and `skip()` either way, so a caller has one code path rather than two — and `viewTransitionName="header"` is what makes an element animate _from where it was to where it is_ instead of being cross-faded along with the page. `viewTransitionClass="card"` groups several names, so `::view-transition-group(.card)` styles them at once. **In React the update has to be flushed**: the browser screenshots the page the moment the callback returns and a `setState` has not rendered by then, so a hand-rolled version captures the old state twice and nothing appears to move — write `Box.viewTransition(() => flushSync(() => setTheme('dark')))`, or use **`<Box.Theme viewTransition>`**, which is that call for the one change every app has. **A name has to be unique in the document while the transition runs**, so a list wants a name per item — and a class per item is a rule this library would never free, which is why that one goes in `props={{ style: { viewTransitionName: id } }}`, the exception `useAnchorPosition`'s anchor name and a `Slider` thumb already take. Reduced motion **skips** the transition by default and keeps the update, a whole-page cross-fade being exactly the motion the preference is about; `{ reducedMotion: 'play' }` is the opt-out, and `{ types: ['forward'] }` is what `:active-view-transition-type()` selects on.
83
+ 47. **A view transition is two props and one call.** `Box.viewTransition(() => applyChange())` runs a DOM change inside `document.startViewTransition` where the browser has one and plainly where it has not — the same `ready`/`finished`/`updateCallbackDone` promises and `skip()` either way, so a caller has one code path rather than two — and `viewTransitionName="header"` is what makes an element animate _from where it was to where it is_ instead of being cross-faded along with the page. `viewTransitionClass="card"` groups several names, so `::view-transition-group(.card)` styles them at once. **In React the update has to be flushed**: the browser screenshots the page the moment the callback returns and a `setState` has not rendered by then, so a hand-rolled version captures the old state twice and nothing appears to move — write `Box.viewTransition(() => flushSync(() => setTheme('dark')))`, or use **`<Box.Theme viewTransition>`**, which is that call for the one change every app has — `viewTransition="reveal"` grows the new theme out of the control that was pressed as a circle instead of cross-fading. **A theme change pauses every CSS transition** until it has painted (and until the view transition has finished), because `color` is inherited and the base class transitions `all`, so each element would otherwise start one of its own; the view transition is the animation, and it costs the same on any size of page. **A name has to be unique in the document while the transition runs**, so a list wants a name per item — and a class per item is a rule this library would never free, which is why that one goes in `props={{ style: { viewTransitionName: id } }}`, the exception `useAnchorPosition`'s anchor name and a `Slider` thumb already take. Reduced motion **skips** the transition by default and keeps the update, a whole-page cross-fade being exactly the motion the preference is about; `{ reducedMotion: 'play' }` is the opt-out, and `{ types: ['forward'] }` is what `:active-view-transition-type()` selects on.
84
84
 
85
85
  48. **In development the console is part of the answer: read it.** Three mistakes write nothing, or the wrong thing, and say so in one `[box-kite]` warning that names what rendered and the line to write instead. **A value a prop does not take** — `bgColor="bleu-500"` → `wrote no CSS … Did you mean "blue-500"?`, `opacity={50}` → `It takes one of 0, 0.1, …`. **An HTML attribute at the top level** — `href`, `onClick`, `data-*`, `aria-*` → `write props={{ href: ... }}`. **A number in a unit it almost certainly did not mean**, which is valid CSS and so the one mistake nothing else catches: `fontSize={4}` is 4px text (`For 16px write fontSize={16}`), `lineHeight={1.5}` a 1.5px line, `transitionDuration={0.3}` 0.3ms, `brightness={1.1}` 1.1%. Each prints once per value, in any environment whose `NODE_ENV` is not `production` — a test run included, which is the cheapest place to see them — and never in a production build, where a rejected value is still dropped with nothing said. `Box.configure({ warnings: false })` turns them off; `check_styles` in `@box-kite/mcp` gives the same three verdicts before anything renders.
86
86
 
@@ -153,7 +153,7 @@ Detect the package manager from the lock file rather than asking:
153
153
 
154
154
  46. **A scroll-driven animation is a timeline prop, and it costs no JavaScript at all.** `animationTimeline="scroll()"` runs an animation off the nearest scrollport's progress and `"view()"` off this element's own pass across it — no listener, no rAF, no state, and the browser drives it on the compositor. Where the scroller is not an ancestor, name one: `scrollTimeline="page block"` on the scroller (or `viewTimeline="card block"` on the subject), `animationTimeline="page"` on the animated element, and `timelineScope="page"` on their common ancestor when the two are not nested — the `--` on a name is optional and added for you, the axis is `block`/`inline`/`x`/`y` (logical, so it mirrors), and the longhands `scrollTimelineName`/`Axis` and `viewTimelineName`/`Axis` exist to override half a shorthand. **`animationRange` is which part of the pass the animation occupies**: `cover`, `contain`, `entry`, `exit`, `entry-crossing`, `exit-crossing`, each taking an offset — `animationRange="entry 0% entry 50%"`, or the `animationRangeStart`/`animationRangeEnd` pair — and `viewTimelineInset` shrinks the scrollport the pass is measured against, which is how a reveal fires before the element really reaches the edge. **Three traps.** The CSS `animation` shorthand **resets `animation-timeline`**, so a timeline declared before it is silently undone; the registry's order settles that for the props, but not for an `animation` written inside `css`. A scroll-driven animation has **no duration**, so `--transitionTime` cannot zero it and it is the one motion here that does _not_ stop itself under `prefers-reduced-motion` — `motionReduce={{ animation: 'none' }}` is the opt-in, and it is not optional. And where the browser has none (Safari, and Firefox behind a flag) the declaration is dropped rather than the animation: it runs on the **document timeline** over whatever duration it was given, so make the end state the resting state and a missing timeline degrades to "already arrived" instead of a loop.
155
155
 
156
- 47. **A view transition is two props and one call.** `Box.viewTransition(() => applyChange())` runs a DOM change inside `document.startViewTransition` where the browser has one and plainly where it has not — the same `ready`/`finished`/`updateCallbackDone` promises and `skip()` either way, so a caller has one code path rather than two — and `viewTransitionName="header"` is what makes an element animate _from where it was to where it is_ instead of being cross-faded along with the page. `viewTransitionClass="card"` groups several names, so `::view-transition-group(.card)` styles them at once. **In React the update has to be flushed**: the browser screenshots the page the moment the callback returns and a `setState` has not rendered by then, so a hand-rolled version captures the old state twice and nothing appears to move — write `Box.viewTransition(() => flushSync(() => setTheme('dark')))`, or use **`<Box.Theme viewTransition>`**, which is that call for the one change every app has. **A name has to be unique in the document while the transition runs**, so a list wants a name per item — and a class per item is a rule this library would never free, which is why that one goes in `props={{ style: { viewTransitionName: id } }}`, the exception `useAnchorPosition`'s anchor name and a `Slider` thumb already take. Reduced motion **skips** the transition by default and keeps the update, a whole-page cross-fade being exactly the motion the preference is about; `{ reducedMotion: 'play' }` is the opt-out, and `{ types: ['forward'] }` is what `:active-view-transition-type()` selects on.
156
+ 47. **A view transition is two props and one call.** `Box.viewTransition(() => applyChange())` runs a DOM change inside `document.startViewTransition` where the browser has one and plainly where it has not — the same `ready`/`finished`/`updateCallbackDone` promises and `skip()` either way, so a caller has one code path rather than two — and `viewTransitionName="header"` is what makes an element animate _from where it was to where it is_ instead of being cross-faded along with the page. `viewTransitionClass="card"` groups several names, so `::view-transition-group(.card)` styles them at once. **In React the update has to be flushed**: the browser screenshots the page the moment the callback returns and a `setState` has not rendered by then, so a hand-rolled version captures the old state twice and nothing appears to move — write `Box.viewTransition(() => flushSync(() => setTheme('dark')))`, or use **`<Box.Theme viewTransition>`**, which is that call for the one change every app has — `viewTransition="reveal"` grows the new theme out of the control that was pressed as a circle instead of cross-fading. **A theme change pauses every CSS transition** until it has painted (and until the view transition has finished), because `color` is inherited and the base class transitions `all`, so each element would otherwise start one of its own; the view transition is the animation, and it costs the same on any size of page. **A name has to be unique in the document while the transition runs**, so a list wants a name per item — and a class per item is a rule this library would never free, which is why that one goes in `props={{ style: { viewTransitionName: id } }}`, the exception `useAnchorPosition`'s anchor name and a `Slider` thumb already take. Reduced motion **skips** the transition by default and keeps the update, a whole-page cross-fade being exactly the motion the preference is about; `{ reducedMotion: 'play' }` is the opt-out, and `{ types: ['forward'] }` is what `:active-view-transition-type()` selects on.
157
157
 
158
158
  48. **In development the console is part of the answer: read it.** Three mistakes write nothing, or the wrong thing, and say so in one `[box-kite]` warning that names what rendered and the line to write instead. **A value a prop does not take** — `bgColor="bleu-500"` → `wrote no CSS … Did you mean "blue-500"?`, `opacity={50}` → `It takes one of 0, 0.1, …`. **An HTML attribute at the top level** — `href`, `onClick`, `data-*`, `aria-*` → `write props={{ href: ... }}`. **A number in a unit it almost certainly did not mean**, which is valid CSS and so the one mistake nothing else catches: `fontSize={4}` is 4px text (`For 16px write fontSize={16}`), `lineHeight={1.5}` a 1.5px line, `transitionDuration={0.3}` 0.3ms, `brightness={1.1}` 1.1%. Each prints once per value, in any environment whose `NODE_ENV` is not `production` — a test run included, which is the cheapest place to see them — and never in a production build, where a rejected value is still dropped with nothing said. `Box.configure({ warnings: false })` turns them off; `check_styles` in `@box-kite/mcp` gives the same three verdicts before anything renders.
159
159
 
@@ -237,4 +237,4 @@ import DataGrid from '@box-kite/react/components/dataGrid';
237
237
 
238
238
  **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. Space on any cell selects its row, and on the header cell the select-all box sits in it toggles every row the filters leave on screen. Every cell carries the **current-cell mark**, which survives the grid losing focus so `Ctrl+C` has something to act on; with `def.rangeSelection` the cells of a marked block carry `aria-selected` and the grid is `aria-multiselectable`, while a lone current cell carries neither. 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. Every widget a cell holds is out of the page tab sequence, so the grid is one tab stop: Enter or F2 steps in, Tab walks the widgets **within that cell** and wraps, and Escape is the way out.
239
239
 
240
- **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`).
240
+ **Style tree**: `datagrid` > `content`, `topBar` > (`globalFilter` > `stats`, `columnGroups` > `icon`|`separator`|`item` > `icon`, `columnVisibility` > `badge`|`panel`|`search`|`searchIcon`|`searchInput`|`toolbar`|`count`|`action`|`list`|`section`|`group`|`item`|`empty`), `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`).
@@ -107,7 +107,8 @@ while the transition runs) and `viewTransitionClass` (a shared name, so `::view-
107
107
  `Box.viewTransition(update, { reducedMotion, types })` around the change itself: it feature-detects, hands back the same
108
108
  `ready`/`finished`/`updateCallbackDone`/`skip()` handle whether or not a transition ran, and **skips under reduced motion while still
109
109
  applying the update**. In React the update must be flushed inside the callback — `Box.viewTransition(() => flushSync(() => setTab(next)))` —
110
- or the screenshot catches the old state twice; `<Box.Theme viewTransition>` is that already done for a theme switch. A name _per list item_
110
+ or the screenshot catches the old state twice; `<Box.Theme viewTransition>` is that already done for a theme switch (`viewTransition="reveal"`
111
+ grows the new theme out of the pressed control instead), and a theme switch pauses every CSS transition while it is applied. A name _per list item_
111
112
  is a rule per item that is never freed, so that one belongs in `props={{ style: { viewTransitionName: id } }}`.
112
113
 
113
114
  **Gradients**: `bgGradient` — a gradient as a _value_, written as a record, so its stops are palette tokens and it is
@@ -88,7 +88,7 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
88
88
 
89
89
  46. **A scroll-driven animation is a timeline prop, and it costs no JavaScript at all.** `animationTimeline="scroll()"` runs an animation off the nearest scrollport's progress and `"view()"` off this element's own pass across it — no listener, no rAF, no state, and the browser drives it on the compositor. Where the scroller is not an ancestor, name one: `scrollTimeline="page block"` on the scroller (or `viewTimeline="card block"` on the subject), `animationTimeline="page"` on the animated element, and `timelineScope="page"` on their common ancestor when the two are not nested — the `--` on a name is optional and added for you, the axis is `block`/`inline`/`x`/`y` (logical, so it mirrors), and the longhands `scrollTimelineName`/`Axis` and `viewTimelineName`/`Axis` exist to override half a shorthand. **`animationRange` is which part of the pass the animation occupies**: `cover`, `contain`, `entry`, `exit`, `entry-crossing`, `exit-crossing`, each taking an offset — `animationRange="entry 0% entry 50%"`, or the `animationRangeStart`/`animationRangeEnd` pair — and `viewTimelineInset` shrinks the scrollport the pass is measured against, which is how a reveal fires before the element really reaches the edge. **Three traps.** The CSS `animation` shorthand **resets `animation-timeline`**, so a timeline declared before it is silently undone; the registry's order settles that for the props, but not for an `animation` written inside `css`. A scroll-driven animation has **no duration**, so `--transitionTime` cannot zero it and it is the one motion here that does _not_ stop itself under `prefers-reduced-motion` — `motionReduce={{ animation: 'none' }}` is the opt-in, and it is not optional. And where the browser has none (Safari, and Firefox behind a flag) the declaration is dropped rather than the animation: it runs on the **document timeline** over whatever duration it was given, so make the end state the resting state and a missing timeline degrades to "already arrived" instead of a loop.
90
90
 
91
- 47. **A view transition is two props and one call.** `Box.viewTransition(() => applyChange())` runs a DOM change inside `document.startViewTransition` where the browser has one and plainly where it has not — the same `ready`/`finished`/`updateCallbackDone` promises and `skip()` either way, so a caller has one code path rather than two — and `viewTransitionName="header"` is what makes an element animate _from where it was to where it is_ instead of being cross-faded along with the page. `viewTransitionClass="card"` groups several names, so `::view-transition-group(.card)` styles them at once. **In React the update has to be flushed**: the browser screenshots the page the moment the callback returns and a `setState` has not rendered by then, so a hand-rolled version captures the old state twice and nothing appears to move — write `Box.viewTransition(() => flushSync(() => setTheme('dark')))`, or use **`<Box.Theme viewTransition>`**, which is that call for the one change every app has. **A name has to be unique in the document while the transition runs**, so a list wants a name per item — and a class per item is a rule this library would never free, which is why that one goes in `props={{ style: { viewTransitionName: id } }}`, the exception `useAnchorPosition`'s anchor name and a `Slider` thumb already take. Reduced motion **skips** the transition by default and keeps the update, a whole-page cross-fade being exactly the motion the preference is about; `{ reducedMotion: 'play' }` is the opt-out, and `{ types: ['forward'] }` is what `:active-view-transition-type()` selects on.
91
+ 47. **A view transition is two props and one call.** `Box.viewTransition(() => applyChange())` runs a DOM change inside `document.startViewTransition` where the browser has one and plainly where it has not — the same `ready`/`finished`/`updateCallbackDone` promises and `skip()` either way, so a caller has one code path rather than two — and `viewTransitionName="header"` is what makes an element animate _from where it was to where it is_ instead of being cross-faded along with the page. `viewTransitionClass="card"` groups several names, so `::view-transition-group(.card)` styles them at once. **In React the update has to be flushed**: the browser screenshots the page the moment the callback returns and a `setState` has not rendered by then, so a hand-rolled version captures the old state twice and nothing appears to move — write `Box.viewTransition(() => flushSync(() => setTheme('dark')))`, or use **`<Box.Theme viewTransition>`**, which is that call for the one change every app has — `viewTransition="reveal"` grows the new theme out of the control that was pressed as a circle instead of cross-fading. **A theme change pauses every CSS transition** until it has painted (and until the view transition has finished), because `color` is inherited and the base class transitions `all`, so each element would otherwise start one of its own; the view transition is the animation, and it costs the same on any size of page. **A name has to be unique in the document while the transition runs**, so a list wants a name per item — and a class per item is a rule this library would never free, which is why that one goes in `props={{ style: { viewTransitionName: id } }}`, the exception `useAnchorPosition`'s anchor name and a `Slider` thumb already take. Reduced motion **skips** the transition by default and keeps the update, a whole-page cross-fade being exactly the motion the preference is about; `{ reducedMotion: 'play' }` is the opt-out, and `{ types: ['forward'] }` is what `:active-view-transition-type()` selects on.
92
92
 
93
93
  48. **In development the console is part of the answer: read it.** Three mistakes write nothing, or the wrong thing, and say so in one `[box-kite]` warning that names what rendered and the line to write instead. **A value a prop does not take** — `bgColor="bleu-500"` → `wrote no CSS … Did you mean "blue-500"?`, `opacity={50}` → `It takes one of 0, 0.1, …`. **An HTML attribute at the top level** — `href`, `onClick`, `data-*`, `aria-*` → `write props={{ href: ... }}`. **A number in a unit it almost certainly did not mean**, which is valid CSS and so the one mistake nothing else catches: `fontSize={4}` is 4px text (`For 16px write fontSize={16}`), `lineHeight={1.5}` a 1.5px line, `transitionDuration={0.3}` 0.3ms, `brightness={1.1}` 1.1%. Each prints once per value, in any environment whose `NODE_ENV` is not `production` — a test run included, which is the cheapest place to see them — and never in a production build, where a rejected value is still dropped with nothing said. `Box.configure({ warnings: false })` turns them off; `check_styles` in `@box-kite/mcp` gives the same three verdicts before anything renders.
94
94
 
package/AGENTS.md CHANGED
@@ -120,7 +120,7 @@ Every value there is a prop, and every prop value becomes one class the engine i
120
120
 
121
121
  46. **A scroll-driven animation is a timeline prop, and it costs no JavaScript at all.** `animationTimeline="scroll()"` runs an animation off the nearest scrollport's progress and `"view()"` off this element's own pass across it — no listener, no rAF, no state, and the browser drives it on the compositor. Where the scroller is not an ancestor, name one: `scrollTimeline="page block"` on the scroller (or `viewTimeline="card block"` on the subject), `animationTimeline="page"` on the animated element, and `timelineScope="page"` on their common ancestor when the two are not nested — the `--` on a name is optional and added for you, the axis is `block`/`inline`/`x`/`y` (logical, so it mirrors), and the longhands `scrollTimelineName`/`Axis` and `viewTimelineName`/`Axis` exist to override half a shorthand. **`animationRange` is which part of the pass the animation occupies**: `cover`, `contain`, `entry`, `exit`, `entry-crossing`, `exit-crossing`, each taking an offset — `animationRange="entry 0% entry 50%"`, or the `animationRangeStart`/`animationRangeEnd` pair — and `viewTimelineInset` shrinks the scrollport the pass is measured against, which is how a reveal fires before the element really reaches the edge. **Three traps.** The CSS `animation` shorthand **resets `animation-timeline`**, so a timeline declared before it is silently undone; the registry's order settles that for the props, but not for an `animation` written inside `css`. A scroll-driven animation has **no duration**, so `--transitionTime` cannot zero it and it is the one motion here that does _not_ stop itself under `prefers-reduced-motion` — `motionReduce={{ animation: 'none' }}` is the opt-in, and it is not optional. And where the browser has none (Safari, and Firefox behind a flag) the declaration is dropped rather than the animation: it runs on the **document timeline** over whatever duration it was given, so make the end state the resting state and a missing timeline degrades to "already arrived" instead of a loop.
122
122
 
123
- 47. **A view transition is two props and one call.** `Box.viewTransition(() => applyChange())` runs a DOM change inside `document.startViewTransition` where the browser has one and plainly where it has not — the same `ready`/`finished`/`updateCallbackDone` promises and `skip()` either way, so a caller has one code path rather than two — and `viewTransitionName="header"` is what makes an element animate _from where it was to where it is_ instead of being cross-faded along with the page. `viewTransitionClass="card"` groups several names, so `::view-transition-group(.card)` styles them at once. **In React the update has to be flushed**: the browser screenshots the page the moment the callback returns and a `setState` has not rendered by then, so a hand-rolled version captures the old state twice and nothing appears to move — write `Box.viewTransition(() => flushSync(() => setTheme('dark')))`, or use **`<Box.Theme viewTransition>`**, which is that call for the one change every app has. **A name has to be unique in the document while the transition runs**, so a list wants a name per item — and a class per item is a rule this library would never free, which is why that one goes in `props={{ style: { viewTransitionName: id } }}`, the exception `useAnchorPosition`'s anchor name and a `Slider` thumb already take. Reduced motion **skips** the transition by default and keeps the update, a whole-page cross-fade being exactly the motion the preference is about; `{ reducedMotion: 'play' }` is the opt-out, and `{ types: ['forward'] }` is what `:active-view-transition-type()` selects on.
123
+ 47. **A view transition is two props and one call.** `Box.viewTransition(() => applyChange())` runs a DOM change inside `document.startViewTransition` where the browser has one and plainly where it has not — the same `ready`/`finished`/`updateCallbackDone` promises and `skip()` either way, so a caller has one code path rather than two — and `viewTransitionName="header"` is what makes an element animate _from where it was to where it is_ instead of being cross-faded along with the page. `viewTransitionClass="card"` groups several names, so `::view-transition-group(.card)` styles them at once. **In React the update has to be flushed**: the browser screenshots the page the moment the callback returns and a `setState` has not rendered by then, so a hand-rolled version captures the old state twice and nothing appears to move — write `Box.viewTransition(() => flushSync(() => setTheme('dark')))`, or use **`<Box.Theme viewTransition>`**, which is that call for the one change every app has — `viewTransition="reveal"` grows the new theme out of the control that was pressed as a circle instead of cross-fading. **A theme change pauses every CSS transition** until it has painted (and until the view transition has finished), because `color` is inherited and the base class transitions `all`, so each element would otherwise start one of its own; the view transition is the animation, and it costs the same on any size of page. **A name has to be unique in the document while the transition runs**, so a list wants a name per item — and a class per item is a rule this library would never free, which is why that one goes in `props={{ style: { viewTransitionName: id } }}`, the exception `useAnchorPosition`'s anchor name and a `Slider` thumb already take. Reduced motion **skips** the transition by default and keeps the update, a whole-page cross-fade being exactly the motion the preference is about; `{ reducedMotion: 'play' }` is the opt-out, and `{ types: ['forward'] }` is what `:active-view-transition-type()` selects on.
124
124
 
125
125
  48. **In development the console is part of the answer: read it.** Three mistakes write nothing, or the wrong thing, and say so in one `[box-kite]` warning that names what rendered and the line to write instead. **A value a prop does not take** — `bgColor="bleu-500"` → `wrote no CSS … Did you mean "blue-500"?`, `opacity={50}` → `It takes one of 0, 0.1, …`. **An HTML attribute at the top level** — `href`, `onClick`, `data-*`, `aria-*` → `write props={{ href: ... }}`. **A number in a unit it almost certainly did not mean**, which is valid CSS and so the one mistake nothing else catches: `fontSize={4}` is 4px text (`For 16px write fontSize={16}`), `lineHeight={1.5}` a 1.5px line, `transitionDuration={0.3}` 0.3ms, `brightness={1.1}` 1.1%. Each prints once per value, in any environment whose `NODE_ENV` is not `production` — a test run included, which is the cheapest place to see them — and never in a production build, where a rejected value is still dropped with nothing said. `Box.configure({ warnings: false })` turns them off; `check_styles` in `@box-kite/mcp` gives the same three verdicts before anything renders.
126
126
 
package/BOX_AI_CONTEXT.md CHANGED
@@ -726,7 +726,7 @@ so a change that was a jump becomes a transition with nothing animated by hand.
726
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
727
  | `viewTransitionClass` | A name shared by several elements, so `::view-transition-group(.card)` styles them together. Space-separated for more than one |
728
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 |
729
+ | `<Box.Theme viewTransition>` | That call around a theme change, `flushSync` included. `viewTransition="reveal"` grows the new theme out of the pressed control as a circle |
730
730
 
731
731
  ```tsx
732
732
  const handle = Box.viewTransition(() => flushSync(() => setTab(next)), { types: ['forward'] });
@@ -756,7 +756,9 @@ interface ViewTransitionHandle {
756
756
  - **In React the update has to be flushed.** The browser takes its second screenshot the moment the
757
757
  callback returns, and a `setState` has not rendered by then — so a hand-rolled version captures the old
758
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.
759
+ `<Box.Theme viewTransition>` is it already done for the one change every app has. A theme change also
760
+ pauses every CSS transition until the new theme has painted (and the view transition has finished) — `color` is inherited and every Box transitions
761
+ `all`, so each element would start one of its own — which leaves the view transition as the only animation.
760
762
  - **A name has to be unique in the document while the transition runs.** Two elements sharing one is how a
761
763
  transition silently does nothing. The prop is for the handful of names a layout has (`header`, `main`);
762
764
  a name **per list item** is a rule per item that is never freed, so that one goes in
@@ -2801,8 +2803,20 @@ stops, as they were before A7.
2801
2803
  | `datagrid.topBar.columnGroups.separator` | Separator between groups | — |
2802
2804
  | `datagrid.topBar.columnGroups.item` | Column group chip | — |
2803
2805
  | `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.topBar.columnVisibility` | Columns button (opens the columns panel) | — |
2807
+ | `datagrid.topBar.columnVisibility.badge` | Shown/total count, only while columns are hidden | `empty` |
2808
+ | `datagrid.topBar.columnVisibility.panel` | Columns panel body | — |
2809
+ | `datagrid.topBar.columnVisibility.search` | Search row (past six columns) | — |
2810
+ | `datagrid.topBar.columnVisibility.searchIcon` | Magnifier inside the search field | — |
2811
+ | `datagrid.topBar.columnVisibility.searchInput` | Search field | — |
2812
+ | `datagrid.topBar.columnVisibility.toolbar` | Count + Show all / Hide all row | — |
2813
+ | `datagrid.topBar.columnVisibility.count` | "6 of 8 shown" | — |
2814
+ | `datagrid.topBar.columnVisibility.action` | Show all / Hide all buttons | — |
2815
+ | `datagrid.topBar.columnVisibility.list` | Scrolling list of switches | — |
2816
+ | `datagrid.topBar.columnVisibility.section` | A run of columns under one header group | `divided` |
2817
+ | `datagrid.topBar.columnVisibility.group` | Header group name above its columns | — |
2818
+ | `datagrid.topBar.columnVisibility.item` | One column: label + switch (the `<label>`) | — |
2819
+ | `datagrid.topBar.columnVisibility.empty` | No-match message | — |
2806
2820
  | `datagrid.filter.cell` | Filter row cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned` |
2807
2821
  | `datagrid.filter.cell.input` | Filter input container (text/number/multiselect) | — |
2808
2822
  | `datagrid.header` | Header grid container (sticky) | — |
@@ -726,7 +726,7 @@ so a change that was a jump becomes a transition with nothing animated by hand.
726
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
727
  | `viewTransitionClass` | A name shared by several elements, so `::view-transition-group(.card)` styles them together. Space-separated for more than one |
728
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 |
729
+ | `<Box.Theme viewTransition>` | That call around a theme change, `flushSync` included. `viewTransition="reveal"` grows the new theme out of the pressed control as a circle |
730
730
 
731
731
  ```tsx
732
732
  const handle = Box.viewTransition(() => flushSync(() => setTab(next)), { types: ['forward'] });
@@ -756,7 +756,9 @@ interface ViewTransitionHandle {
756
756
  - **In React the update has to be flushed.** The browser takes its second screenshot the moment the
757
757
  callback returns, and a `setState` has not rendered by then — so a hand-rolled version captures the old
758
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.
759
+ `<Box.Theme viewTransition>` is it already done for the one change every app has. A theme change also
760
+ pauses every CSS transition until the new theme has painted (and the view transition has finished) — `color` is inherited and every Box transitions
761
+ `all`, so each element would start one of its own — which leaves the view transition as the only animation.
760
762
  - **A name has to be unique in the document while the transition runs.** Two elements sharing one is how a
761
763
  transition silently does nothing. The prop is for the handful of names a layout has (`header`, `main`);
762
764
  a name **per list item** is a rule per item that is never freed, so that one goes in
@@ -2801,8 +2803,20 @@ stops, as they were before A7.
2801
2803
  | `datagrid.topBar.columnGroups.separator` | Separator between groups | — |
2802
2804
  | `datagrid.topBar.columnGroups.item` | Column group chip | — |
2803
2805
  | `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.topBar.columnVisibility` | Columns button (opens the columns panel) | — |
2807
+ | `datagrid.topBar.columnVisibility.badge` | Shown/total count, only while columns are hidden | `empty` |
2808
+ | `datagrid.topBar.columnVisibility.panel` | Columns panel body | — |
2809
+ | `datagrid.topBar.columnVisibility.search` | Search row (past six columns) | — |
2810
+ | `datagrid.topBar.columnVisibility.searchIcon` | Magnifier inside the search field | — |
2811
+ | `datagrid.topBar.columnVisibility.searchInput` | Search field | — |
2812
+ | `datagrid.topBar.columnVisibility.toolbar` | Count + Show all / Hide all row | — |
2813
+ | `datagrid.topBar.columnVisibility.count` | "6 of 8 shown" | — |
2814
+ | `datagrid.topBar.columnVisibility.action` | Show all / Hide all buttons | — |
2815
+ | `datagrid.topBar.columnVisibility.list` | Scrolling list of switches | — |
2816
+ | `datagrid.topBar.columnVisibility.section` | A run of columns under one header group | `divided` |
2817
+ | `datagrid.topBar.columnVisibility.group` | Header group name above its columns | — |
2818
+ | `datagrid.topBar.columnVisibility.item` | One column: label + switch (the `<label>`) | — |
2819
+ | `datagrid.topBar.columnVisibility.empty` | No-match message | — |
2806
2820
  | `datagrid.filter.cell` | Filter row cell | `isPinned`, `isFirstLeftPinned`, `isLastLeftPinned`, `isFirstRightPinned`, `isLastRightPinned` |
2807
2821
  | `datagrid.filter.cell.input` | Filter input container (text/number/multiselect) | — |
2808
2822
  | `datagrid.header` | Header grid container (sticky) | — |
package/README.md CHANGED
@@ -20,9 +20,6 @@ version behind what it is writing against: [Built for AI](https://www.box-kite.d
20
20
  [`npx @box-kite/mcp`](https://www.npmjs.com/package/@box-kite/mcp) ·
21
21
  [generative UI](https://www.box-kite.dev/generative-ui/)
22
22
 
23
- > **Renamed at 1.0.0.** This was `@cronocode/react-box`. The API is unchanged — the move is one
24
- > find-and-replace, and `@cronocode/react-box@3.4.0` re-exports this package.
25
-
26
23
  ## Getting Started
27
24
 
28
25
  1. Installation
package/client.cjs CHANGED
@@ -1 +1 @@
1
- const e=require("./rolldown-runtime.cjs"),t=require("./react-shared.cjs"),n=require("./platform.cjs"),r=require("./effects.cjs"),i=require("./box.cjs");let a=require("react");a=e.t(a,1);let o=require("@box-kite/core"),s=require("react-dom"),c=require("react/jsx-runtime");function l(e){let{node:t=null,event:r=`click`,hideOnScroll:i=!1,hideOnResize:o=!1,hideOnEscape:s=!0}=e??{},[c,l]=(0,a.useState)(!1),u=(0,a.useRef)(null);return(0,a.useEffect)(()=>{function e(e){let r=t??u.current;r&&!n.l(e,[r])&&l(!1)}function a(){l(!1)}function d(e){e.key===`Escape`&&l(!1)}let f=new AbortController;return c&&(window.addEventListener(r,e,f),s&&window.addEventListener(`keydown`,d,f),o&&window.addEventListener(`resize`,a,f),i&&window.addEventListener(`scroll`,e,{signal:f.signal,capture:!0})),()=>{f.abort()}},[t,c,r,s,o,i]),[c,l,u]}var u=r.t;function d(e,n){let{classNames:r,signature:i,styleElements:a}=t.l(e,n);return u(()=>{(0,o.getDefaultEngine)().flushSync()},[i??e]),{classNames:r,styleElements:a}}function f(e,n){let r=e?(0,o.getDefaultEngine)().addGlobalStyles(e,n):void 0;return u(()=>{(0,o.getDefaultEngine)().flushSync()},[e,n]),t.u(r)}var p;(function(e){function t(){(0,o.getDefaultEngine)().flushSync()}e.flushSync=t;function n(){(0,o.getDefaultEngine)().clear()}e.clear=n;function r(){return(0,o.getDefaultEngine)().getStyles()}e.getStyles=r;function i(){return(0,o.getDefaultEngine)().getStyleTag()}e.getStyleTag=i;function a(){return(0,o.getDefaultEngine)().styleElementId}e.styleElementId=a;function s(e){(0,o.getDefaultEngine)().configure(e)}e.configure=s})(p||={});var m=`[box-kite] The client build was loaded into a React Server Component graph, where React has no createContext. Import '@box-kite/react' by its package name — the react-server condition resolves it to a Box that renders on the server — and import the components that hold state (Dropdown, Tooltip, DataGrid, Checkbox, Select, Form) from a 'use client' module. See https://github.com/box-kite/box-kite#react-server-components-react-19`;function h(e){if(typeof e.createContext!=`function`)throw Error(m)}h(a.default);var g=a.default.createContext({theme:``,setTheme:()=>{}});function _(e){let{children:t,theme:n,use:r=`local`,storageKey:l,globalStyles:u,viewTransition:d}=e,p=f(r===`global`?u:void 0,`html`),[m,h]=(0,a.useState)(n??o.defaultThemeName),[_,v]=(0,a.useState)(n!==void 0),y=(0,a.useRef)(null),b=(0,a.useRef)(m),x=(0,a.useCallback)(e=>{let t=()=>{e===null?(l&&(0,o.clearStoredTheme)(l),v(!1)):(l&&(0,o.writeStoredTheme)(l,e),h(e),v(!0))};d?(0,o.startViewTransition)(()=>(0,s.flushSync)(t)):t()},[l,d]),[S,C]=(0,a.useState)(n);return n!==S&&(C(n),n===void 0?v(!1):(h(n),v(!0))),(0,a.useLayoutEffect)(()=>{if(_)return;let e=l?(0,o.readStoredTheme)(l):null,t=e??(0,o.getSystemTheme)();if(t!==b.current&&(y.current=t),h(t),e){v(!0);return}return(0,o.watchSystemTheme)(h)},[_,l]),(0,a.useLayoutEffect)(()=>{if(b.current=m,y.current!==null&&y.current!==m||(y.current=null,r===`local`))return;let e=(0,o.documentRoot)();if(e)return(0,o.applyThemeToElement)(e,m)},[m,r]),r===`local`?(0,c.jsxs)(g.Provider,{value:{theme:m,setTheme:x},children:[p,(0,c.jsx)(i.default,{className:m,props:{"data-theme":m},children:t})]}):(0,c.jsxs)(g.Provider,{value:{theme:m,setTheme:x},children:[p,t]})}(function(e){function t(){let{theme:e,setTheme:t}=(0,a.useContext)(g);return[e,t]}e.useTheme=t})(_||={});var v=_,y=new WeakMap;function b(e){let t=y.get(e);return t||(t=new Map,y.set(e,t)),t}function x(e=!0){let[t]=v.useTheme(),r=(0,a.useMemo)(()=>{if(!e)return null;let t=n.t();if(!t)return null;let r=`box-kite-portal`,i=t.getElementById(r);return i||(i=document.createElement(`div`),i.id=r,document.body.appendChild(i)),i},[e]);return(0,a.useLayoutEffect)(()=>{if(!t||!r)return;let e=b(r),n=e.get(t)??0;return e.set(t,n+1),n===0&&r.classList.add(t),()=>{let n=(e.get(t)??1)-1;n<=0?(e.delete(t),r.classList.remove(t)):e.set(t,n)}},[r,t]),r}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return v}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return x}});
1
+ const e=require("./rolldown-runtime.cjs"),t=require("./react-shared.cjs"),n=require("./platform.cjs"),r=require("./effects.cjs"),i=require("./box.cjs");let a=require("react");a=e.t(a,1);let o=require("@box-kite/core"),s=require("react-dom"),c=require("react/jsx-runtime");function l(e){let{node:t=null,event:r=`click`,hideOnScroll:i=!1,hideOnResize:o=!1,hideOnEscape:s=!0}=e??{},[c,l]=(0,a.useState)(!1),u=(0,a.useRef)(null);return(0,a.useEffect)(()=>{function e(e){let r=t??u.current;r&&!n.l(e,[r])&&l(!1)}function a(){l(!1)}function d(e){e.key===`Escape`&&l(!1)}let f=new AbortController;return c&&(window.addEventListener(r,e,f),s&&window.addEventListener(`keydown`,d,f),o&&window.addEventListener(`resize`,a,f),i&&window.addEventListener(`scroll`,e,{signal:f.signal,capture:!0})),()=>{f.abort()}},[t,c,r,s,o,i]),[c,l,u]}var u=r.t;function d(e,n){let{classNames:r,signature:i,styleElements:a}=t.l(e,n);return u(()=>{(0,o.getDefaultEngine)().flushSync()},[i??e]),{classNames:r,styleElements:a}}function f(e,n){let r=e?(0,o.getDefaultEngine)().addGlobalStyles(e,n):void 0;return u(()=>{(0,o.getDefaultEngine)().flushSync()},[e,n]),t.u(r)}var p;(function(e){function t(){(0,o.getDefaultEngine)().flushSync()}e.flushSync=t;function n(){(0,o.getDefaultEngine)().clear()}e.clear=n;function r(){return(0,o.getDefaultEngine)().getStyles()}e.getStyles=r;function i(){return(0,o.getDefaultEngine)().getStyleTag()}e.getStyleTag=i;function a(){return(0,o.getDefaultEngine)().styleElementId}e.styleElementId=a;function s(e){(0,o.getDefaultEngine)().configure(e)}e.configure=s})(p||={});var m=`[box-kite] The client build was loaded into a React Server Component graph, where React has no createContext. Import '@box-kite/react' by its package name — the react-server condition resolves it to a Box that renders on the server — and import the components that hold state (Dropdown, Tooltip, DataGrid, Checkbox, Select, Form) from a 'use client' module. See https://github.com/box-kite/box-kite#react-server-components-react-19`;function h(e){if(typeof e.createContext!=`function`)throw Error(m)}h(a.default);var g=a.default.createContext({theme:``,setTheme:()=>{}});function _(e){let{children:t,theme:n,use:l=`local`,storageKey:u,globalStyles:d,viewTransition:p}=e,m=f(l===`global`?d:void 0,`html`),[h,_]=(0,a.useState)(n??o.defaultThemeName),[v,y]=(0,a.useState)(n!==void 0),b=(0,a.useRef)(null),x=(0,a.useRef)(h),S=(0,a.useRef)(null),C=(0,a.useRef)(void 0),w=(0,a.useCallback)(e=>{let t=()=>{e===null?(u&&(0,o.clearStoredTheme)(u),y(!1)):(u&&(0,o.writeStoredTheme)(u,e),_(e),y(!0))};if(p){let e=(0,o.transitionTheme)(()=>(0,s.flushSync)(t),p===`reveal`?`reveal`:`fade`);C.current=e.finished,e.finished.then(()=>C.current=void 0,()=>C.current=void 0)}else t()},[u,p]),T=p===`reveal`;(0,a.useEffect)(()=>T?(0,o.prepareReveal)():void 0,[T]);let[E,D]=(0,a.useState)(n);return n!==E&&(D(n),n===void 0?y(!1):(_(n),y(!0))),(0,a.useLayoutEffect)(()=>{if(v)return;let e=u?(0,o.readStoredTheme)(u):null,t=e??(0,o.getSystemTheme)();if(t!==x.current&&(b.current=t),_(t),e){y(!0);return}return(0,o.watchSystemTheme)(_)},[v,u]),r.t(()=>{h!==x.current&&(S.current??=(0,o.pauseTransitions)())},[h]),(0,a.useLayoutEffect)(()=>{if(x.current=h,b.current!==null&&b.current!==h)return;b.current=null;let e=S.current;S.current=null;let t=l===`global`?(0,o.documentRoot)():null,n=t?(0,o.applyThemeToElement)(t,h):void 0;return e?.(C.current),n},[h,l]),l===`local`?(0,c.jsxs)(g.Provider,{value:{theme:h,setTheme:w},children:[m,(0,c.jsx)(i.default,{className:h,props:{"data-theme":h},children:t})]}):(0,c.jsxs)(g.Provider,{value:{theme:h,setTheme:w},children:[m,t]})}(function(e){function t(){let{theme:e,setTheme:t}=(0,a.useContext)(g);return[e,t]}e.useTheme=t})(_||={});var v=_,y=new WeakMap;function b(e){let t=y.get(e);return t||(t=new Map,y.set(e,t)),t}function x(e=!0){let[t]=v.useTheme(),r=(0,a.useMemo)(()=>{if(!e)return null;let t=n.t();if(!t)return null;let r=`box-kite-portal`,i=t.getElementById(r);return i||(i=document.createElement(`div`),i.id=r,document.body.appendChild(i)),i},[e]);return(0,a.useLayoutEffect)(()=>{if(!t||!r)return;let e=b(r),n=e.get(t)??0;return e.set(t,n+1),n===0&&r.classList.add(t),()=>{let n=(e.get(t)??1)-1;n<=0?(e.delete(t),r.classList.remove(t)):e.set(t,n)}},[r,t]),r}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return v}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return x}});
package/client.mjs CHANGED
@@ -3,11 +3,11 @@ import { l as n, t as r } from "./platform.mjs";
3
3
  import { t as i } from "./effects.mjs";
4
4
  import a from "./box.mjs";
5
5
  import o, { useCallback as s, useContext as c, useEffect as l, useLayoutEffect as u, useMemo as d, useRef as f, useState as p } from "react";
6
- import { applyThemeToElement as m, clearStoredTheme as h, defaultThemeName as g, documentRoot as _, getDefaultEngine as v, getSystemTheme as y, readStoredTheme as b, startViewTransition as x, watchSystemTheme as S, writeStoredTheme as C } from "@box-kite/core";
7
- import { flushSync as w } from "react-dom";
8
- import { jsx as T, jsxs as E } from "react/jsx-runtime";
6
+ import { applyThemeToElement as m, clearStoredTheme as h, defaultThemeName as g, documentRoot as _, getDefaultEngine as v, getSystemTheme as y, pauseTransitions as b, prepareReveal as x, readStoredTheme as S, transitionTheme as C, watchSystemTheme as w, writeStoredTheme as T } from "@box-kite/core";
7
+ import { flushSync as E } from "react-dom";
8
+ import { jsx as D, jsxs as O } from "react/jsx-runtime";
9
9
  //#region src/react/hooks/useVisibility.ts
10
- function D(e) {
10
+ function k(e) {
11
11
  let { node: t = null, event: r = "click", hideOnScroll: i = !1, hideOnResize: a = !1, hideOnEscape: o = !0 } = e ?? {}, [s, c] = p(!1), u = f(null);
12
12
  return l(() => {
13
13
  function e(e) {
@@ -42,23 +42,23 @@ function D(e) {
42
42
  }
43
43
  //#endregion
44
44
  //#region src/react/useStyles.ts
45
- var O = i;
46
- function k(t, n) {
45
+ var A = i;
46
+ function j(t, n) {
47
47
  let { classNames: r, signature: i, styleElements: a } = e(t, n);
48
- return O(() => {
48
+ return A(() => {
49
49
  v().flushSync();
50
50
  }, [i ?? t]), {
51
51
  classNames: r,
52
52
  styleElements: a
53
53
  };
54
54
  }
55
- function A(e, n) {
55
+ function M(e, n) {
56
56
  let r = e ? v().addGlobalStyles(e, n) : void 0;
57
- return O(() => {
57
+ return A(() => {
58
58
  v().flushSync();
59
59
  }, [e, n]), t(r);
60
60
  }
61
- var j;
61
+ var N;
62
62
  (function(e) {
63
63
  function t() {
64
64
  v().flushSync();
@@ -84,73 +84,83 @@ var j;
84
84
  v().configure(e);
85
85
  }
86
86
  e.configure = o;
87
- })(j ||= {});
87
+ })(N ||= {});
88
88
  //#endregion
89
89
  //#region src/react/clientRuntime.ts
90
- var M = "[box-kite] The client build was loaded into a React Server Component graph, where React has no createContext. Import '@box-kite/react' by its package name — the react-server condition resolves it to a Box that renders on the server — and import the components that hold state (Dropdown, Tooltip, DataGrid, Checkbox, Select, Form) from a 'use client' module. See https://github.com/box-kite/box-kite#react-server-components-react-19";
91
- function N(e) {
92
- if (typeof e.createContext != "function") throw Error(M);
90
+ var P = "[box-kite] The client build was loaded into a React Server Component graph, where React has no createContext. Import '@box-kite/react' by its package name — the react-server condition resolves it to a Box that renders on the server — and import the components that hold state (Dropdown, Tooltip, DataGrid, Checkbox, Select, Form) from a 'use client' module. See https://github.com/box-kite/box-kite#react-server-components-react-19";
91
+ function F(e) {
92
+ if (typeof e.createContext != "function") throw Error(P);
93
93
  }
94
94
  //#endregion
95
95
  //#region src/react/theme/themeContext.ts
96
- N(o);
97
- var P = o.createContext({
96
+ F(o);
97
+ var I = o.createContext({
98
98
  theme: "",
99
99
  setTheme: () => {}
100
100
  });
101
101
  //#endregion
102
102
  //#region src/react/theme/theme.tsx
103
- function F(e) {
104
- let { children: t, theme: n, use: r = "local", storageKey: i, globalStyles: o, viewTransition: c } = e, l = A(r === "global" ? o : void 0, "html"), [d, v] = p(n ?? g), [D, O] = p(n !== void 0), k = f(null), j = f(d), M = s((e) => {
103
+ function L(e) {
104
+ let { children: t, theme: n, use: r = "local", storageKey: o, globalStyles: c, viewTransition: d } = e, v = M(r === "global" ? c : void 0, "html"), [k, A] = p(n ?? g), [j, N] = p(n !== void 0), P = f(null), F = f(k), L = f(null), R = f(void 0), z = s((e) => {
105
105
  let t = () => {
106
- e === null ? (i && h(i), O(!1)) : (i && C(i, e), v(e), O(!0));
106
+ e === null ? (o && h(o), N(!1)) : (o && T(o, e), A(e), N(!0));
107
107
  };
108
- c ? x(() => w(t)) : t();
109
- }, [i, c]), [N, F] = p(n);
110
- return n !== N && (F(n), n === void 0 ? O(!1) : (v(n), O(!0))), u(() => {
111
- if (D) return;
112
- let e = i ? b(i) : null, t = e ?? y();
113
- if (t !== j.current && (k.current = t), v(t), e) {
114
- O(!0);
108
+ if (d) {
109
+ let e = C(() => E(t), d === "reveal" ? "reveal" : "fade");
110
+ R.current = e.finished, e.finished.then(() => R.current = void 0, () => R.current = void 0);
111
+ } else t();
112
+ }, [o, d]), B = d === "reveal";
113
+ l(() => B ? x() : void 0, [B]);
114
+ let [V, H] = p(n);
115
+ return n !== V && (H(n), n === void 0 ? N(!1) : (A(n), N(!0))), u(() => {
116
+ if (j) return;
117
+ let e = o ? S(o) : null, t = e ?? y();
118
+ if (t !== F.current && (P.current = t), A(t), e) {
119
+ N(!0);
115
120
  return;
116
121
  }
117
- return S(v);
118
- }, [D, i]), u(() => {
119
- if (j.current = d, k.current !== null && k.current !== d || (k.current = null, r === "local")) return;
120
- let e = _();
121
- if (e) return m(e, d);
122
- }, [d, r]), r === "local" ? /* @__PURE__ */ E(P.Provider, {
122
+ return w(A);
123
+ }, [j, o]), i(() => {
124
+ k !== F.current && (L.current ??= b());
125
+ }, [k]), u(() => {
126
+ if (F.current = k, P.current !== null && P.current !== k) return;
127
+ P.current = null;
128
+ let e = L.current;
129
+ L.current = null;
130
+ let t = r === "global" ? _() : null, n = t ? m(t, k) : void 0;
131
+ return e?.(R.current), n;
132
+ }, [k, r]), r === "local" ? /* @__PURE__ */ O(I.Provider, {
123
133
  value: {
124
- theme: d,
125
- setTheme: M
134
+ theme: k,
135
+ setTheme: z
126
136
  },
127
- children: [l, /* @__PURE__ */ T(a, {
128
- className: d,
129
- props: { "data-theme": d },
137
+ children: [v, /* @__PURE__ */ D(a, {
138
+ className: k,
139
+ props: { "data-theme": k },
130
140
  children: t
131
141
  })]
132
- }) : /* @__PURE__ */ E(P.Provider, {
142
+ }) : /* @__PURE__ */ O(I.Provider, {
133
143
  value: {
134
- theme: d,
135
- setTheme: M
144
+ theme: k,
145
+ setTheme: z
136
146
  },
137
- children: [l, t]
147
+ children: [v, t]
138
148
  });
139
149
  }
140
150
  (function(e) {
141
151
  function t() {
142
- let { theme: e, setTheme: t } = c(P);
152
+ let { theme: e, setTheme: t } = c(I);
143
153
  return [e, t];
144
154
  }
145
155
  e.useTheme = t;
146
- })(F ||= {});
147
- var I = F, L = /* @__PURE__ */ new WeakMap();
148
- function R(e) {
149
- let t = L.get(e);
150
- return t || (t = /* @__PURE__ */ new Map(), L.set(e, t)), t;
156
+ })(L ||= {});
157
+ var R = L, z = /* @__PURE__ */ new WeakMap();
158
+ function B(e) {
159
+ let t = z.get(e);
160
+ return t || (t = /* @__PURE__ */ new Map(), z.set(e, t)), t;
151
161
  }
152
- function z(e = !0) {
153
- let [t] = I.useTheme(), n = d(() => {
162
+ function V(e = !0) {
163
+ let [t] = R.useTheme(), n = d(() => {
154
164
  if (!e) return null;
155
165
  let t = r();
156
166
  if (!t) return null;
@@ -159,7 +169,7 @@ function z(e = !0) {
159
169
  }, [e]);
160
170
  return u(() => {
161
171
  if (!t || !n) return;
162
- let e = R(n), r = e.get(t) ?? 0;
172
+ let e = B(n), r = e.get(t) ?? 0;
163
173
  return e.set(t, r + 1), r === 0 && n.classList.add(t), () => {
164
174
  let r = (e.get(t) ?? 1) - 1;
165
175
  r <= 0 ? (e.delete(t), n.classList.remove(t)) : e.set(t, r);
@@ -167,4 +177,4 @@ function z(e = !0) {
167
177
  }, [n, t]), n;
168
178
  }
169
179
  //#endregion
170
- export { D as a, k as i, I as n, j as r, z as t };
180
+ export { k as a, j as i, R as n, N as r, V as t };
@@ -0,0 +1,10 @@
1
+ import { default as GridModel } from '../models/gridModel';
2
+ interface Props<TRow> {
3
+ grid: GridModel<TRow>;
4
+ }
5
+ /**
6
+ * The columns button and its panel: a non-modal dialog with a search field, the count and the two bulk
7
+ * actions, and one switch per column under its header group's name.
8
+ */
9
+ export default function DataGridColumnsPanel<TRow>(props: Props<TRow>): import("react").JSX.Element;
10
+ export {};
@@ -52,6 +52,8 @@ export default class ColumnModel<TRow> {
52
52
  get isLastLeaf(): boolean;
53
53
  get key(): Key;
54
54
  get header(): string | undefined;
55
+ /** The header group this column sits under, if any. A pinned half of a group is a copy with the same header. */
56
+ get group(): ColumnModel<TRow> | undefined;
55
57
  get align(): "center" | "left" | "right" | "start" | "end" | undefined;
56
58
  /** End-aligned either way it is spelled — `right` is the physical name of `end` in a left-to-right grid. */
57
59
  get isEndAligned(): boolean;
@@ -3,12 +3,19 @@ import { default as GridModel } from './gridModel';
3
3
  export interface ColumnVisibilityEntry<TRow> {
4
4
  id: string;
5
5
  label: string;
6
+ /** The header group's text, when the column sits under one. */
7
+ group?: string;
6
8
  column: ColumnModel<TRow>;
7
9
  visible: boolean;
8
10
  }
11
+ /** A run of entries under one header group, in column order. `group` is absent for ungrouped columns. */
12
+ export interface ColumnVisibilitySection<TRow> {
13
+ group?: string;
14
+ entries: ColumnVisibilityEntry<TRow>[];
15
+ }
9
16
  /**
10
- * Backs the top-bar column-visibility menu: the toggleable (user data) columns, which
11
- * are currently shown, and a diff-based bulk setter.
17
+ * Backs the top-bar columns panel: the toggleable (user data) columns, which are currently shown, the
18
+ * search over them, and a diff-based bulk setter.
12
19
  */
13
20
  export default class ColumnVisibilityModel<TRow> {
14
21
  readonly grid: GridModel<TRow>;
@@ -17,8 +24,15 @@ export default class ColumnVisibilityModel<TRow> {
17
24
  get entries(): ColumnVisibilityEntry<TRow>[];
18
25
  get selectedIds(): string[];
19
26
  get total(): number;
27
+ get visibleCount(): number;
20
28
  get hiddenCount(): number;
21
29
  get hasHidden(): boolean;
30
+ /** The entries whose label or group contains `query`, ignoring case and surrounding space. */
31
+ search(query: string): ColumnVisibilityEntry<TRow>[];
32
+ /** `entries` cut into consecutive runs that share a group, which is what the panel draws a heading over. */
33
+ sections(entries: ColumnVisibilityEntry<TRow>[]): ColumnVisibilitySection<TRow>[];
22
34
  /** Make exactly the columns in `ids` visible, toggling only those whose state must change. */
23
35
  setVisibility: (ids: string[]) => void;
36
+ /** Show or hide every column in `entries` (all of them by default), leaving the rest as they are. */
37
+ setAll: (visible: boolean, entries?: ColumnVisibilityEntry<TRow>[]) => void;
24
38
  }
@@ -179,6 +179,11 @@ export default class GridModel<TRow> {
179
179
  private _sizingElement;
180
180
  get sizingElement(): HTMLElement | null;
181
181
  setSizingElement: (el: HTMLElement | null) => void;
182
+ /**
183
+ * Writes the width variables straight onto the sizing element, ahead of the render that will agree with
184
+ * them: a resize drag repaints without re-rendering every row, and a new scroller width lands before paint.
185
+ */
186
+ paintSizes: () => void;
182
187
  /**
183
188
  * Which way the grid reads, resolved off the element the widths are written to. A model with no
184
189
  * element yet — a server render, a headless host — reads as left to right, the initial value.
@@ -31,14 +31,14 @@ export default class GroupRowModel<TRow> {
31
31
  aggregateValue(_column: ColumnModel<TRow>): AggregateValue | undefined;
32
32
  get key(): Key;
33
33
  parentRow?: GroupRowModel<TRow>;
34
- private readonly _cells;
34
+ private _cells?;
35
35
  get cells(): GroupRowCellModel<TRow>[];
36
+ private cellCache;
36
37
  /**
37
38
  * The cells this row actually renders, each with the column it starts at. A `hidden` cell is a
38
39
  * data column the grouping cell has absorbed into its span: no element, and nothing to navigate
39
40
  * to — which is why a group row's cell positions and its `aria-colindex` values differ.
40
41
  */
41
- private readonly _renderedCells;
42
42
  get renderedCells(): {
43
43
  cell: GroupRowCellModel<TRow>;
44
44
  columnIndex: number;
@@ -35,6 +35,11 @@ export default class HeaderCellModel<TRow> {
35
35
  get ariaSort(): 'ascending' | 'descending' | 'none' | undefined;
36
36
  /** What the resizer names itself. A separator carries no text, so it has nothing else to go on. */
37
37
  get resizerLabel(): string;
38
+ /**
39
+ * Whether this header ends where the grid does. A resizer straddling that edge overhangs the scroller,
40
+ * which made every grid whose columns exactly fit scroll sideways by 3px.
41
+ */
42
+ get isAtGridEnd(): boolean;
38
43
  /** `aria-valuenow`: how wide the column is right now, in px. */
39
44
  get width(): number;
40
45
  /** `aria-valuemin`: every leaf the header covers at the narrowest the grid allows. */
@@ -6,11 +6,17 @@
6
6
  export interface DataGridLocaleText {
7
7
  /** The global search box's placeholder. */
8
8
  searchPlaceholder: string;
9
- /** The column chooser's accessible name. */
9
+ /** The columns button's text and the panel's accessible name. */
10
10
  columnsMenu: string;
11
+ /** The button's accessible name while some columns are hidden — it has to start with `columnsMenu`. */
12
+ columnsMenuLabel: (shown: number, total: number) => string;
11
13
  columnsMenuSearchPlaceholder: string;
12
14
  columnsMenuShowAll: string;
13
15
  columnsMenuHideAll: string;
16
+ /** The count above the list: "6 of 8 shown". */
17
+ columnsMenuShown: (shown: number, total: number) => string;
18
+ /** What the list says when the search matches no column. */
19
+ columnsMenuNoMatch: (query: string) => string;
14
20
  /** The ✕ on a grouped column's chip. */
15
21
  stopGrouping: (column: string) => string;
16
22
  /** The export buttons' visible text, and their accessible names. */
@@ -29,7 +29,7 @@ export default class RowModel<TRow> {
29
29
  /** Whether clicking the row toggles its detail panel. */
30
30
  get expandOnRowClick(): boolean;
31
31
  toggleDetail: () => void;
32
- private readonly _cells;
32
+ private _cells?;
33
33
  get cells(): CellModel<TRow>[];
34
34
  /** One cell per visible column — the width the grid navigation sees for this row. */
35
35
  get cellCount(): number;