@box-kite/react 2.2.3 → 2.2.5
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.
- package/.claude/rules/box-kite-rules.md +8 -0
- package/.claude/skills/box-kite/SKILL.md +8 -0
- package/.claude/skills/box-kite/references/components.md +49 -3
- package/.cursor/rules/box-kite.mdc +8 -0
- package/AGENTS.md +8 -0
- package/BOX_AI_CONTEXT.md +276 -20
- package/BOX_KITE_AI_CONTEXT.md +276 -20
- package/behavior.cjs +1 -1
- package/behavior.mjs +57 -57
- package/box.cjs +1 -1
- package/box.d.ts +5 -0
- package/box.mjs +17 -17
- package/catalog.cjs +1 -1
- package/catalog.mjs +385 -0
- package/client.cjs +1 -1
- package/client.mjs +113 -87
- package/components/dashboard.cjs +1 -1
- package/components/dashboard.mjs +1 -1
- package/components/dataGrid/components/dataGridList.d.ts +12 -0
- package/components/dataGrid/components/dataGridListAggregate.d.ts +7 -0
- package/components/dataGrid/components/dataGridListFilter.d.ts +7 -0
- package/components/dataGrid/components/dataGridListFilterSheet.d.ts +11 -0
- package/components/dataGrid/components/dataGridListFooter.d.ts +13 -0
- package/components/dataGrid/components/dataGridListGroup.d.ts +13 -0
- package/components/dataGrid/components/dataGridListItem.d.ts +21 -0
- package/components/dataGrid/components/dataGridListLayout.d.ts +20 -0
- package/components/dataGrid/components/dataGridListRecord.d.ts +11 -0
- package/components/dataGrid/components/dataGridListSelectionBar.d.ts +11 -0
- package/components/dataGrid/components/dataGridListSheet.d.ts +30 -0
- package/components/dataGrid/components/dataGridListSortSheet.d.ts +10 -0
- package/components/dataGrid/components/dataGridListToolbar.d.ts +10 -0
- package/components/dataGrid/components/dataGridListValue.d.ts +12 -0
- package/components/dataGrid/contracts/dataGridContract.d.ts +32 -0
- package/components/dataGrid/models/gridModel.d.ts +3 -0
- package/components/dataGrid/models/listModel.d.ts +129 -0
- package/components/dataGrid/models/localeText.d.ts +32 -0
- package/components/dataGrid/models/viewportModel.d.ts +0 -2
- package/components/dataGrid/useListLayout.d.ts +13 -0
- package/components/dataGrid.cjs +3 -3
- package/components/dataGrid.mjs +1052 -788
- package/components/dialog.cjs +1 -1
- package/components/dialog.mjs +85 -97
- package/components/list/listItemParts.d.ts +9 -0
- package/components/list.cjs +1 -0
- package/components/list.d.ts +118 -0
- package/components/list.mjs +391 -0
- package/components/menu.cjs +1 -1
- package/components/menu.mjs +25 -25
- package/components/overlay.cjs +1 -1
- package/components/overlay.mjs +12 -12
- package/components/semantics.d.ts +29 -29
- package/components/slider.cjs +1 -1
- package/components/slider.mjs +1 -1
- package/components/sortableList.cjs +1 -0
- package/components/sortableList.d.ts +49 -0
- package/components/sortableList.mjs +384 -0
- package/components/swipeItem.cjs +1 -0
- package/components/swipeItem.d.ts +52 -0
- package/components/swipeItem.mjs +321 -0
- package/components/tabs.cjs +1 -1
- package/components/tabs.mjs +1 -1
- package/components/toaster.cjs +1 -1
- package/components/toaster.mjs +24 -24
- package/components/virtualList.cjs +1 -0
- package/components/virtualList.d.ts +32 -0
- package/components/virtualList.mjs +188 -0
- package/dataGridList.cjs +1 -0
- package/dataGridList.mjs +1351 -0
- package/docs/components.md +5 -1
- package/icons/closeIcon.d.ts +2 -0
- package/icons/filterIcon.d.ts +2 -0
- package/layer.cjs +1 -1
- package/layer.mjs +1 -1
- package/mcp/src/data.d.ts +2 -0
- package/package.json +86 -22
- package/placement.cjs +1 -1
- package/placement.mjs +10 -10
- package/platform.cjs +1 -1
- package/platform.mjs +4 -1
- package/react/a11y/useControllableState.d.ts +2 -1
- package/react/componentsContext.d.ts +4 -0
- package/react/componentsProvider.d.ts +10 -0
- package/react/resolveStyles.d.ts +2 -1
- package/react/theme/theme.d.ts +6 -1
- package/react-shared.cjs +1 -1
- package/react-shared.mjs +40 -29
- package/registry/blocks/dashboard-shell/dashboard-shell.d.ts +3 -1
- package/registry/designs/store/address-form.d.ts +11 -0
- package/registry/designs/store/cart-items.d.ts +10 -0
- package/registry/designs/store/cart-page.d.ts +4 -0
- package/registry/designs/store/checkout-page.d.ts +7 -0
- package/registry/designs/store/credit-card-form.d.ts +10 -0
- package/registry/designs/store/form-field.d.ts +19 -0
- package/registry/designs/store/order-summary.d.ts +18 -0
- package/registry/designs/store/product-art.d.ts +26 -0
- package/registry/designs/store/product-card.d.ts +11 -0
- package/registry/designs/store/product-gallery.d.ts +11 -0
- package/registry/designs/store/product-grid.d.ts +10 -0
- package/registry/designs/store/product-page.d.ts +5 -0
- package/registry/designs/store/quantity-stepper.d.ts +13 -0
- package/registry/designs/store/shipping-method.d.ts +14 -0
- package/registry/designs/store/store-data.d.ts +53 -0
- package/registry/designs/store/store-design.d.ts +63 -0
- package/registry/designs/store/store-shell.d.ts +12 -0
- package/registry/designs/store/storefront-page.d.ts +4 -0
- package/registry/designs/store/variant-picker.d.ts +15 -0
- package/rsc.cjs +1 -1
- package/rsc.d.ts +2 -0
- package/rsc.mjs +1 -1
- package/scrolling.cjs +1 -0
- package/scrolling.mjs +22 -0
- package/ssg.cjs +1 -1
- package/ssg.mjs +1 -1
- package/utils/scroll/scrollUtils.d.ts +6 -0
- package/utils/sortable/sortableUtils.d.ts +25 -0
- package/utils/swipe/swipeUtils.d.ts +53 -0
- package/utils/virtual/measuredWindow.d.ts +28 -0
- package/utils/virtual/rowSearch.d.ts +11 -0
|
@@ -84,4 +84,12 @@ globs: '**/*.{ts,tsx,jsx}'
|
|
|
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
|
|
|
87
|
+
49. **A design that stops at a subtree is `Box.createComponents()`, not `Box.components()`.** `Box.components()` is one registry for the whole app: a second call merges into the first, so two designs cannot share a page through it. `const aurora = Box.createComponents({ button: { styles: { borderRadius: 999 } } })` takes the same shape and registers nothing, and `<Box.Components value={aurora}>` — or `<Box.Theme use="local" components={aurora}>`, a design in light and dark in one wrapper — applies it to every Box inside, pre-built components included. `base` is what it merges over: `'global'` (default, the app's own `Box.components()`), `'builtin'` (ignores them) or another registry. Props still beat the design. It is identified by its content, so an inline one costs no cache, but declare it at module scope. The provider renders no element and a Server Component cannot read it. Two designs that both name `button` type as `interface ComponentsTypes extends MergeComponentsAndVariants<[typeof aurora, typeof harbor]> {}`.
|
|
88
|
+
|
|
89
|
+
50. **A list of things to read and pick from is `<List>`, and one action covers each item.** `<List><List.Item chevron><List.Media variant="avatar">AL</List.Media><List.Content><List.Row><List.Title lines={1}><List.Action props={{ href: '/inbox/42' }}>Ada Lovelace</List.Action></List.Title><List.Meta>9:41</List.Meta></List.Row><List.Description lines={2}>…</List.Description></List.Content></List.Item></List>` (`components/list`). **An item is a flex row of three slots** — `List.Media` leading (`variant` `icon`/`avatar`/`image`), `List.Content` taking the room, `List.End` trailing (a value over its label, aligned to the end) — and the content is a column of `List.Row`s, each its parts side by side with the last pushed to the end: a main row (the title and its time) and a `secondary` one under it (smaller, muted). `wrap` on a row or an item lets the parts wrap instead of squeezing; `List.Overline`/`List.Title`/`List.Description` take `lines={1|2|3}` (an ellipsis, then a clamp); `List.Meta` is a small value that never wraps; `List.Fields` + `List.Field label` is a `<dl>` of labelled values; `List.Group label level sticky` is a section with a nested list of its own. **`List.Action` goes in the title and its `::after` covers the item**, so the whole row presses while the name a reader hears is the title alone — an `href` in `props` makes it an `<a>`, an `htmlFor` a `<label>` (a press anywhere ticks that checkbox), neither a `<button>` with a top-level `onClick`; a control of the item's own (a switch, a menu, that checkbox) goes in `List.Actions`, which sits above it. **Four things to know.** It renders on a server — no hook, no context — so the list's look reaches its items as **custom properties**: `variant="plain"|"card"|"separated"` on `List`, and `vars={{ 'list-divider': …, 'list-divider-inset': '4.5rem', 'list-item-radius': …, 'list-sticky-top': '4rem' }}` to tune it. The style tree is **paint only** (`list`, `list.item`, `list.media`, `list.muted`, `list.action`, `list.label`), the hairlines and tints mixed from `currentColor`; the spacing is each part's default props, so `<List.Item py={1}>` wins and `Box.components` cannot move it. Hover, pressed, `aria-current` and disabled are read off the action with `:has()` — no prop — and `selected` on an item is a tint only, the state a reader hears belonging on the control. And it is a `<ul role="list">` (Safari drops the semantics otherwise; `tag="ol"` for an ordered one), **not a composite widget**: every action is a tab stop and the arrows stay the page's — arrow-key selection among options is a listbox, `Combobox` or `Select`.
|
|
90
|
+
|
|
91
|
+
51. **On a phone a DataGrid is a list, and a column says where it goes in an item.** Below `def.list.breakpoint` (576px) of **the grid's own width** — not the viewport's — `layout: 'auto'` (the default) draws each row as a list item: `list: 'title'` is its name and the button that opens it, `'kicker'` a small line above, `'subtitle'` a line under, `'end'` the one value opposite the title with its column's name under it, `'field'` a labelled value, `'detail'` the record view only, `'hidden'` nowhere. Left unset, the first column is the title, the first end-aligned one the end, the next four fields (`def.list.fields`) and the rest detail. A press opens the **record view** — every column of one row in a bottom sheet with a pager (buttons, Left/Right, a swipe) that follows the list's sort and filters, and an **Edit** button on each editable column through the same `onCellEdit`. Sort and filters are bottom sheets behind two pills, the filters in force are removable chips, **Select** is a mode (a press ticks instead of opening, and a bar pinned to the bottom counts the selection), and the list grows twenty a **Show more** — a button, so the totals and the page below stay reachable — or pages when the host paginates. **Four things to know.** It is a `<List>` (rule 50) — each row a `List.Item` whose title is the `List.Action` — not a restyled `role="grid"`, so the `list` tree styles its rows. It is a chunk of its own, loaded the first time a grid is narrow, and the grid draws its table until it arrives — in a test, `await loadListLayout()` first. Unmeasured is a table, so a server prerenders the table and `layout: 'list'` is how to say otherwise. And the selection bar sticks to the viewport, so no ancestor may be `overflow: hidden`/`auto` — `css={{ overflow: 'clip' }}` clips without being a scroll container. `layout: 'table'` keeps the table at every width.
|
|
92
|
+
|
|
93
|
+
52. **Swiping, reordering and windowing a list are three client entries on top of `List`.** `<SwipeItem endActions={<SwipeItem.Action variant="danger" onClick={remove}>Delete</SwipeItem.Action>} fullSwipe>` (`components/swipeItem`) is a `List.Item` — same parts, `size`, `chevron`, `selected` — whose row slides to open a tray; the first action of each tray is the outermost, and `fullSwipe` performs it by clicking it, so its `onClick` is where the command lives. The actions are tab stops and focus opens their tray, but a swipe is a path gesture: put the same commands where a press reaches too. `<SortableList onOrderChange={(order) => setIds(order)}>` (`components/sortableList`) with `<SortableList.Item value={id} label={name}>` and a `<SortableList.Handle />` in each: **the app owns the order** and renders it, `onOrderChange` fires once per drop and per keyboard step, `onOrderCommit` when it settles; the keyboard grabs (Enter/Space, arrows, Escape puts back), and a press on a handle then a press on another item drops it there. `<VirtualList items={rows} itemKey={(row) => row.id}>{(row) => <List.Item>…</List.Item>}</VirtualList>` (`components/virtualList`) renders a window, measuring each item by key; the scroller is the nearest scrolling ancestor or the page, `scrollerRef` when that is wrong, and `onRangeChange` fetches the next page. **Three things to know.** The children function returns exactly one `<li>` per item and nothing around it, since the list measures its children. A windowed list has no `List.Group`s and find-in-page cannot see what is not rendered. And a `SwipeItem` windows like any item, but a `SortableList` holds its whole list: reorder a list short enough to render, window one too long to.
|
|
94
|
+
|
|
87
95
|
Full reference: `src/BOX_KITE_AI_CONTEXT.md` or invoke `/box-kite` skill.
|
|
@@ -157,4 +157,12 @@ Detect the package manager from the lock file rather than asking:
|
|
|
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
|
|
|
160
|
+
49. **A design that stops at a subtree is `Box.createComponents()`, not `Box.components()`.** `Box.components()` is one registry for the whole app: a second call merges into the first, so two designs cannot share a page through it. `const aurora = Box.createComponents({ button: { styles: { borderRadius: 999 } } })` takes the same shape and registers nothing, and `<Box.Components value={aurora}>` — or `<Box.Theme use="local" components={aurora}>`, a design in light and dark in one wrapper — applies it to every Box inside, pre-built components included. `base` is what it merges over: `'global'` (default, the app's own `Box.components()`), `'builtin'` (ignores them) or another registry. Props still beat the design. It is identified by its content, so an inline one costs no cache, but declare it at module scope. The provider renders no element and a Server Component cannot read it. Two designs that both name `button` type as `interface ComponentsTypes extends MergeComponentsAndVariants<[typeof aurora, typeof harbor]> {}`.
|
|
161
|
+
|
|
162
|
+
50. **A list of things to read and pick from is `<List>`, and one action covers each item.** `<List><List.Item chevron><List.Media variant="avatar">AL</List.Media><List.Content><List.Row><List.Title lines={1}><List.Action props={{ href: '/inbox/42' }}>Ada Lovelace</List.Action></List.Title><List.Meta>9:41</List.Meta></List.Row><List.Description lines={2}>…</List.Description></List.Content></List.Item></List>` (`components/list`). **An item is a flex row of three slots** — `List.Media` leading (`variant` `icon`/`avatar`/`image`), `List.Content` taking the room, `List.End` trailing (a value over its label, aligned to the end) — and the content is a column of `List.Row`s, each its parts side by side with the last pushed to the end: a main row (the title and its time) and a `secondary` one under it (smaller, muted). `wrap` on a row or an item lets the parts wrap instead of squeezing; `List.Overline`/`List.Title`/`List.Description` take `lines={1|2|3}` (an ellipsis, then a clamp); `List.Meta` is a small value that never wraps; `List.Fields` + `List.Field label` is a `<dl>` of labelled values; `List.Group label level sticky` is a section with a nested list of its own. **`List.Action` goes in the title and its `::after` covers the item**, so the whole row presses while the name a reader hears is the title alone — an `href` in `props` makes it an `<a>`, an `htmlFor` a `<label>` (a press anywhere ticks that checkbox), neither a `<button>` with a top-level `onClick`; a control of the item's own (a switch, a menu, that checkbox) goes in `List.Actions`, which sits above it. **Four things to know.** It renders on a server — no hook, no context — so the list's look reaches its items as **custom properties**: `variant="plain"|"card"|"separated"` on `List`, and `vars={{ 'list-divider': …, 'list-divider-inset': '4.5rem', 'list-item-radius': …, 'list-sticky-top': '4rem' }}` to tune it. The style tree is **paint only** (`list`, `list.item`, `list.media`, `list.muted`, `list.action`, `list.label`), the hairlines and tints mixed from `currentColor`; the spacing is each part's default props, so `<List.Item py={1}>` wins and `Box.components` cannot move it. Hover, pressed, `aria-current` and disabled are read off the action with `:has()` — no prop — and `selected` on an item is a tint only, the state a reader hears belonging on the control. And it is a `<ul role="list">` (Safari drops the semantics otherwise; `tag="ol"` for an ordered one), **not a composite widget**: every action is a tab stop and the arrows stay the page's — arrow-key selection among options is a listbox, `Combobox` or `Select`.
|
|
163
|
+
|
|
164
|
+
51. **On a phone a DataGrid is a list, and a column says where it goes in an item.** Below `def.list.breakpoint` (576px) of **the grid's own width** — not the viewport's — `layout: 'auto'` (the default) draws each row as a list item: `list: 'title'` is its name and the button that opens it, `'kicker'` a small line above, `'subtitle'` a line under, `'end'` the one value opposite the title with its column's name under it, `'field'` a labelled value, `'detail'` the record view only, `'hidden'` nowhere. Left unset, the first column is the title, the first end-aligned one the end, the next four fields (`def.list.fields`) and the rest detail. A press opens the **record view** — every column of one row in a bottom sheet with a pager (buttons, Left/Right, a swipe) that follows the list's sort and filters, and an **Edit** button on each editable column through the same `onCellEdit`. Sort and filters are bottom sheets behind two pills, the filters in force are removable chips, **Select** is a mode (a press ticks instead of opening, and a bar pinned to the bottom counts the selection), and the list grows twenty a **Show more** — a button, so the totals and the page below stay reachable — or pages when the host paginates. **Four things to know.** It is a `<List>` (rule 50) — each row a `List.Item` whose title is the `List.Action` — not a restyled `role="grid"`, so the `list` tree styles its rows. It is a chunk of its own, loaded the first time a grid is narrow, and the grid draws its table until it arrives — in a test, `await loadListLayout()` first. Unmeasured is a table, so a server prerenders the table and `layout: 'list'` is how to say otherwise. And the selection bar sticks to the viewport, so no ancestor may be `overflow: hidden`/`auto` — `css={{ overflow: 'clip' }}` clips without being a scroll container. `layout: 'table'` keeps the table at every width.
|
|
165
|
+
|
|
166
|
+
52. **Swiping, reordering and windowing a list are three client entries on top of `List`.** `<SwipeItem endActions={<SwipeItem.Action variant="danger" onClick={remove}>Delete</SwipeItem.Action>} fullSwipe>` (`components/swipeItem`) is a `List.Item` — same parts, `size`, `chevron`, `selected` — whose row slides to open a tray; the first action of each tray is the outermost, and `fullSwipe` performs it by clicking it, so its `onClick` is where the command lives. The actions are tab stops and focus opens their tray, but a swipe is a path gesture: put the same commands where a press reaches too. `<SortableList onOrderChange={(order) => setIds(order)}>` (`components/sortableList`) with `<SortableList.Item value={id} label={name}>` and a `<SortableList.Handle />` in each: **the app owns the order** and renders it, `onOrderChange` fires once per drop and per keyboard step, `onOrderCommit` when it settles; the keyboard grabs (Enter/Space, arrows, Escape puts back), and a press on a handle then a press on another item drops it there. `<VirtualList items={rows} itemKey={(row) => row.id}>{(row) => <List.Item>…</List.Item>}</VirtualList>` (`components/virtualList`) renders a window, measuring each item by key; the scroller is the nearest scrolling ancestor or the page, `scrollerRef` when that is wrong, and `onRangeChange` fetches the next page. **Three things to know.** The children function returns exactly one `<li>` per item and nothing around it, since the list measures its children. A windowed list has no `List.Group`s and find-in-page cannot see what is not rendered. And a `SwipeItem` windows like any item, but a `SortableList` holds its whole list: reorder a list short enough to render, window one too long to.
|
|
167
|
+
|
|
160
168
|
Full reference: `references/` beside this file, and `docs/props.md` inside the package.
|
|
@@ -18,6 +18,7 @@ Which component replaces which `<Box tag>`, and the three that carry a pattern o
|
|
|
18
18
|
| a menu button and its menu | `<Menu>` | `components/menu` |
|
|
19
19
|
| sections that open one at a time | `<Accordion>` | `components/accordion` |
|
|
20
20
|
| one "show more" disclosure | `<Collapsible>` | `components/accordion` |
|
|
21
|
+
| rows to read and pick from | `<List>` | `components/list` |
|
|
21
22
|
| a value dragged along a track | `<Slider>` | `components/slider` |
|
|
22
23
|
| how far a task has got | `<Progress>` | `components/progress` |
|
|
23
24
|
| a message sent from anywhere | `<Toaster>` + `toast()` | `components/toaster` |
|
|
@@ -67,6 +68,51 @@ own tab stop; Down/Up/Home/End are a shortcut. Style the open state with `ariaAt
|
|
|
67
68
|
never a variant, and leave `accordion.track`/`accordion.clip` alone: padding on a grid item cannot be
|
|
68
69
|
squeezed, so it floors the `0fr` track — which is why your padded panel sits inside the clip (#142).
|
|
69
70
|
|
|
71
|
+
## List
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import List from '@box-kite/react/components/list';
|
|
75
|
+
|
|
76
|
+
<List variant="card">
|
|
77
|
+
<List.Group label="Today" level={3} sticky>
|
|
78
|
+
<List.Item chevron>
|
|
79
|
+
<List.Media variant="avatar">AL</List.Media>
|
|
80
|
+
<List.Content>
|
|
81
|
+
<List.Row>
|
|
82
|
+
<List.Title lines={1}>
|
|
83
|
+
<List.Action props={{ href: '/inbox/42' }}>Ada Lovelace</List.Action>
|
|
84
|
+
</List.Title>
|
|
85
|
+
<List.Meta>9:41</List.Meta>
|
|
86
|
+
</List.Row>
|
|
87
|
+
<List.Row secondary>Re: the engine</List.Row>
|
|
88
|
+
<List.Description lines={2}>It might compose elaborate pieces of music.</List.Description>
|
|
89
|
+
</List.Content>
|
|
90
|
+
</List.Item>
|
|
91
|
+
</List.Group>
|
|
92
|
+
</List>;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`List`: `variant` (`plain`, `card`, `separated`; dividers by default), `tag="ol"`. `List.Item`: `size` (`sm` 44px, `md` 56px,
|
|
96
|
+
`lg` 72px), `wrap`, `selected`, `chevron`. `List.Row`: `wrap`, `secondary`. `List.Title`/`Description`/`Overline`: `lines`
|
|
97
|
+
(`1` ellipsis, `2`/`3` clamp). `List.Media`: `variant` `icon`/`avatar`/`image`. `List.Field`: `label`. `List.Group`:
|
|
98
|
+
`label`, `level`, `sticky`. `List.Content`, `List.Meta`, `List.End`, `List.Actions`, `List.Fields`: every Box prop.
|
|
99
|
+
|
|
100
|
+
`List.Action` goes in the title and covers the item: `props={{ href }}` is a link, `props={{ htmlFor }}` the label of a
|
|
101
|
+
checkbox in `List.Actions` (a pick-list), otherwise a button with `onClick`. Anything else interactive goes in
|
|
102
|
+
`List.Actions`, which sits above the action. No hook and no context — it renders on a server — so the look reaches
|
|
103
|
+
the items as `--list-divider`, `--list-divider-inset`, `--list-item-frame`, `--list-item-radius` and
|
|
104
|
+
`--list-sticky-top`, set with `vars`. The tree is paint only (`list`, `list.item`, `list.media`, `list.muted`,
|
|
105
|
+
`list.action`, `list.label`); the spacing is the parts' own default props, which a prop on the part beats.
|
|
106
|
+
|
|
107
|
+
Three client-only entries build on it. `SwipeItem` (`components/swipeItem`) is a `List.Item` whose row slides aside:
|
|
108
|
+
`startActions`/`endActions` of `SwipeItem.Action` (`variant` `accent`/`warning`/`danger`, `icon`), `fullSwipe` to
|
|
109
|
+
perform the outermost by swiping across, `open`/`onOpenChange`; the actions are tab stops and focus opens their tray.
|
|
110
|
+
`SortableList` (`components/sortableList`) reorders `SortableList.Item`s (`value`, `label`) by a `SortableList.Handle`:
|
|
111
|
+
the app owns the order, `onOrderChange(order, { reason, value, from, to })` fires on the drop and on each keyboard step
|
|
112
|
+
(Enter/Space grabs, arrows move, Escape puts back), `onOrderCommit` once it settles. `VirtualList`
|
|
113
|
+
(`components/virtualList`) windows `items` through a `children(item, index)` that returns one `<li>`, measuring each
|
|
114
|
+
item by `itemKey`; `estimateSize`, `overscan`, `onRangeChange`, `scrollerRef`. `List.Chevron` is the chevron as a part.
|
|
115
|
+
|
|
70
116
|
## Slider and Progress
|
|
71
117
|
|
|
72
118
|
```tsx
|
|
@@ -229,12 +275,12 @@ import DataGrid from '@box-kite/react/components/dataGrid';
|
|
|
229
275
|
|
|
230
276
|
**DataGridProps**: `data`, `def`, `component` (default `'datagrid'`), `loading`, `filters` (predicate[]), `page`/`onPageChange`, `onSortChange`, `onServerStateChange` (`{ page, pageSize, sortColumn, sortDirection, columnFilters, globalFilterValue }`), `onSelectionChange` (`{ action, selectedRowKeys, affectedRowKeys, isAllSelected }`), `expandedRowKeys`/`onExpandedRowKeysChange`, `globalFilterValue`/`onGlobalFilterChange`, `columnFilters`/`onColumnFiltersChange`, `onCellEditsChange` (`edit`/`paste`/`clear`), `onRangeChange` (`(range, { reason })` with `select`/`extend`/`clear`; `range` is `{ startRow, endRow, columns, values() }` or `undefined`), `onPaste` (`{ range, applied, rejected, skipped }`, once per Ctrl+V).
|
|
231
277
|
|
|
232
|
-
**GridDefinition**: `columns` (required), `rowKey`, `rowHeight` (px, default 48), `visibleRowsCount` (number/`'all'`), `showRowNumber` (bool/`{ pinned?, width? }`), `rowSelection` (bool/`{ pinned? }` — the header checkbox acts on the rows the filters leave on screen, and the bottom bar counts selected rows the filters hide, with Show selected and Clear selection), `rowDetail` (`{ content, height?, expandOnRowClick?, pinned?, expandColumnWidth?, expandColumnHeader? }`), `pagination` (`{ totalCount, pageSize? }`), `topBar`/`bottomBar`, `title`/`topBarContent`, `globalFilter`, `globalFilterKeys`, `sortable`/`resizable` (default true), `contextMenu` (bool/`{ sort?, pin?, group? }`, default true), `resizerStyle` (`'visible'`/`'hover'`/`'hidden'`), `headerHover` (`'static'` — an inset pill per sortable cell — or `'sliding'`, one pill that flows between cells), `localeText` (any subset of `DataGridLocaleText` — every word the grid writes, for a translation; values with a number or a name in them are functions, and `DATA_GRID_LOCALE_TEXT` is the English to spread over), `noDataComponent`, `editable`/`onCellEdit`, `tabNavigation` (`'none'` default — APG's single tab stop — or `'cells'`, the spreadsheet reading), `rangeSelection` (a drag or Shift+arrow marks a block of cells; `Ctrl+C` copies it as TSV and `Ctrl+V` fills it back through `onCellEdit`, one judgement per cell — both need no opt-in for the current cell alone).
|
|
278
|
+
**GridDefinition**: `columns` (required), `rowKey`, `rowHeight` (px, default 48), `visibleRowsCount` (number/`'all'`), `showRowNumber` (bool/`{ pinned?, width? }`), `rowSelection` (bool/`{ pinned? }` — the header checkbox acts on the rows the filters leave on screen, and the bottom bar counts selected rows the filters hide, with Show selected and Clear selection), `rowDetail` (`{ content, height?, expandOnRowClick?, pinned?, expandColumnWidth?, expandColumnHeader? }`), `pagination` (`{ totalCount, pageSize? }`), `topBar`/`bottomBar`, `title`/`topBarContent`, `globalFilter`, `globalFilterKeys`, `sortable`/`resizable` (default true), `contextMenu` (bool/`{ sort?, pin?, group? }`, default true), `resizerStyle` (`'visible'`/`'hover'`/`'hidden'`), `headerHover` (`'static'` — an inset pill per sortable cell — or `'sliding'`, one pill that flows between cells), `localeText` (any subset of `DataGridLocaleText` — every word the grid writes, for a translation; values with a number or a name in them are functions, and `DATA_GRID_LOCALE_TEXT` is the English to spread over), `noDataComponent`, `editable`/`onCellEdit`, `tabNavigation` (`'none'` default — APG's single tab stop — or `'cells'`, the spreadsheet reading), `layout` (`'auto'` default — a list item per row once the grid's own width is under `list.breakpoint`, 576px; `'table'`/`'list'` choose for good — rule 51), `list` (`{ breakpoint?, fields?, batchSize?, recordView? }`), `rangeSelection` (a drag or Shift+arrow marks a block of cells; `Ctrl+C` copies it as TSV and `Ctrl+V` fills it back through `onCellEdit`, one judgement per cell — both need no opt-in for the current cell alone).
|
|
233
279
|
|
|
234
|
-
**ColumnType**: `key`, `header`, `width` (px, default 200), `align` (`'start'`/`'end'`/`'center'`, or the physical `'left'`/`'right'`), `pin` (`'START'`/`'END'` — an edge of the **inline axis**, so a pinned column holds under either `dir`; `'LEFT'`/`'RIGHT'` are the older spelling of the same two), `columns` (grouped headers), `Cell` (`({ cell }) => ReactNode`), `sortable`/`resizable` (override grid), `flexible`, `filterable` (`true`=text, `{ type: 'number' }`, `{ type: 'multiselect' }`), `contextMenu` (override grid).
|
|
280
|
+
**ColumnType**: `key`, `header`, `width` (px, default 200), `align` (`'start'`/`'end'`/`'center'`, or the physical `'left'`/`'right'`), `pin` (`'START'`/`'END'` — an edge of the **inline axis**, so a pinned column holds under either `dir`; `'LEFT'`/`'RIGHT'` are the older spelling of the same two), `columns` (grouped headers), `Cell` (`({ cell }) => ReactNode`), `sortable`/`resizable` (override grid), `flexible`, `filterable` (`true`=text, `{ type: 'number' }`, `{ type: 'multiselect' }`), `contextMenu` (override grid), `list` (`'title'`/`'kicker'`/`'subtitle'`/`'end'`/`'field'`/`'detail'`/`'hidden'` — where it goes in a list item).
|
|
235
281
|
|
|
236
282
|
**Server-side**: `def={{ pagination: { totalCount }, bottomBar: true }}` + `page={page}` + `onServerStateChange={fetchData}`.
|
|
237
283
|
|
|
238
284
|
**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
285
|
|
|
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`).
|
|
286
|
+
**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`, `list` > (`toolbar`, `toggle`, `detail`, `button`, `selectionBar`, `sheet`) — its rows are `List.Item`s, styled by the `list` tree, `bottomBar` > (`info`, `clearFilters`, `pagination` > `button`|`info`).
|
|
@@ -92,4 +92,12 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
|
|
|
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
|
|
|
95
|
+
49. **A design that stops at a subtree is `Box.createComponents()`, not `Box.components()`.** `Box.components()` is one registry for the whole app: a second call merges into the first, so two designs cannot share a page through it. `const aurora = Box.createComponents({ button: { styles: { borderRadius: 999 } } })` takes the same shape and registers nothing, and `<Box.Components value={aurora}>` — or `<Box.Theme use="local" components={aurora}>`, a design in light and dark in one wrapper — applies it to every Box inside, pre-built components included. `base` is what it merges over: `'global'` (default, the app's own `Box.components()`), `'builtin'` (ignores them) or another registry. Props still beat the design. It is identified by its content, so an inline one costs no cache, but declare it at module scope. The provider renders no element and a Server Component cannot read it. Two designs that both name `button` type as `interface ComponentsTypes extends MergeComponentsAndVariants<[typeof aurora, typeof harbor]> {}`.
|
|
96
|
+
|
|
97
|
+
50. **A list of things to read and pick from is `<List>`, and one action covers each item.** `<List><List.Item chevron><List.Media variant="avatar">AL</List.Media><List.Content><List.Row><List.Title lines={1}><List.Action props={{ href: '/inbox/42' }}>Ada Lovelace</List.Action></List.Title><List.Meta>9:41</List.Meta></List.Row><List.Description lines={2}>…</List.Description></List.Content></List.Item></List>` (`components/list`). **An item is a flex row of three slots** — `List.Media` leading (`variant` `icon`/`avatar`/`image`), `List.Content` taking the room, `List.End` trailing (a value over its label, aligned to the end) — and the content is a column of `List.Row`s, each its parts side by side with the last pushed to the end: a main row (the title and its time) and a `secondary` one under it (smaller, muted). `wrap` on a row or an item lets the parts wrap instead of squeezing; `List.Overline`/`List.Title`/`List.Description` take `lines={1|2|3}` (an ellipsis, then a clamp); `List.Meta` is a small value that never wraps; `List.Fields` + `List.Field label` is a `<dl>` of labelled values; `List.Group label level sticky` is a section with a nested list of its own. **`List.Action` goes in the title and its `::after` covers the item**, so the whole row presses while the name a reader hears is the title alone — an `href` in `props` makes it an `<a>`, an `htmlFor` a `<label>` (a press anywhere ticks that checkbox), neither a `<button>` with a top-level `onClick`; a control of the item's own (a switch, a menu, that checkbox) goes in `List.Actions`, which sits above it. **Four things to know.** It renders on a server — no hook, no context — so the list's look reaches its items as **custom properties**: `variant="plain"|"card"|"separated"` on `List`, and `vars={{ 'list-divider': …, 'list-divider-inset': '4.5rem', 'list-item-radius': …, 'list-sticky-top': '4rem' }}` to tune it. The style tree is **paint only** (`list`, `list.item`, `list.media`, `list.muted`, `list.action`, `list.label`), the hairlines and tints mixed from `currentColor`; the spacing is each part's default props, so `<List.Item py={1}>` wins and `Box.components` cannot move it. Hover, pressed, `aria-current` and disabled are read off the action with `:has()` — no prop — and `selected` on an item is a tint only, the state a reader hears belonging on the control. And it is a `<ul role="list">` (Safari drops the semantics otherwise; `tag="ol"` for an ordered one), **not a composite widget**: every action is a tab stop and the arrows stay the page's — arrow-key selection among options is a listbox, `Combobox` or `Select`.
|
|
98
|
+
|
|
99
|
+
51. **On a phone a DataGrid is a list, and a column says where it goes in an item.** Below `def.list.breakpoint` (576px) of **the grid's own width** — not the viewport's — `layout: 'auto'` (the default) draws each row as a list item: `list: 'title'` is its name and the button that opens it, `'kicker'` a small line above, `'subtitle'` a line under, `'end'` the one value opposite the title with its column's name under it, `'field'` a labelled value, `'detail'` the record view only, `'hidden'` nowhere. Left unset, the first column is the title, the first end-aligned one the end, the next four fields (`def.list.fields`) and the rest detail. A press opens the **record view** — every column of one row in a bottom sheet with a pager (buttons, Left/Right, a swipe) that follows the list's sort and filters, and an **Edit** button on each editable column through the same `onCellEdit`. Sort and filters are bottom sheets behind two pills, the filters in force are removable chips, **Select** is a mode (a press ticks instead of opening, and a bar pinned to the bottom counts the selection), and the list grows twenty a **Show more** — a button, so the totals and the page below stay reachable — or pages when the host paginates. **Four things to know.** It is a `<List>` (rule 50) — each row a `List.Item` whose title is the `List.Action` — not a restyled `role="grid"`, so the `list` tree styles its rows. It is a chunk of its own, loaded the first time a grid is narrow, and the grid draws its table until it arrives — in a test, `await loadListLayout()` first. Unmeasured is a table, so a server prerenders the table and `layout: 'list'` is how to say otherwise. And the selection bar sticks to the viewport, so no ancestor may be `overflow: hidden`/`auto` — `css={{ overflow: 'clip' }}` clips without being a scroll container. `layout: 'table'` keeps the table at every width.
|
|
100
|
+
|
|
101
|
+
52. **Swiping, reordering and windowing a list are three client entries on top of `List`.** `<SwipeItem endActions={<SwipeItem.Action variant="danger" onClick={remove}>Delete</SwipeItem.Action>} fullSwipe>` (`components/swipeItem`) is a `List.Item` — same parts, `size`, `chevron`, `selected` — whose row slides to open a tray; the first action of each tray is the outermost, and `fullSwipe` performs it by clicking it, so its `onClick` is where the command lives. The actions are tab stops and focus opens their tray, but a swipe is a path gesture: put the same commands where a press reaches too. `<SortableList onOrderChange={(order) => setIds(order)}>` (`components/sortableList`) with `<SortableList.Item value={id} label={name}>` and a `<SortableList.Handle />` in each: **the app owns the order** and renders it, `onOrderChange` fires once per drop and per keyboard step, `onOrderCommit` when it settles; the keyboard grabs (Enter/Space, arrows, Escape puts back), and a press on a handle then a press on another item drops it there. `<VirtualList items={rows} itemKey={(row) => row.id}>{(row) => <List.Item>…</List.Item>}</VirtualList>` (`components/virtualList`) renders a window, measuring each item by key; the scroller is the nearest scrolling ancestor or the page, `scrollerRef` when that is wrong, and `onRangeChange` fetches the next page. **Three things to know.** The children function returns exactly one `<li>` per item and nothing around it, since the list measures its children. A windowed list has no `List.Group`s and find-in-page cannot see what is not rendered. And a `SwipeItem` windows like any item, but a `SortableList` holds its whole list: reorder a list short enough to render, window one too long to.
|
|
102
|
+
|
|
95
103
|
Full reference: `node_modules/@box-kite/react/docs/props.md` in the project, or https://www.box-kite.dev/props.md — every prop, the CSS it writes and one measured example.
|
package/AGENTS.md
CHANGED
|
@@ -124,4 +124,12 @@ Every value there is a prop, and every prop value becomes one class the engine i
|
|
|
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
|
|
|
127
|
+
49. **A design that stops at a subtree is `Box.createComponents()`, not `Box.components()`.** `Box.components()` is one registry for the whole app: a second call merges into the first, so two designs cannot share a page through it. `const aurora = Box.createComponents({ button: { styles: { borderRadius: 999 } } })` takes the same shape and registers nothing, and `<Box.Components value={aurora}>` — or `<Box.Theme use="local" components={aurora}>`, a design in light and dark in one wrapper — applies it to every Box inside, pre-built components included. `base` is what it merges over: `'global'` (default, the app's own `Box.components()`), `'builtin'` (ignores them) or another registry. Props still beat the design. It is identified by its content, so an inline one costs no cache, but declare it at module scope. The provider renders no element and a Server Component cannot read it. Two designs that both name `button` type as `interface ComponentsTypes extends MergeComponentsAndVariants<[typeof aurora, typeof harbor]> {}`.
|
|
128
|
+
|
|
129
|
+
50. **A list of things to read and pick from is `<List>`, and one action covers each item.** `<List><List.Item chevron><List.Media variant="avatar">AL</List.Media><List.Content><List.Row><List.Title lines={1}><List.Action props={{ href: '/inbox/42' }}>Ada Lovelace</List.Action></List.Title><List.Meta>9:41</List.Meta></List.Row><List.Description lines={2}>…</List.Description></List.Content></List.Item></List>` (`components/list`). **An item is a flex row of three slots** — `List.Media` leading (`variant` `icon`/`avatar`/`image`), `List.Content` taking the room, `List.End` trailing (a value over its label, aligned to the end) — and the content is a column of `List.Row`s, each its parts side by side with the last pushed to the end: a main row (the title and its time) and a `secondary` one under it (smaller, muted). `wrap` on a row or an item lets the parts wrap instead of squeezing; `List.Overline`/`List.Title`/`List.Description` take `lines={1|2|3}` (an ellipsis, then a clamp); `List.Meta` is a small value that never wraps; `List.Fields` + `List.Field label` is a `<dl>` of labelled values; `List.Group label level sticky` is a section with a nested list of its own. **`List.Action` goes in the title and its `::after` covers the item**, so the whole row presses while the name a reader hears is the title alone — an `href` in `props` makes it an `<a>`, an `htmlFor` a `<label>` (a press anywhere ticks that checkbox), neither a `<button>` with a top-level `onClick`; a control of the item's own (a switch, a menu, that checkbox) goes in `List.Actions`, which sits above it. **Four things to know.** It renders on a server — no hook, no context — so the list's look reaches its items as **custom properties**: `variant="plain"|"card"|"separated"` on `List`, and `vars={{ 'list-divider': …, 'list-divider-inset': '4.5rem', 'list-item-radius': …, 'list-sticky-top': '4rem' }}` to tune it. The style tree is **paint only** (`list`, `list.item`, `list.media`, `list.muted`, `list.action`, `list.label`), the hairlines and tints mixed from `currentColor`; the spacing is each part's default props, so `<List.Item py={1}>` wins and `Box.components` cannot move it. Hover, pressed, `aria-current` and disabled are read off the action with `:has()` — no prop — and `selected` on an item is a tint only, the state a reader hears belonging on the control. And it is a `<ul role="list">` (Safari drops the semantics otherwise; `tag="ol"` for an ordered one), **not a composite widget**: every action is a tab stop and the arrows stay the page's — arrow-key selection among options is a listbox, `Combobox` or `Select`.
|
|
130
|
+
|
|
131
|
+
51. **On a phone a DataGrid is a list, and a column says where it goes in an item.** Below `def.list.breakpoint` (576px) of **the grid's own width** — not the viewport's — `layout: 'auto'` (the default) draws each row as a list item: `list: 'title'` is its name and the button that opens it, `'kicker'` a small line above, `'subtitle'` a line under, `'end'` the one value opposite the title with its column's name under it, `'field'` a labelled value, `'detail'` the record view only, `'hidden'` nowhere. Left unset, the first column is the title, the first end-aligned one the end, the next four fields (`def.list.fields`) and the rest detail. A press opens the **record view** — every column of one row in a bottom sheet with a pager (buttons, Left/Right, a swipe) that follows the list's sort and filters, and an **Edit** button on each editable column through the same `onCellEdit`. Sort and filters are bottom sheets behind two pills, the filters in force are removable chips, **Select** is a mode (a press ticks instead of opening, and a bar pinned to the bottom counts the selection), and the list grows twenty a **Show more** — a button, so the totals and the page below stay reachable — or pages when the host paginates. **Four things to know.** It is a `<List>` (rule 50) — each row a `List.Item` whose title is the `List.Action` — not a restyled `role="grid"`, so the `list` tree styles its rows. It is a chunk of its own, loaded the first time a grid is narrow, and the grid draws its table until it arrives — in a test, `await loadListLayout()` first. Unmeasured is a table, so a server prerenders the table and `layout: 'list'` is how to say otherwise. And the selection bar sticks to the viewport, so no ancestor may be `overflow: hidden`/`auto` — `css={{ overflow: 'clip' }}` clips without being a scroll container. `layout: 'table'` keeps the table at every width.
|
|
132
|
+
|
|
133
|
+
52. **Swiping, reordering and windowing a list are three client entries on top of `List`.** `<SwipeItem endActions={<SwipeItem.Action variant="danger" onClick={remove}>Delete</SwipeItem.Action>} fullSwipe>` (`components/swipeItem`) is a `List.Item` — same parts, `size`, `chevron`, `selected` — whose row slides to open a tray; the first action of each tray is the outermost, and `fullSwipe` performs it by clicking it, so its `onClick` is where the command lives. The actions are tab stops and focus opens their tray, but a swipe is a path gesture: put the same commands where a press reaches too. `<SortableList onOrderChange={(order) => setIds(order)}>` (`components/sortableList`) with `<SortableList.Item value={id} label={name}>` and a `<SortableList.Handle />` in each: **the app owns the order** and renders it, `onOrderChange` fires once per drop and per keyboard step, `onOrderCommit` when it settles; the keyboard grabs (Enter/Space, arrows, Escape puts back), and a press on a handle then a press on another item drops it there. `<VirtualList items={rows} itemKey={(row) => row.id}>{(row) => <List.Item>…</List.Item>}</VirtualList>` (`components/virtualList`) renders a window, measuring each item by key; the scroller is the nearest scrolling ancestor or the page, `scrollerRef` when that is wrong, and `onRangeChange` fetches the next page. **Three things to know.** The children function returns exactly one `<li>` per item and nothing around it, since the list measures its children. A windowed list has no `List.Group`s and find-in-page cannot see what is not rendered. And a `SwipeItem` windows like any item, but a `SortableList` holds its whole list: reorder a list short enough to render, window one too long to.
|
|
134
|
+
|
|
127
135
|
Full reference: `docs/props.md` and `BOX_KITE_AI_CONTEXT.md`, in this package.
|
package/BOX_AI_CONTEXT.md
CHANGED
|
@@ -1064,6 +1064,36 @@ Box.components({
|
|
|
1064
1064
|
});
|
|
1065
1065
|
```
|
|
1066
1066
|
|
|
1067
|
+
`extends` is resolved over everything registered so far, chains included, so overriding the base later still reaches the component built on it.
|
|
1068
|
+
|
|
1069
|
+
**A prop on the element wins in every theme.** `<Box component="card" bgColor="red-500">` replaces the card's `bgColor` and also its `theme.dark.bgColor` (at the same level: `hover.bgColor` replaces the themed hover). Everything else the card styles for dark stays. To keep a different dark value, write it: `theme={{ dark: { bgColor: 'red-900' } }}`.
|
|
1070
|
+
|
|
1071
|
+
### Box.createComponents() — A Design for One Subtree
|
|
1072
|
+
|
|
1073
|
+
`Box.components()` styles the whole app: every call merges into one registry, so two designs cannot share a page through it. `Box.createComponents()` takes the same shape and registers nothing. It returns a design, and `<Box.Components value>`, or `components` on `Box.Theme`, applies it to everything inside.
|
|
1074
|
+
|
|
1075
|
+
```tsx
|
|
1076
|
+
const aurora = Box.createComponents({ button: { styles: { borderRadius: 999 } }, card: { styles: { p: 6 } } });
|
|
1077
|
+
const harbor = Box.createComponents({ button: { styles: { borderRadius: 0 } } }, { base: 'builtin' });
|
|
1078
|
+
|
|
1079
|
+
<Box.Theme use="local" components={aurora}> {/* light/dark + design in one wrapper */}
|
|
1080
|
+
<Button>Pill</Button>
|
|
1081
|
+
</Box.Theme>
|
|
1082
|
+
<Box.Components value={harbor}>
|
|
1083
|
+
<Button>Square</Button>
|
|
1084
|
+
</Box.Components>
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
- **`base`** is what the design is merged over:
|
|
1088
|
+
- `'global'` (the default) is the app's own `Box.components()`, followed as it changes.
|
|
1089
|
+
- `'builtin'` ignores the app's registrations.
|
|
1090
|
+
- Another registry chains onto that design.
|
|
1091
|
+
- **Props still beat the design**, the way they beat `Box.components()`.
|
|
1092
|
+
- **A design is identified by its content**, so building one inline does not grow the cache. Module scope still reads best.
|
|
1093
|
+
- **`<Box.Components>` renders no element.** `value={undefined}` hands a subtree back to the app's design.
|
|
1094
|
+
- **A Server Component cannot read the provider.** A client component under it can.
|
|
1095
|
+
- **Types for two designs that both name a component**: `interface ComponentsTypes extends MergeComponentsAndVariants<[typeof aurora, typeof harbor]> {}` (from `@box-kite/core/types`). Extending two `ExtractComponentsAndVariants` that disagree on `button` is an interface conflict.
|
|
1096
|
+
|
|
1067
1097
|
---
|
|
1068
1098
|
|
|
1069
1099
|
## Extension System
|
|
@@ -1891,6 +1921,170 @@ on something at the panel's edge needs, since the clip is permanent.
|
|
|
1891
1921
|
|
|
1892
1922
|
---
|
|
1893
1923
|
|
|
1924
|
+
## List (`components/list`)
|
|
1925
|
+
|
|
1926
|
+
```tsx
|
|
1927
|
+
import List from '@box-kite/react/components/list';
|
|
1928
|
+
|
|
1929
|
+
<List>
|
|
1930
|
+
{people.map((person) => (
|
|
1931
|
+
<List.Item key={person.id} chevron>
|
|
1932
|
+
<List.Media variant="avatar">{person.initials}</List.Media>
|
|
1933
|
+
<List.Content>
|
|
1934
|
+
<List.Row>
|
|
1935
|
+
<List.Title lines={1}>
|
|
1936
|
+
<List.Action props={{ href: `/people/${person.id}` }}>{person.name}</List.Action>
|
|
1937
|
+
</List.Title>
|
|
1938
|
+
<List.Meta>{person.lastSeen}</List.Meta>
|
|
1939
|
+
</List.Row>
|
|
1940
|
+
<List.Description lines={2}>{person.bio}</List.Description>
|
|
1941
|
+
</List.Content>
|
|
1942
|
+
</List.Item>
|
|
1943
|
+
))}
|
|
1944
|
+
</List>;
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
A list of things to read and pick from — messages, orders, people, settings. **An item is a flex row of
|
|
1948
|
+
three slots**, and the content is a column of rows:
|
|
1949
|
+
|
|
1950
|
+
| Part | Element | What it is |
|
|
1951
|
+
| ------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------- |
|
|
1952
|
+
| `List` | `<ul role="list">` | `variant`: dividers by default, `plain`, `card` (one framed block), `separated` (a frame per item); `tag="ol"` |
|
|
1953
|
+
| `List.Item` | `<li>` | A flex row. `size` `sm`/`md`/`lg` (44/56/72px), `wrap`, `selected` (a tint), `chevron` |
|
|
1954
|
+
| `List.Media` | `<div>` | Leading: `variant` `icon` (a tinted square), `avatar` (a circle that clips), `image` (a thumbnail) |
|
|
1955
|
+
| `List.Content` | `<div>` | The middle, taking the room; a column |
|
|
1956
|
+
| `List.Row` | `<div>` | Parts side by side, the last at the end. `secondary` (smaller, muted), `wrap` |
|
|
1957
|
+
| `List.Overline` | `<span>` | A small line above the title. `lines` |
|
|
1958
|
+
| `List.Title` | `<span>` | The name, holding the `List.Action`. `lines` `1` (ellipsis), `2`/`3` (clamp); unset, it wraps |
|
|
1959
|
+
| `List.Description` | `<span>` | Muted supporting text. `lines` |
|
|
1960
|
+
| `List.Meta` | `<span>` | A small value that never wraps: a time, a count |
|
|
1961
|
+
| `List.End` | `<div>` | Trailing: a value over its label, aligned to the end in tabular figures |
|
|
1962
|
+
| `List.Actions` | `<div>` | Controls of the item's own, lifted above the action |
|
|
1963
|
+
| `List.Action` | `<a>` / `<label>` / `<button>` | The primary action, covering the item: `href` → link, `htmlFor` → label, otherwise a button |
|
|
1964
|
+
| `List.Fields` | `<dl>` | Labelled values, as many to a line as fit at 8rem |
|
|
1965
|
+
| `List.Field` | `<div>` + `<dt>` + `<dd>` | `label` and the value |
|
|
1966
|
+
| `List.Group` | `<li>` + `<ul>` | A section: `label`, `level` (makes the label a heading), `sticky` |
|
|
1967
|
+
| `List.Chevron` | `<span aria-hidden>` | The chevron `chevron` draws, for an item something else renders |
|
|
1968
|
+
|
|
1969
|
+
**One action covers the item.** `List.Action`'s `::after` stretches over the whole `<li>`, so a press
|
|
1970
|
+
anywhere on the item is a press on it and the focus ring draws round the item — while its accessible
|
|
1971
|
+
name is the title alone, not every value in the row. A pick-list is a label: put the checkbox in
|
|
1972
|
+
`List.Actions` and give the action its `htmlFor`.
|
|
1973
|
+
|
|
1974
|
+
```tsx
|
|
1975
|
+
<List.Item selected={picked}>
|
|
1976
|
+
<List.Actions>
|
|
1977
|
+
<Checkbox id="pick-ada" checked={picked} onChange={toggle} />
|
|
1978
|
+
</List.Actions>
|
|
1979
|
+
<List.Title>
|
|
1980
|
+
<List.Action props={{ htmlFor: 'pick-ada' }}>Ada Lovelace</List.Action>
|
|
1981
|
+
</List.Title>
|
|
1982
|
+
</List.Item>
|
|
1983
|
+
```
|
|
1984
|
+
|
|
1985
|
+
Hover, pressed, current (`aria-current` on the action) and disabled are read off the action with
|
|
1986
|
+
`:has()`, so an item with no action is plain text and needs no prop to say so.
|
|
1987
|
+
|
|
1988
|
+
**It renders on a server**: no hook, no context, no effect. That is why the list's look reaches its
|
|
1989
|
+
items as custom properties rather than through React — and they are yours to set with `vars`:
|
|
1990
|
+
|
|
1991
|
+
| Variable | Default | What it is |
|
|
1992
|
+
| ---------------------- | --------------------- | ------------------------------------------------------------- |
|
|
1993
|
+
| `--list-divider` | `currentColor` at 12% | The line between items; `plain` and `separated` make it clear |
|
|
1994
|
+
| `--list-divider-inset` | `0px` | Where the line starts — `4.5rem` lines it up with the text |
|
|
1995
|
+
| `--list-item-frame` | `none` | The border round each item, which `separated` sets |
|
|
1996
|
+
| `--list-item-radius` | `0` | Each item's corners, which its tint follows |
|
|
1997
|
+
| `--list-sticky-top` | `0` | How far down a sticky group label stops — an app's header |
|
|
1998
|
+
|
|
1999
|
+
**The style tree is paint only** — `list` (the three variants), `list.item` (the divider colour, the
|
|
2000
|
+
states, `selected`), `list.media`, `list.muted`, `list.action` (the focus ring) and `list.label` —
|
|
2001
|
+
because every engine entry carries it. The spacing is each part's default props, so a prop on the part
|
|
2002
|
+
beats it (`<List.Item py={1}>`) and `Box.components` cannot move it. The hairlines and tints are mixed
|
|
2003
|
+
from `currentColor`, so they follow any theme with no rule for it.
|
|
2004
|
+
|
|
2005
|
+
It is a `<ul role="list">` — the role spelled out because Safari stops reporting a list whose items have
|
|
2006
|
+
lost their markers — and **not a composite widget**: each action is a tab stop, Enter follows or presses
|
|
2007
|
+
it, and the arrow keys stay the page's. Arrow-key selection among options is a listbox: `Combobox` or
|
|
2008
|
+
`Select`.
|
|
2009
|
+
|
|
2010
|
+
### Swipe actions, reorder and a long list
|
|
2011
|
+
|
|
2012
|
+
Three client-only entries on top of `List`, so `List` itself stays server-safe. Each item they draw is a
|
|
2013
|
+
`List.Item` in everything but name — same parts, same sizes, same `list.item` paint.
|
|
2014
|
+
|
|
2015
|
+
```tsx
|
|
2016
|
+
import SwipeItem from '@box-kite/react/components/swipeItem';
|
|
2017
|
+
import SortableList from '@box-kite/react/components/sortableList';
|
|
2018
|
+
import VirtualList from '@box-kite/react/components/virtualList';
|
|
2019
|
+
|
|
2020
|
+
<SwipeItem
|
|
2021
|
+
fullSwipe
|
|
2022
|
+
startActions={
|
|
2023
|
+
<SwipeItem.Action variant="accent" onClick={pin}>
|
|
2024
|
+
Pin
|
|
2025
|
+
</SwipeItem.Action>
|
|
2026
|
+
}
|
|
2027
|
+
endActions={
|
|
2028
|
+
<SwipeItem.Action variant="danger" onClick={remove}>
|
|
2029
|
+
Delete
|
|
2030
|
+
</SwipeItem.Action>
|
|
2031
|
+
}
|
|
2032
|
+
>
|
|
2033
|
+
<List.Content>…</List.Content>
|
|
2034
|
+
</SwipeItem>;
|
|
2035
|
+
|
|
2036
|
+
<SortableList onOrderChange={(order) => setIds(order)} onOrderCommit={save}>
|
|
2037
|
+
{ids.map((id) => (
|
|
2038
|
+
<SortableList.Item key={id} value={id} label={names[id]}>
|
|
2039
|
+
<List.Content>…</List.Content>
|
|
2040
|
+
<SortableList.Handle />
|
|
2041
|
+
</SortableList.Item>
|
|
2042
|
+
))}
|
|
2043
|
+
</SortableList>;
|
|
2044
|
+
|
|
2045
|
+
<VirtualList items={messages} itemKey={(message) => message.id} onRangeChange={loadMore}>
|
|
2046
|
+
{(message) => <List.Item>…</List.Item>}
|
|
2047
|
+
</VirtualList>;
|
|
2048
|
+
```
|
|
2049
|
+
|
|
2050
|
+
| Component | Props |
|
|
2051
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2052
|
+
| `SwipeItem` | `startActions`, `endActions` (the first of each is outermost), `fullSwipe`, `open`/`defaultOpen`/`onOpenChange`, and `List.Item`'s `size`/`wrap`/`selected`/`chevron` |
|
|
2053
|
+
| `SwipeItem.Action` | A `<button>`: `variant` `accent`/`warning`/`danger` (unset is neutral), `icon`, `onClick`, `closeOnSelect` (default `true`) |
|
|
2054
|
+
| `SortableList` | Everything `List` takes, `onOrderChange(order, { reason, value, from, to })`, `onOrderCommit` (the drop — the one to save) |
|
|
2055
|
+
| `SortableList.Item` | Everything `List.Item` takes, `value` (required, unique), `label` (what the handle and announcements call it), `disabled` |
|
|
2056
|
+
| `SortableList.Handle` | The `<button>` a drag starts from; six dots unless given children; named "Move" + the item's `label` |
|
|
2057
|
+
| `VirtualList` | Everything `List` takes, `items`, `itemKey`, `children` (a function returning one `<li>`), `estimateSize` (57), `overscan` (6), `initialCount` (20), `onRangeChange`, `scrollerRef` |
|
|
2058
|
+
|
|
2059
|
+
**`SwipeItem`'s row slides and a tray opens in the space it leaves** — nothing is under the row, so it
|
|
2060
|
+
needs no background. Past half open opens, short of it closes, a flick goes the way it was going, and
|
|
2061
|
+
`fullSwipe` carried most of the way across performs the outermost action (it clicks it, so its `onClick`
|
|
2062
|
+
is the one place the command lives). A press on an open item closes it instead of following the action.
|
|
2063
|
+
The actions are real buttons in the tab order and focus reaching one opens its tray; Escape closes it.
|
|
2064
|
+
A swipe is a path gesture (WCAG 2.5.1), so put the same commands somewhere a press reaches too.
|
|
2065
|
+
|
|
2066
|
+
**`SortableList`: the app owns the order.** During a drag nothing is reordered — the item follows the
|
|
2067
|
+
pointer, its neighbours step aside as inline translates — and `onOrderChange` fires once, on the drop,
|
|
2068
|
+
which then glides into place (the Web Animations API, from where it was drawn). The keyboard is a grab,
|
|
2069
|
+
DashboardGrid's model: Enter/Space picks up, ArrowUp/ArrowDown/Home/End move (reporting each step),
|
|
2070
|
+
Enter drops, Escape puts back. A press on a handle that does not drag also picks the item up, and a
|
|
2071
|
+
press on another item then drops it there — the single-pointer way WCAG 2.5.7 asks for. Render the
|
|
2072
|
+
items in the order you hold; a refused move is just not applied.
|
|
2073
|
+
|
|
2074
|
+
**`VirtualList` measures each item it draws** and keeps the height by `itemKey`, so mixed heights work;
|
|
2075
|
+
an undrawn item counts as the mean of those drawn (`estimateSize` before any is), and the list holds its
|
|
2076
|
+
height while it scrolls, since a scrollbar drag loses a move whenever it changes. The scroller is the nearest ancestor that scrolls, else the
|
|
2077
|
+
page; `scrollerRef` overrides. Each item gets `aria-setsize`/`aria-posinset`, the item focus is in
|
|
2078
|
+
stays rendered, and an item above the screen that turns out taller than estimated scrolls the list by
|
|
2079
|
+
the difference so nothing on screen moves. Groups and sticky labels need the whole list and do not
|
|
2080
|
+
window; find-in-page cannot see an unrendered item.
|
|
2081
|
+
|
|
2082
|
+
Styling: `swipeItem.row`/`swipeItem.tray` (`dragging` variants), `swipeItem.action` (the three colour
|
|
2083
|
+
variants, `armed`), `sortableList.handle` (`grabbed`), and on `list.item` the `lifted` (dragged),
|
|
2084
|
+
`grabbed` (keyboard) and `settling` variants. `List.Chevron` is the item's chevron as a part.
|
|
2085
|
+
|
|
2086
|
+
---
|
|
2087
|
+
|
|
1894
2088
|
## Slider & Progress
|
|
1895
2089
|
|
|
1896
2090
|
```tsx
|
|
@@ -2396,30 +2590,33 @@ named ones carry the reason.
|
|
|
2396
2590
|
| `footer` | `boolean \| { label? }` | `false` | A pinned row of grand totals over every column with an `aggregate`. `label` replaces the default `Total` |
|
|
2397
2591
|
| `export` | `boolean \| ExportConfig` | `false` | Export buttons in the top bar: `{ csv?, xlsx?, fileName? }`. The writers load on the first press |
|
|
2398
2592
|
| `tabNavigation` | `'none' \| 'cells'` | `'none'` | What Tab does. `'none'` is APG's single tab stop; `'cells'` is the spreadsheet reading |
|
|
2593
|
+
| `layout` | `'auto' \| 'table' \| 'list'` | `'auto'` | A table, or a list once the grid's own width is under `list.breakpoint` (see "On a phone") |
|
|
2594
|
+
| `list` | `ListConfig` | — | `{ breakpoint?, fields?, batchSize?, recordView? }` — the list layout's settings |
|
|
2399
2595
|
| `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) |
|
|
2400
2596
|
|
|
2401
2597
|
### ColumnType
|
|
2402
2598
|
|
|
2403
|
-
| Prop | Type
|
|
2404
|
-
| ------------------------ |
|
|
2405
|
-
| `key` | `Key`
|
|
2406
|
-
| `header` | `string`
|
|
2407
|
-
| `width` | `number`
|
|
2408
|
-
| `align` | `'start' \| 'end' \| 'center' \| 'left' \| 'right'`
|
|
2409
|
-
| `pin` | `'START' \| 'END'` (also `'LEFT'`/`'RIGHT'`)
|
|
2410
|
-
| `columns` | `ColumnType[]`
|
|
2411
|
-
| `Cell` | `({ cell }) => ReactNode`
|
|
2412
|
-
| `sortable` / `resizable` | `boolean`
|
|
2413
|
-
| `flexible` | `boolean`
|
|
2414
|
-
| `filterable` | `boolean \| FilterConfig`
|
|
2415
|
-
| `contextMenu` | `boolean \| ContextMenuConfig`
|
|
2416
|
-
| `aggregate` | `AggregateName \| (values, rows) => value`
|
|
2417
|
-
| `AggregateCell` | `({ cell }) => ReactNode`
|
|
2418
|
-
| `editable` | `boolean \| (row) => boolean`
|
|
2419
|
-
| `editor` | `CellEditorType \| CellEditorConfig`
|
|
2420
|
-
| `EditCell` | `({ cell }) => ReactNode`
|
|
2421
|
-
| `exportValue` | `(row: TRow) => value`
|
|
2422
|
-
| `
|
|
2599
|
+
| Prop | Type | Default | Description |
|
|
2600
|
+
| ------------------------ | ------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2601
|
+
| `key` | `Key` | required | Column identifier (maps to TRow property) |
|
|
2602
|
+
| `header` | `string` | — | Header text |
|
|
2603
|
+
| `width` | `number` | `200` | Base width in px |
|
|
2604
|
+
| `align` | `'start' \| 'end' \| 'center' \| 'left' \| 'right'` | `'left'` | Cell alignment. `start`/`end` follow the reading order; `left`/`right` stay on the screen side they name |
|
|
2605
|
+
| `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 |
|
|
2606
|
+
| `columns` | `ColumnType[]` | — | Nested columns (grouped header) |
|
|
2607
|
+
| `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 |
|
|
2608
|
+
| `sortable` / `resizable` | `boolean` | inherits | Override grid-level setting |
|
|
2609
|
+
| `flexible` | `boolean` | `true` | Participate in flex width distribution |
|
|
2610
|
+
| `filterable` | `boolean \| FilterConfig` | — | `true` (text), `{ type: 'number', min?, max? }`, `{ type: 'multiselect', options? }` |
|
|
2611
|
+
| `contextMenu` | `boolean \| ContextMenuConfig` | inherits | Override grid-level context menu. `false` hides entirely. `{ sort?, pin?, group? }` controls sections |
|
|
2612
|
+
| `aggregate` | `AggregateName \| (values, rows) => value` | — | Total this column over each group row, and over the footer. See below |
|
|
2613
|
+
| `AggregateCell` | `({ cell }) => ReactNode` | — | Renders the aggregate. `cell`: `{ value, column, rows, scope }`, `scope` being `'group'` or `'footer'` |
|
|
2614
|
+
| `editable` | `boolean \| (row) => boolean` | inherits | Whether this column’s cells can be edited — every row, or the rows a predicate says |
|
|
2615
|
+
| `editor` | `CellEditorType \| CellEditorConfig` | inferred | `text`/`number`/`checkbox`/`select`. Without one it is read off the value in the cell |
|
|
2616
|
+
| `EditCell` | `({ cell }) => ReactNode` | — | An editor of your own, bound to `cell.draft` with `cell.commitEdit()`/`cell.cancelEdit()` |
|
|
2617
|
+
| `exportValue` | `(row: TRow) => value` | the field | What this column writes to an exported file — an export runs no React, so a `Cell` renderer needs it |
|
|
2618
|
+
| `list` | `'title' \| 'kicker' \| 'subtitle' \| 'end' \| 'field' \| 'detail' \| 'hidden'` | inferred | Where the column goes in a list item, on a phone |
|
|
2619
|
+
| `exportFormat` | `string` | — | The number format its cells wear in a workbook, `'#,##0.00'` or `'yyyy-mm-dd'` |
|
|
2423
2620
|
|
|
2424
2621
|
### Aggregation
|
|
2425
2622
|
|
|
@@ -2715,6 +2912,65 @@ only gives it a bigger target.
|
|
|
2715
2912
|
landed, so `range.values()` reads what the grid now shows. Everything it accepted lands in the same
|
|
2716
2913
|
stream a typed value does: `onCellEditsChange(edits, { reason: 'paste' })`.
|
|
2717
2914
|
|
|
2915
|
+
### On a phone: the list layout
|
|
2916
|
+
|
|
2917
|
+
Below `def.list.breakpoint` (576px, 36rem) of **the grid's own width** — measured with a `ResizeObserver`, not read off the viewport — the grid draws each row as a list item instead of a table row. Same rows, filters, sort, selection, grouping, tree and edits; a different drawing. `layout: 'table'` or `layout: 'list'` chooses one for good.
|
|
2918
|
+
|
|
2919
|
+
```tsx
|
|
2920
|
+
<DataGrid
|
|
2921
|
+
data={orders}
|
|
2922
|
+
def={{
|
|
2923
|
+
title: 'Orders',
|
|
2924
|
+
globalFilter: true,
|
|
2925
|
+
rowSelection: true,
|
|
2926
|
+
columns: [
|
|
2927
|
+
{ key: 'customer', header: 'Customer', list: 'title' },
|
|
2928
|
+
{ key: 'id', header: 'Order', list: 'kicker' },
|
|
2929
|
+
{ key: 'total', header: 'Total', align: 'end', list: 'end', filterable: { type: 'number' } },
|
|
2930
|
+
{ key: 'status', header: 'Status', Cell: StatusBadge, filterable: { type: 'multiselect' } },
|
|
2931
|
+
{ key: 'date', header: 'Date' },
|
|
2932
|
+
{ key: 'notes', header: 'Notes', list: 'detail' },
|
|
2933
|
+
],
|
|
2934
|
+
}}
|
|
2935
|
+
/>
|
|
2936
|
+
```
|
|
2937
|
+
|
|
2938
|
+
| `column.list` | Where the column goes |
|
|
2939
|
+
| ------------- | ------------------------------------------------------------------------------------------------ |
|
|
2940
|
+
| `'title'` | The item's name, 16px semibold, and the button that opens the record view |
|
|
2941
|
+
| `'kicker'` | A small muted line above the title — an id, a date |
|
|
2942
|
+
| `'subtitle'` | A line under the title; several are joined with `·` |
|
|
2943
|
+
| `'end'` | The one value opposite the title, with its column's name under it — a total, a status |
|
|
2944
|
+
| `'field'` | A labelled value in the two-column `<dl>` under the title |
|
|
2945
|
+
| `'detail'` | Only the record view |
|
|
2946
|
+
| `'hidden'` | Nowhere in the list layout |
|
|
2947
|
+
| (unset) | First column → title, first end-aligned → end, next `def.list.fields` (4) → field, rest → detail |
|
|
2948
|
+
|
|
2949
|
+
| `def.list` | Default | Description |
|
|
2950
|
+
| ------------ | ------- | -------------------------------------------------------------------------------------------------- |
|
|
2951
|
+
| `breakpoint` | `576` | The grid's own width, in px, below which `layout: 'auto'` draws the list |
|
|
2952
|
+
| `fields` | `4` | How many unassigned columns become fields before the rest wait for the record view |
|
|
2953
|
+
| `batchSize` | `20` | How many items one **Show more** adds. A paginated grid draws its page and a compact pager instead |
|
|
2954
|
+
| `recordView` | `true` | Whether pressing an item opens the record view |
|
|
2955
|
+
|
|
2956
|
+
What the list does, in the order a reader meets it:
|
|
2957
|
+
|
|
2958
|
+
- **Toolbar**: the title and row count, a full-width search (`globalFilter`), then pills — **Sort** and **Filters** open bottom sheets, **Select** (with `rowSelection`) turns selecting on. The filters in force sit under them as chips, each removable on its own.
|
|
2959
|
+
- **Sort sheet**: one radio group of the sortable columns and one of the direction, applied as they are picked.
|
|
2960
|
+
- **Filter sheet**: a section per filterable column — a full-width field, the phone's own operator picker for a number, chips for eight options or fewer and a searchable checklist past that. A filter applies as it is set, and the main button reads **Show N rows**.
|
|
2961
|
+
- **Item**: a press opens the record view; while selecting it ticks the row instead, a checkbox leads every item and a bar pinned to the bottom of the viewport counts the selection, with **Select all** and **Clear selection**. A tree row carries its chevron before the title, a `rowDetail` row a **Details** toggle, a group is a section header with its count and aggregates.
|
|
2962
|
+
- **Record view**: every column of one row in a bottom sheet, with a pager (`3 of 40`) that follows the list's own sort and filters — the arrow buttons, Left/Right, or a sideways swipe. An editable column has an **Edit** button that opens the column's editor through the same `def.onCellEdit`. Closing hands focus to the item of the row on screen.
|
|
2963
|
+
- **Footer**: the `def.footer` totals as a summary block, then **Show more** (`20 of 340`) — a button, never an endless scroll, so the totals and the page below stay reachable — or the compact pager when the host paginates.
|
|
2964
|
+
|
|
2965
|
+
Four things to know:
|
|
2966
|
+
|
|
2967
|
+
1. **It is a list, not a restyled grid.** A `<ul>` of `<li>`s, each value in a `<dl>`, `aria-posinset`/`aria-setsize` on each item; the grid's APG keyboard is the table's alone. Do not reach for `role="grid"` in a list item.
|
|
2968
|
+
2. **It is a chunk of its own.** The sheets and the record view load the first time a grid is narrow (and on import, on a phone-width screen); until then the grid goes on drawing its table. In a test, `await loadListLayout()` from `components/dataGrid/useListLayout` before rendering.
|
|
2969
|
+
3. **Unmeasured is a table.** A server render cannot know the width, so `layout: 'auto'` prerenders the table and a phone switches before its first paint after hydration. Force `layout: 'list'` where the server knows better.
|
|
2970
|
+
4. **The selection bar sticks to the viewport**, so no ancestor of the grid may be a scroll container (`overflow: hidden`/`auto`): `overflow: clip` clips the same corners without being one. The grid itself switches to `clip` in the list layout.
|
|
2971
|
+
|
|
2972
|
+
Styling: every row and group header is a `List.Item` (see List), so the `list` tree styles them — the dividers, the hover and pressed tints, `selected`, the muted text. The grid's own is `datagrid.list` > `toolbar` > (`count`, `search`, `button` with an `active` variant, `chip`), `toggle` (a tree's chevron, the Details toggle), `detail`, `button`, `selectionBar`, `sheet` > (`line`, `input`, `chip`, `button`, `primary`). Both trees carry paint only; the geometry is in the list's own chunk.
|
|
2973
|
+
|
|
2718
2974
|
### ContextMenuConfig
|
|
2719
2975
|
|
|
2720
2976
|
| Prop | Type | Default | Description |
|