@box-kite/react 2.2.5 → 2.2.7
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 +5 -1
- package/.claude/skills/box-kite/SKILL.md +5 -1
- package/.claude/skills/box-kite/references/components.md +34 -0
- package/.claude/skills/box-kite/references/extending.md +10 -0
- package/.claude/skills/box-kite/references/styling.md +3 -2
- package/.cursor/rules/box-kite.mdc +5 -1
- package/AGENTS.md +5 -1
- package/BOX_AI_CONTEXT.md +147 -7
- package/BOX_KITE_AI_CONTEXT.md +147 -7
- package/box.cjs +1 -1
- package/box.d.ts +5 -0
- package/box.mjs +7 -7
- package/catalog.cjs +1 -1
- package/catalog.mjs +521 -0
- package/client.cjs +1 -1
- package/client.mjs +136 -117
- package/components/calendar.cjs +1 -0
- package/components/calendar.d.ts +77 -0
- package/components/calendar.mjs +483 -0
- package/components/dataGrid/components/dataGridBooleanValue.d.ts +11 -0
- package/components/dataGrid/components/dataGridDateParts.d.ts +40 -0
- package/components/dataGrid/components/dataGridDateValue.d.ts +14 -0
- package/components/dataGrid/contracts/dataGridContract.d.ts +38 -11
- package/components/dataGrid/models/columnModel.d.ts +3 -1
- package/components/dataGrid/models/editModel.d.ts +3 -1
- package/components/dataGrid/models/headerCellModel.d.ts +1 -0
- package/components/dataGrid/models/localeText.d.ts +4 -0
- package/components/dataGrid/useDateParts.d.ts +17 -0
- package/components/dataGrid.cjs +3 -3
- package/components/dataGrid.mjs +824 -690
- package/components/dateField.cjs +1 -0
- package/components/dateField.d.ts +78 -0
- package/components/dateField.mjs +313 -0
- package/components/datePicker.cjs +1 -0
- package/components/datePicker.d.ts +120 -0
- package/components/datePicker.mjs +529 -0
- package/components/dateTime.cjs +1 -0
- package/components/dateTime.d.ts +23 -0
- package/components/dateTime.mjs +34 -0
- package/components/dialog.cjs +1 -1
- package/components/dialog.mjs +87 -75
- package/components/overlay.cjs +1 -1
- package/components/overlay.mjs +1 -1
- package/components/semantics.d.ts +29 -29
- package/components/timeZoneSelect.cjs +1 -0
- package/components/timeZoneSelect.d.ts +52 -0
- package/components/timeZoneSelect.mjs +74 -0
- package/components/toaster.cjs +1 -1
- package/components/toaster.mjs +1 -1
- package/create/src/index.d.ts +1 -0
- package/create/src/options.d.ts +27 -0
- package/create/src/options.test.d.ts +1 -0
- package/create/src/packageManager.d.ts +5 -0
- package/create/src/project.d.ts +25 -0
- package/create/src/project.test.d.ts +1 -0
- package/create/templates/ts/designs/azure.d.ts +5 -0
- package/create/templates/ts/designs/ember.d.ts +5 -0
- package/create/templates/ts/designs/mono.d.ts +5 -0
- package/create/templates/ts/src/App.d.ts +2 -0
- package/create/templates/ts/src/design.d.ts +5 -0
- package/create/templates/ts/src/main.d.ts +0 -0
- package/dataGridDateParts.cjs +1 -0
- package/dataGridDateParts.mjs +223 -0
- package/dataGridList.cjs +1 -1
- package/dataGridList.mjs +335 -372
- package/dataGridListLayout.cjs +1 -0
- package/dataGridListLayout.mjs +49 -0
- package/date-environment.cjs +1 -0
- package/date-environment.mjs +29 -0
- package/date-input.cjs +1 -0
- package/date-input.mjs +346 -0
- package/date-math.cjs +1 -0
- package/date-math.mjs +317 -0
- package/date.cjs +1 -0
- package/date.d.ts +40 -0
- package/date.mjs +4 -0
- package/dates.cjs +1 -0
- package/dates.mjs +689 -0
- package/docs/components.md +6 -1
- package/docs/props.md +19 -19
- package/package.json +124 -27
- package/react/boxComponent.d.ts +1 -1
- package/react/date/dateChange.d.ts +18 -0
- package/react/date/dateEnvironment.d.ts +34 -0
- package/react/theme/theme.d.ts +9 -0
- package/react/theme/themeContext.d.ts +2 -0
- package/react-shared.cjs +1 -1
- package/react-shared.mjs +1 -1
- package/rsc.cjs +1 -1
- package/rsc.d.ts +3 -0
- package/rsc.mjs +1 -1
- package/ssg.cjs +1 -1
- package/ssg.mjs +1 -1
- package/utils/date/calendar.d.ts +44 -0
- package/utils/date/calendarModel.d.ts +46 -0
- package/utils/date/dateTime.d.ts +144 -0
- package/utils/date/fieldModel.d.ts +74 -0
- package/utils/date/format.d.ts +39 -0
- package/utils/date/iso.d.ts +40 -0
- package/utils/date/locale.d.ts +17 -0
- package/utils/date/parse.d.ts +20 -0
- package/utils/date/settings.d.ts +26 -0
- package/utils/date/value.d.ts +43 -0
- package/utils/date/zoneOptions.d.ts +19 -0
- package/utils/date/zones.d.ts +32 -0
|
@@ -60,7 +60,7 @@ globs: '**/*.{ts,tsx,jsx}'
|
|
|
60
60
|
|
|
61
61
|
36. **A message you send rather than render is `<Toaster>` plus `toast()`.** `<Toaster />` once near the root of the app (`components/toaster`), and after that `toast.success('Saved')` from anywhere at all — an event handler, a fetch, a router guard, a module with no React in it. The store behind it is framework-free, so a call made **before** the viewport mounts is queued rather than lost, and the component only draws what is there. The surface is `toast(message, options)` with `success`/`error`/`warning`/`info`/`loading` beside it, `toast.promise(p, { loading, success, error })` (one toast for the whole call, and the promise comes back untouched), `toast.update(id, message, options)` and `toast.dismiss(id?)`; an `options.id` already on screen is an **update**, which is what makes a promise one toast rather than two. A toast takes a `description`, one `action` (`{ label, onClick }`, which dismisses the toast it answered unless `closeOnClick: false`), a `duration` in milliseconds (`Infinity` waits, and `loading` already does), and `onDismiss(reason)` with `timeout`/`close`/`action`/`imperative`. **The limit is a queue, not a cap**: past `limit` (default 3) a toast waits **with its timer unstarted**, so nothing expires that was never on screen, and a counter at the far end of the stack says how many are waiting. **Four things to know, all measured in Chrome 153.** The viewport is a **polite live region that exists before there is anything in it** — a region inserted together with its content is not reliably announced — and an error toast is `role="alert"`, the one announcement pattern every screen reader implements; nothing else carries a region of its own, because the nearest region to a change is the one that speaks. It is in the **top layer** with no portal (`popover="manual"`, shown as it mounts), so it inherits the theme and the direction around it, and it takes **no pointer events at all** while the toasts take them back — a corner-sized fixed strip would otherwise swallow every press in its gaps. **Timers stop on hover, on focus and in a background tab** and pick up where they left off, which is WCAG 2.2.1 and the only reason a toast may carry a control at all; a toast **never takes focus**, so `F6` (or `hotkey`, which takes `'alt+t'` or `false`) is what moves focus to the stack, Tab walks it, and Escape dismisses the toast focus is in and hands focus back once the stack is empty. And the viewport has to answer the UA's own `[popover]` rule: `inset: 0` leaves a corner-pinned stack over-constrained — `top`/`left` win and it sits in the wrong corner — and `overflow: auto` makes it a scroll container, so the style tree declares all four sides `auto` and `overflow: visible` before a `position` variant names the two it wants. `position` is `'bottom-end'` by default and its inline half is logical (`start`/`end`), the newest toast is always nearest the screen edge, and styling is the `toaster`/`toaster.toast`/`toaster.message`/`toaster.description`/`toaster.action`/`toaster.close`/`toaster.overflow` tree, where the kind is a variant on the toast.
|
|
62
62
|
|
|
63
|
-
37. **A DataGrid cell is edited by two props, and one of them judges the edit.** `column.editable` (`true`, or a predicate over the row) is the whole opt-in — `def.editable` is the grid-wide default a column falls back to — and **`def.onCellEdit({ rowKey, columnKey, row, value, oldValue })` both validates the edit and is told about it**: answer nothing to accept the value, a string to refuse it with that message, `false` to refuse it with the grid's own. It may return a **promise**, so a uniqueness check against a server is the same function; an answer to an edit the user has since abandoned is dropped rather than applied to whatever they are editing now, and a rejected promise is its message. **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean `checkbox
|
|
63
|
+
37. **A DataGrid cell is edited by two props, and one of them judges the edit.** `column.editable` (`true`, or a predicate over the row) is the whole opt-in — `def.editable` is the grid-wide default a column falls back to — and **`def.onCellEdit({ rowKey, columnKey, row, value, oldValue })` both validates the edit and is told about it**: answer nothing to accept the value, a string to refuse it with that message, `false` to refuse it with the grid's own. It may return a **promise**, so a uniqueness check against a server is the same function; an answer to an edit the user has since abandoned is dropped rather than applied to whatever they are editing now, and a rejected promise is its message. **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean `checkbox` (and is drawn as a check or a cross), an ISO date, time or date-time a `DateField` (an edit is written back in the shape the cell held: `…Z` stays UTC, a `Date` stays a `Date`), everything else `text` — so a column of numbers has a numeric keypad without being told; `column.editor` names one of the seven (`text`/`number`/`checkbox`/`select`/`date`/`time`/`datetime`) or configures it (`{ type, options?, placeholder?, step?, min?, max?, zone? }`, where `options` may be a function of the row), and `column.EditCell` is a control of your own bound to `cell.draft`, with `cell.setDraft(v)`, `cell.commitEdit()` and `cell.cancelEdit()` beside it — the same three calls the built-in four make, so a date picker is a component rather than a second copy of the commit rules. **A press chooses and a double press opens**: clicking a cell makes it the current one — the cell the arrows carry on from, marked whether the pointer or the keyboard put it there — and double-clicking an editable one opens its editor, the way a spreadsheet reads a double press; a cell that cannot be edited takes both as focus and nothing else, and a double press landing on a widget inside a cell (a tree chevron, a link in a `Cell` of your own) belongs to the widget rather than the editor. **The keyboard is APG's**: Enter or F2 opens the editor, any printable character opens it on that character (replacing the value, the way a spreadsheet reads a keystroke), Escape throws the draft away, Enter commits and hands the keyboard back to the cell, a press elsewhere commits, and **Tab commits and opens the next editable cell** — along the row and on into the rows after it, so a row of corrections is one gesture; `checkbox` and `select` commit on the change instead, since one interaction is the whole value. **Four things to know.** A **refused value keeps the editor open and the caret in it**, with the message in a `role="alert"` **in the top layer** — a bubble inside the scroller would be clipped away on the last row, which is the row a long grid is most often edited on — plus `aria-invalid` and `aria-describedby` on the field; typing clears it. **The grid does not own `data`, so an accepted value is kept as an edit _over_ the rows it was given** and is what every cell, every `Cell` renderer and every export reads from then on — which is what lets a `def.dataSource` grid be edited at all, since a value that was only reported would be gone on the next block. **`onCellEditsChange(edits, { reason })` reports the whole list, oldest first** — the stream an undo is built out of — and **`clearEdits()` on the grid's ref** is how a host says its own data has caught up; a datasource `refresh()` drops them with the blocks, because what the server says next is the newer answer. And **an edit never re-sorts or re-filters the grid**: sorting and filtering read the row's own values and only what is _displayed_ reads the edit, so a row cannot jump out from under the person typing into it. Styling is `datagrid.body.cell`'s `isEditing` and `isInvalid` variants — one ring, red when the value was refused, because two rings on the same rectangle means the editing one paints over the other — plus `datagrid.body.cell.editor` (`isPending`) and `datagrid.body.cell.error`.
|
|
64
64
|
|
|
65
65
|
38. **A DataGrid marks the cell it is on, and `def.rangeSelection` marks a block of them.** Every grid marks the cell its arrows carry on from — whether the pointer or the keyboard put it there — and the mark **survives the grid losing focus**, which is the whole point of it: a `:focus-visible` ring never matches a pointer, so a _clicked_ cell drew nothing (bug #64), and a ring made of focus leaves a copy with nothing to act on. **`Ctrl+C` needs no opt-in**: on the current cell it copies that one cell, which is the degenerate range. `def.rangeSelection: true` is the rest — **drag across cells, or hold Shift with the arrow keys** (Home/End, Ctrl+Home/End and PageUp/Down extend too), and a Shift+press extends from wherever the block was anchored; a move with no modifier collapses it onto the cell it landed on, and after a drag the ring, the tab stop and the next Shift+arrow are all the cell the drag ended on. What `Ctrl+C` writes is **tab-separated text with CRLF between the rows**, which is what Excel, Sheets and Numbers paste as columns; a field carrying a tab, a newline or a quote is quoted and its quotes doubled. **Four things to know.** The block is what reports itself: its cells carry `aria-selected` and the grid is `aria-multiselectable`, while a **lone current cell reports neither** — it has chosen nothing, and announcing "selected" on every arrow key is an announcement about nothing. The block is tinted _around_ the current cell, which keeps its ring, the way a sheet says where typing would land. **A copied figure is the one a file would carry**: the clipboard is an export, so a column's `exportValue` wins where it has one, an accepted edit wins over the row, and a group row copies its own value in the column that spans it; the copy rides the browser's own copy event, so text selected on the page still belongs to the page. And **marking cells is a drag, so text selection is not** — a grid with `rangeSelection` on gives text selection up (an open editor hands it back), a press that lands on a widget belongs to the widget, and a touch is left alone entirely, since that press is how the grid scrolls. `onRangeChange(range, { reason })` reports `{ startRow, endRow, columns, values() }` with `select`/`extend`/`clear` — indices rather than row keys, because a block may reach rows nobody has fetched, and `values()` is a call for the same reason. Styling is `datagrid.body.cell`'s `isCurrentCell` and `isInRange` variants.
|
|
66
66
|
|
|
@@ -92,4 +92,8 @@ globs: '**/*.{ts,tsx,jsx}'
|
|
|
92
92
|
|
|
93
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
94
|
|
|
95
|
+
53. **A date, a time or a moment is an ISO string, and its shape is its kind.** `2026-10-19` is a date (no time, no zone — the same day everywhere), `14:30:00` a time, `2026-10-19T14:30:00` a wall clock with no zone, and anything with an offset (`…+03:00`, `…Z`) an instant. **Never hold a `Date`**: it is a moment in the runtime's zone, which is how a birthday becomes the day before in another country. `@box-kite/react/date` (no React, no engine, under 8 KB gz, every locale and zone from `Intl`) converts by naming the target — `toUtc(value, { from: 'Europe/Chisinau' })`, `toLocal(value)`, `toZone(value, 'America/New_York')`, `toPlainDate(value, { zone })` — and `format(value, { locale, zone, dateStyle })` writes one in the reader's words (presets, never format tokens); `parse(text, { locale, kind })` reads ISO, a `Date`, epoch ms, an HTTP date or text in the locale's order (`03/04/2026` is March in `en-US` and April in `en-GB`). `add`/`subtract` move the wall clock for days and up and the instant for hours and down, so a day later is the same time tomorrow across DST; `startOf(…, 'week')` follows the locale. **`'local'` is the reader**: the browser's zone in a browser, and on a server `configureDates({ timeZone })` or a `<DateProvider timeZone>` — read off the cookie `zoneCookie()` writes, with `readZoneCookie(header)` — while `systemZone()` is always the server's own. **The controls** — `<DateField>` (`components/dateField`: `type="date"|"time"|"datetime"`, segments in the locale's order, nothing typed is ever parsed), `<Calendar>` (`components/calendar`, `mode="range"`), `<DatePicker>`/`<DateRangePicker>` (`components/datePicker`, the top layer, a list of times every `step` minutes for `time`/`datetime`, `presets`), `<TimeZoneSelect>` (`components/timeZoneSelect`, the IANA name as its value) and `<DateTime value>` (`components/dateTime`, a `<time datetime>` in the reader's zone) — take and give that string, and **`onValueChange(value, { reason, utc, local, zoned, date, epoch, offset })`** hands the readings over beside it. A date-time's `zone` is `'local'` (default), `'utc'`, an IANA name or `'none'` (a wall clock that belongs to nobody); the value is the instant on that zone's clock (`…Z` for UTC). **They cannot cause a hydration error**: a server and a hydrating browser both render in the fallback (`UTC`, `en-US`, or what a `DateProvider` says) and the render after switches to the reader. `name` posts the canonical string and `required` stops the form; a DataGrid column takes `editor: 'date'|'time'|'datetime'`, `dateFormat` and `filterable: { type: 'date' }`, from a chunk fetched only when a grid shows a date. `<Textbox type="date">` is still the native input, whose language is the browser's rather than the page's.
|
|
96
|
+
|
|
95
97
|
Full reference: `src/BOX_KITE_AI_CONTEXT.md` or invoke `/box-kite` skill.
|
|
98
|
+
|
|
99
|
+
54. **A design is two families and a `Box.components()` object, and it is chosen in CSS.** Every built-in component is drawn in `accent` and `base` — two palette families that start as copies of `indigo` and `gray` — so `vars={Box.palette({ accent: 'orange', base: 'stone' })}` on an ancestor recolours every component inside it, by family name or with a ramp of your own (`{ 500: 'oklch(…)' }`); write `accent-*`/`base-*` in your own components when they should follow, and a named family when the colour is the content. The rest of a design is `Box.components(styles, { design: 'ember' })`: merged over the app's own and applied under `data-design="ember"`, which `<Box.Theme design="classic" designStorageKey="design">` writes on `<html>` and `Box.useDesign()` switches — chosen by an attribute rather than by context, so a pre-paint script that copies the stored name onto `<html>` is all a prerendered page needs, and server and browser render the same classes. **Three things to know.** A design merges and cannot delete a key, so it takes something back off with that prop's `none` (`shadow: 'none'`, `bgGradient: 'none'` — never `bgImage: 'none'`, which loses `background-image` to `bgGradient`). A design nested inside another stops the outer one, but `vars` inherit, so a `data-design="classic"` preview inside an Ember page redeclares its own `Box.palette()`. And a Box under `<Box.Components value>` resolves against that registry and no named design — `Box.createComponents()` is still the answer for a subtree whose design React decides. A part extends like a component does (`trigger: { extends: 'button' }`, `'dialog.title'`), and `letterSpacing` takes the tracking names `tighter`…`widest` in `em`.
|
|
@@ -133,7 +133,7 @@ Detect the package manager from the lock file rather than asking:
|
|
|
133
133
|
|
|
134
134
|
36. **A message you send rather than render is `<Toaster>` plus `toast()`.** `<Toaster />` once near the root of the app (`components/toaster`), and after that `toast.success('Saved')` from anywhere at all — an event handler, a fetch, a router guard, a module with no React in it. The store behind it is framework-free, so a call made **before** the viewport mounts is queued rather than lost, and the component only draws what is there. The surface is `toast(message, options)` with `success`/`error`/`warning`/`info`/`loading` beside it, `toast.promise(p, { loading, success, error })` (one toast for the whole call, and the promise comes back untouched), `toast.update(id, message, options)` and `toast.dismiss(id?)`; an `options.id` already on screen is an **update**, which is what makes a promise one toast rather than two. A toast takes a `description`, one `action` (`{ label, onClick }`, which dismisses the toast it answered unless `closeOnClick: false`), a `duration` in milliseconds (`Infinity` waits, and `loading` already does), and `onDismiss(reason)` with `timeout`/`close`/`action`/`imperative`. **The limit is a queue, not a cap**: past `limit` (default 3) a toast waits **with its timer unstarted**, so nothing expires that was never on screen, and a counter at the far end of the stack says how many are waiting. **Four things to know, all measured in Chrome 153.** The viewport is a **polite live region that exists before there is anything in it** — a region inserted together with its content is not reliably announced — and an error toast is `role="alert"`, the one announcement pattern every screen reader implements; nothing else carries a region of its own, because the nearest region to a change is the one that speaks. It is in the **top layer** with no portal (`popover="manual"`, shown as it mounts), so it inherits the theme and the direction around it, and it takes **no pointer events at all** while the toasts take them back — a corner-sized fixed strip would otherwise swallow every press in its gaps. **Timers stop on hover, on focus and in a background tab** and pick up where they left off, which is WCAG 2.2.1 and the only reason a toast may carry a control at all; a toast **never takes focus**, so `F6` (or `hotkey`, which takes `'alt+t'` or `false`) is what moves focus to the stack, Tab walks it, and Escape dismisses the toast focus is in and hands focus back once the stack is empty. And the viewport has to answer the UA's own `[popover]` rule: `inset: 0` leaves a corner-pinned stack over-constrained — `top`/`left` win and it sits in the wrong corner — and `overflow: auto` makes it a scroll container, so the style tree declares all four sides `auto` and `overflow: visible` before a `position` variant names the two it wants. `position` is `'bottom-end'` by default and its inline half is logical (`start`/`end`), the newest toast is always nearest the screen edge, and styling is the `toaster`/`toaster.toast`/`toaster.message`/`toaster.description`/`toaster.action`/`toaster.close`/`toaster.overflow` tree, where the kind is a variant on the toast.
|
|
135
135
|
|
|
136
|
-
37. **A DataGrid cell is edited by two props, and one of them judges the edit.** `column.editable` (`true`, or a predicate over the row) is the whole opt-in — `def.editable` is the grid-wide default a column falls back to — and **`def.onCellEdit({ rowKey, columnKey, row, value, oldValue })` both validates the edit and is told about it**: answer nothing to accept the value, a string to refuse it with that message, `false` to refuse it with the grid's own. It may return a **promise**, so a uniqueness check against a server is the same function; an answer to an edit the user has since abandoned is dropped rather than applied to whatever they are editing now, and a rejected promise is its message. **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean `checkbox
|
|
136
|
+
37. **A DataGrid cell is edited by two props, and one of them judges the edit.** `column.editable` (`true`, or a predicate over the row) is the whole opt-in — `def.editable` is the grid-wide default a column falls back to — and **`def.onCellEdit({ rowKey, columnKey, row, value, oldValue })` both validates the edit and is told about it**: answer nothing to accept the value, a string to refuse it with that message, `false` to refuse it with the grid's own. It may return a **promise**, so a uniqueness check against a server is the same function; an answer to an edit the user has since abandoned is dropped rather than applied to whatever they are editing now, and a rejected promise is its message. **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean `checkbox` (and is drawn as a check or a cross), an ISO date, time or date-time a `DateField` (an edit is written back in the shape the cell held: `…Z` stays UTC, a `Date` stays a `Date`), everything else `text` — so a column of numbers has a numeric keypad without being told; `column.editor` names one of the seven (`text`/`number`/`checkbox`/`select`/`date`/`time`/`datetime`) or configures it (`{ type, options?, placeholder?, step?, min?, max?, zone? }`, where `options` may be a function of the row), and `column.EditCell` is a control of your own bound to `cell.draft`, with `cell.setDraft(v)`, `cell.commitEdit()` and `cell.cancelEdit()` beside it — the same three calls the built-in four make, so a date picker is a component rather than a second copy of the commit rules. **A press chooses and a double press opens**: clicking a cell makes it the current one — the cell the arrows carry on from, marked whether the pointer or the keyboard put it there — and double-clicking an editable one opens its editor, the way a spreadsheet reads a double press; a cell that cannot be edited takes both as focus and nothing else, and a double press landing on a widget inside a cell (a tree chevron, a link in a `Cell` of your own) belongs to the widget rather than the editor. **The keyboard is APG's**: Enter or F2 opens the editor, any printable character opens it on that character (replacing the value, the way a spreadsheet reads a keystroke), Escape throws the draft away, Enter commits and hands the keyboard back to the cell, a press elsewhere commits, and **Tab commits and opens the next editable cell** — along the row and on into the rows after it, so a row of corrections is one gesture; `checkbox` and `select` commit on the change instead, since one interaction is the whole value. **Four things to know.** A **refused value keeps the editor open and the caret in it**, with the message in a `role="alert"` **in the top layer** — a bubble inside the scroller would be clipped away on the last row, which is the row a long grid is most often edited on — plus `aria-invalid` and `aria-describedby` on the field; typing clears it. **The grid does not own `data`, so an accepted value is kept as an edit _over_ the rows it was given** and is what every cell, every `Cell` renderer and every export reads from then on — which is what lets a `def.dataSource` grid be edited at all, since a value that was only reported would be gone on the next block. **`onCellEditsChange(edits, { reason })` reports the whole list, oldest first** — the stream an undo is built out of — and **`clearEdits()` on the grid's ref** is how a host says its own data has caught up; a datasource `refresh()` drops them with the blocks, because what the server says next is the newer answer. And **an edit never re-sorts or re-filters the grid**: sorting and filtering read the row's own values and only what is _displayed_ reads the edit, so a row cannot jump out from under the person typing into it. Styling is `datagrid.body.cell`'s `isEditing` and `isInvalid` variants — one ring, red when the value was refused, because two rings on the same rectangle means the editing one paints over the other — plus `datagrid.body.cell.editor` (`isPending`) and `datagrid.body.cell.error`.
|
|
137
137
|
|
|
138
138
|
38. **A DataGrid marks the cell it is on, and `def.rangeSelection` marks a block of them.** Every grid marks the cell its arrows carry on from — whether the pointer or the keyboard put it there — and the mark **survives the grid losing focus**, which is the whole point of it: a `:focus-visible` ring never matches a pointer, so a _clicked_ cell drew nothing (bug #64), and a ring made of focus leaves a copy with nothing to act on. **`Ctrl+C` needs no opt-in**: on the current cell it copies that one cell, which is the degenerate range. `def.rangeSelection: true` is the rest — **drag across cells, or hold Shift with the arrow keys** (Home/End, Ctrl+Home/End and PageUp/Down extend too), and a Shift+press extends from wherever the block was anchored; a move with no modifier collapses it onto the cell it landed on, and after a drag the ring, the tab stop and the next Shift+arrow are all the cell the drag ended on. What `Ctrl+C` writes is **tab-separated text with CRLF between the rows**, which is what Excel, Sheets and Numbers paste as columns; a field carrying a tab, a newline or a quote is quoted and its quotes doubled. **Four things to know.** The block is what reports itself: its cells carry `aria-selected` and the grid is `aria-multiselectable`, while a **lone current cell reports neither** — it has chosen nothing, and announcing "selected" on every arrow key is an announcement about nothing. The block is tinted _around_ the current cell, which keeps its ring, the way a sheet says where typing would land. **A copied figure is the one a file would carry**: the clipboard is an export, so a column's `exportValue` wins where it has one, an accepted edit wins over the row, and a group row copies its own value in the column that spans it; the copy rides the browser's own copy event, so text selected on the page still belongs to the page. And **marking cells is a drag, so text selection is not** — a grid with `rangeSelection` on gives text selection up (an open editor hands it back), a press that lands on a widget belongs to the widget, and a touch is left alone entirely, since that press is how the grid scrolls. `onRangeChange(range, { reason })` reports `{ startRow, endRow, columns, values() }` with `select`/`extend`/`clear` — indices rather than row keys, because a block may reach rows nobody has fetched, and `values()` is a call for the same reason. Styling is `datagrid.body.cell`'s `isCurrentCell` and `isInRange` variants.
|
|
139
139
|
|
|
@@ -165,4 +165,8 @@ Detect the package manager from the lock file rather than asking:
|
|
|
165
165
|
|
|
166
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
167
|
|
|
168
|
+
53. **A date, a time or a moment is an ISO string, and its shape is its kind.** `2026-10-19` is a date (no time, no zone — the same day everywhere), `14:30:00` a time, `2026-10-19T14:30:00` a wall clock with no zone, and anything with an offset (`…+03:00`, `…Z`) an instant. **Never hold a `Date`**: it is a moment in the runtime's zone, which is how a birthday becomes the day before in another country. `@box-kite/react/date` (no React, no engine, under 8 KB gz, every locale and zone from `Intl`) converts by naming the target — `toUtc(value, { from: 'Europe/Chisinau' })`, `toLocal(value)`, `toZone(value, 'America/New_York')`, `toPlainDate(value, { zone })` — and `format(value, { locale, zone, dateStyle })` writes one in the reader's words (presets, never format tokens); `parse(text, { locale, kind })` reads ISO, a `Date`, epoch ms, an HTTP date or text in the locale's order (`03/04/2026` is March in `en-US` and April in `en-GB`). `add`/`subtract` move the wall clock for days and up and the instant for hours and down, so a day later is the same time tomorrow across DST; `startOf(…, 'week')` follows the locale. **`'local'` is the reader**: the browser's zone in a browser, and on a server `configureDates({ timeZone })` or a `<DateProvider timeZone>` — read off the cookie `zoneCookie()` writes, with `readZoneCookie(header)` — while `systemZone()` is always the server's own. **The controls** — `<DateField>` (`components/dateField`: `type="date"|"time"|"datetime"`, segments in the locale's order, nothing typed is ever parsed), `<Calendar>` (`components/calendar`, `mode="range"`), `<DatePicker>`/`<DateRangePicker>` (`components/datePicker`, the top layer, a list of times every `step` minutes for `time`/`datetime`, `presets`), `<TimeZoneSelect>` (`components/timeZoneSelect`, the IANA name as its value) and `<DateTime value>` (`components/dateTime`, a `<time datetime>` in the reader's zone) — take and give that string, and **`onValueChange(value, { reason, utc, local, zoned, date, epoch, offset })`** hands the readings over beside it. A date-time's `zone` is `'local'` (default), `'utc'`, an IANA name or `'none'` (a wall clock that belongs to nobody); the value is the instant on that zone's clock (`…Z` for UTC). **They cannot cause a hydration error**: a server and a hydrating browser both render in the fallback (`UTC`, `en-US`, or what a `DateProvider` says) and the render after switches to the reader. `name` posts the canonical string and `required` stops the form; a DataGrid column takes `editor: 'date'|'time'|'datetime'`, `dateFormat` and `filterable: { type: 'date' }`, from a chunk fetched only when a grid shows a date. `<Textbox type="date">` is still the native input, whose language is the browser's rather than the page's.
|
|
169
|
+
|
|
168
170
|
Full reference: `references/` beside this file, and `docs/props.md` inside the package.
|
|
171
|
+
|
|
172
|
+
54. **A design is two families and a `Box.components()` object, and it is chosen in CSS.** Every built-in component is drawn in `accent` and `base` — two palette families that start as copies of `indigo` and `gray` — so `vars={Box.palette({ accent: 'orange', base: 'stone' })}` on an ancestor recolours every component inside it, by family name or with a ramp of your own (`{ 500: 'oklch(…)' }`); write `accent-*`/`base-*` in your own components when they should follow, and a named family when the colour is the content. The rest of a design is `Box.components(styles, { design: 'ember' })`: merged over the app's own and applied under `data-design="ember"`, which `<Box.Theme design="classic" designStorageKey="design">` writes on `<html>` and `Box.useDesign()` switches — chosen by an attribute rather than by context, so a pre-paint script that copies the stored name onto `<html>` is all a prerendered page needs, and server and browser render the same classes. **Three things to know.** A design merges and cannot delete a key, so it takes something back off with that prop's `none` (`shadow: 'none'`, `bgGradient: 'none'` — never `bgImage: 'none'`, which loses `background-image` to `bgGradient`). A design nested inside another stops the outer one, but `vars` inherit, so a `data-design="classic"` preview inside an Ember page redeclares its own `Box.palette()`. And a Box under `<Box.Components value>` resolves against that registry and no named design — `Box.createComponents()` is still the answer for a subtree whose design React decides. A part extends like a component does (`trigger: { extends: 'button' }`, `'dialog.title'`), and `letterSpacing` takes the tracking names `tighter`…`widest` in `em`.
|
|
@@ -149,6 +149,40 @@ a zero**: an indeterminate bar omits `aria-valuenow` rather than reporting `0`.
|
|
|
149
149
|
`thumbLabels` names the thumbs. The geometry is logical, so a right-to-left page mirrors for free; only
|
|
150
150
|
the sideways arrows swap, and Up/Down never do.
|
|
151
151
|
|
|
152
|
+
## Dates and times
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import DateField from '@box-kite/react/components/dateField';
|
|
156
|
+
import DatePicker, { DateRangePicker } from '@box-kite/react/components/datePicker';
|
|
157
|
+
import DateTime from '@box-kite/react/components/dateTime';
|
|
158
|
+
import { toUtc, format } from '@box-kite/react/date';
|
|
159
|
+
|
|
160
|
+
<DateField label="Due" name="due" />;
|
|
161
|
+
<DatePicker type="datetime" zone="utc" label="Publish at" onValueChange={(value, { utc, local }) => schedule(utc)} />;
|
|
162
|
+
<DateRangePicker label="Stay" startName="from" endName="to" />;
|
|
163
|
+
<DateTime value={order.createdAt} />;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**A value is an ISO string whose shape is its kind**: `2026-10-19` a date, `14:30:00` a time,
|
|
167
|
+
`2026-10-19T14:30:00` a wall clock with no zone, `…+03:00`/`…Z` an instant. Never a `Date`. Every control
|
|
168
|
+
takes and gives that string, and `onValueChange(value, { reason, utc, local, zoned, date, epoch, offset })`
|
|
169
|
+
hands the readings over beside it.
|
|
170
|
+
|
|
171
|
+
`DateField`: `type` (`date`/`time`/`datetime`), `zone` (`'local'` default, `'utc'`, IANA, `'none'`),
|
|
172
|
+
`locale`, `hourCycle`, `granularity`, `min`/`max`/`isDateUnavailable` (marked `aria-invalid`), `name`,
|
|
173
|
+
`required`, `label`. Segments in the locale's order; a paste is the only text it reads. `Calendar`:
|
|
174
|
+
`mode="range"` (`{ start, end }`), `months`, `weekNumbers`, `firstDayOfWeek`, `min`/`max`. `DatePicker`:
|
|
175
|
+
the field with a calendar, a list of times (`step`) or both, `presets`, `clearable`; `DateRangePicker`:
|
|
176
|
+
two fields over a range calendar, `startName`/`endName`. `TimeZoneSelect`: a `Combobox` whose value is the
|
|
177
|
+
IANA name. `DateTime`: a `<time datetime>` in the reader's zone; the module also exports `DateProvider`.
|
|
178
|
+
|
|
179
|
+
Four things to know. **`'local'` is the reader** — on a server, `configureDates({ timeZone })` or a
|
|
180
|
+
`<DateProvider timeZone>` fed from the cookie `zoneCookie()` writes, and `systemZone()` is the server's own.
|
|
181
|
+
**No hydration error**: server and hydration render in the fallback (`UTC`/`en-US`), then the reader's.
|
|
182
|
+
**DST** is Temporal's `disambiguation` (`'compatible'` default). And the **conversions name their target**
|
|
183
|
+
(`toUtc`, `toLocal`, `toZone`, `toPlainDate`), the arithmetic moves the wall clock for days and the instant
|
|
184
|
+
for hours, and `format` takes `Intl` options, never tokens.
|
|
185
|
+
|
|
152
186
|
## Toaster
|
|
153
187
|
|
|
154
188
|
```tsx
|
|
@@ -16,9 +16,19 @@ export const components = Box.components({
|
|
|
16
16
|
children: { header: { styles: { fontSize: 18, fontWeight: 600 } } },
|
|
17
17
|
},
|
|
18
18
|
subgrid: { extends: 'datagrid', styles: { b: 0, shadow: 'none' } },
|
|
19
|
+
// A part extends too: a component by name, or another part by its dotted path.
|
|
20
|
+
picker: { children: { trigger: { extends: 'button', styles: { px: 3 } } } },
|
|
19
21
|
});
|
|
20
22
|
```
|
|
21
23
|
|
|
24
|
+
**A second design, chosen in CSS**: `Box.components(styles, { design: 'ember' })` merges `styles` over the app's components
|
|
25
|
+
and applies them under `data-design="ember"` — `<Box.Theme design="classic" designStorageKey="design">` writes the
|
|
26
|
+
attribute, `Box.useDesign()` switches it, and a pre-paint script that copies the stored value onto `<html>` means no flash
|
|
27
|
+
on a prerendered page. Each design rule is one class more specific than the app's, so every state is outranked; a design
|
|
28
|
+
cannot delete a key, so it writes `none` (`shadow: 'none'`, `bgGradient: 'none'`). Its colours are `Box.palette()` in a
|
|
29
|
+
root `dataAttr={{ 'design=ember': { vars: … } }}`, since every built-in component is drawn in `accent` and `base`. A
|
|
30
|
+
subtree whose design React decides is still `Box.createComponents()` + `<Box.Components value>`.
|
|
31
|
+
|
|
22
32
|
## Extension System
|
|
23
33
|
|
|
24
34
|
```tsx
|
|
@@ -7,7 +7,7 @@ Every prop by category, the six kinds of nesting, and the theme system. Read thi
|
|
|
7
7
|
**Spacing**: `p`/`px`/`py`/`pt`/`pr`/`pb`/`pl`/`ps`/`pe`, `m`/`mx`/`my`/`mt`/`mr`/`mb`/`ml`/`ms`/`me`, `gap` — the axis pair is already logical (`px` is `padding-inline`) and `ps`/`pe`, `ms`/`me` are its two sides
|
|
8
8
|
**Layout**: `display`, `d` (flex-direction), `wrap`, `ai` (align-items), `jc` (justify-content), `justifyItems`/`placeItems`/`placeContent`/`alignContent`/`alignSelf`/`justifySelf`, `flex`/`grow`/`shrink`, `container`/`containerName`/`containerType` (a query container — see `cq`). Every alignment prop also takes the **overflow-safe** forms — `'safe center'`, `'safe start'`, `'safe end'` and the `'unsafe …'` twins: `safe center` centres until the content stops fitting and then aligns to `start`, where plain `center` overflows both edges and the half above the scrollable origin cannot be reached at all. `aspectRatio` is `'square'` (1/1), `'video'` (16/9), a compact ratio (`'4/3'`), a number, or `'auto'`
|
|
9
9
|
**Sizing**: `width`/`height` — number (÷4=rem), `'auto'`, `'fit'` (100%), `'fit-screen'` (100vw/vh), fractions (`'1/2'`…), `'33%'`. `minWidth`/`maxWidth`/`minHeight`/`maxHeight` same. All accept `"5%"`.
|
|
10
|
-
**Colors**: `bgColor`/`color`/`borderColor`/`outlineColor`/`fill`/`stroke` — Tailwind's OKLCH palette, 26 families × 11 steps `'50'`..`'950'` (slate/gray/zinc/neutral/stone/mauve/mist/olive/taupe/red/orange/amber/yellow/lime/green/emerald/teal/cyan/sky/blue/indigo/violet/purple/fuchsia/pink/rose), plus `'white'`/`'black'`/`'transparent'`/`'currentColor'`. **Opacity modifier on any colour value**: `bgColor="blue-500/40"` → `color-mix(in oklab, var(--blue-500) 40%, transparent)` — the mix wraps the _variable_, so it stays themed and shared; unlike `opacity` it fades one declaration, not the element. Works in `vars` and on a `Box.extend()` variable (`"brand/30"`); an unknown token or a percentage outside 0–100 emits nothing
|
|
10
|
+
**Colors**: `bgColor`/`color`/`borderColor`/`outlineColor`/`fill`/`stroke` — Tailwind's OKLCH palette, 26 families × 11 steps `'50'`..`'950'` (slate/gray/zinc/neutral/stone/mauve/mist/olive/taupe/red/orange/amber/yellow/lime/green/emerald/teal/cyan/sky/blue/indigo/violet/purple/fuchsia/pink/rose), plus `'white'`/`'black'`/`'transparent'`/`'currentColor'`. **Opacity modifier on any colour value**: `bgColor="blue-500/40"` → `color-mix(in oklab, var(--blue-500) 40%, transparent)` — the mix wraps the _variable_, so it stays themed and shared; unlike `opacity` it fades one declaration, not the element. Works in `vars` and on a `Box.extend()` variable (`"brand/30"`); an unknown token or a percentage outside 0–100 emits nothing. Two more families, `accent` and `base` (copies of `indigo` and `gray`), are what every built-in component is drawn in; `vars={Box.palette({ accent: 'orange', base: 'stone' })}` repoints them for a subtree, by family name or with a ramp of your own
|
|
11
11
|
**Borders**: `b`/`bx`/`by`/`bt`/`br`/`bb`/`bl`/`bs`/`be` (px — `bs`/`be` are the logical sides), `borderRadius` (÷4) and its nine physical corners plus six logical ones (`borderRadiusStart`/`borderRadiusEnd`, and `borderRadiusStartStart`/`StartEnd`/`EndEnd`/`EndStart` — **block axis first, inline axis second**), `borderStyle`
|
|
12
12
|
**Text**: `fontSize` (÷16), `fontWeight`, `lineHeight` (px), `textAlign` (`'start'`/`'end'` follow the text direction where `'left'`/`'right'` do not)/`textDecoration`/`textTransform`/`whiteSpace`/`textOverflow`, `textWrap`
|
|
13
13
|
**Position**: `position`, `top`/`right`/`bottom`/`left`/`inset`/`insetX`/`insetY`/`insetStart`/`insetEnd`, `zIndex` — number (÷4), `'auto'`, fractions (`'1/2'`, `'-1/3'`) or a percentage; `insetX`/`insetY` are `inset-inline`/`inset-block`, the way `mx`/`my` are `margin-inline`/`margin-block`, and `insetStart`/`insetEnd` are that axis's two sides
|
|
@@ -121,7 +121,8 @@ minimum**. `interpolate` names the space: `'srgb'`, `'hsl'`, `'oklab'`, `'oklch'
|
|
|
121
121
|
`'oklch'` is what keeps two stops out of the grey middle sRGB drags them through, and a `-longer` hue turns two stops
|
|
122
122
|
into a spectrum. The record is judged **whole**: one unknown stop, one misspelt key (`interpolat`), two kinds at once,
|
|
123
123
|
and the value emits no rule and no class name. It writes `background-image`, so `bgGradient` and `bgImage` are the
|
|
124
|
-
same property — use one.
|
|
124
|
+
same property — use one; `bgGradient: 'none'` is how a variant or a design takes a gradient off. `letterSpacing` is px as a
|
|
125
|
+
number and Tailwind's tracking names in `em` as a string (`'tighter'`…`'widest'`).
|
|
125
126
|
|
|
126
127
|
**Shadows stack, four at a time**: `box-shadow` is one property, so each layer sets its own custom property and all
|
|
127
128
|
four write the same composed declaration — a ring and an elevation coexist instead of the last rule winning.
|
|
@@ -68,7 +68,7 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
|
|
|
68
68
|
|
|
69
69
|
36. **A message you send rather than render is `<Toaster>` plus `toast()`.** `<Toaster />` once near the root of the app (`components/toaster`), and after that `toast.success('Saved')` from anywhere at all — an event handler, a fetch, a router guard, a module with no React in it. The store behind it is framework-free, so a call made **before** the viewport mounts is queued rather than lost, and the component only draws what is there. The surface is `toast(message, options)` with `success`/`error`/`warning`/`info`/`loading` beside it, `toast.promise(p, { loading, success, error })` (one toast for the whole call, and the promise comes back untouched), `toast.update(id, message, options)` and `toast.dismiss(id?)`; an `options.id` already on screen is an **update**, which is what makes a promise one toast rather than two. A toast takes a `description`, one `action` (`{ label, onClick }`, which dismisses the toast it answered unless `closeOnClick: false`), a `duration` in milliseconds (`Infinity` waits, and `loading` already does), and `onDismiss(reason)` with `timeout`/`close`/`action`/`imperative`. **The limit is a queue, not a cap**: past `limit` (default 3) a toast waits **with its timer unstarted**, so nothing expires that was never on screen, and a counter at the far end of the stack says how many are waiting. **Four things to know, all measured in Chrome 153.** The viewport is a **polite live region that exists before there is anything in it** — a region inserted together with its content is not reliably announced — and an error toast is `role="alert"`, the one announcement pattern every screen reader implements; nothing else carries a region of its own, because the nearest region to a change is the one that speaks. It is in the **top layer** with no portal (`popover="manual"`, shown as it mounts), so it inherits the theme and the direction around it, and it takes **no pointer events at all** while the toasts take them back — a corner-sized fixed strip would otherwise swallow every press in its gaps. **Timers stop on hover, on focus and in a background tab** and pick up where they left off, which is WCAG 2.2.1 and the only reason a toast may carry a control at all; a toast **never takes focus**, so `F6` (or `hotkey`, which takes `'alt+t'` or `false`) is what moves focus to the stack, Tab walks it, and Escape dismisses the toast focus is in and hands focus back once the stack is empty. And the viewport has to answer the UA's own `[popover]` rule: `inset: 0` leaves a corner-pinned stack over-constrained — `top`/`left` win and it sits in the wrong corner — and `overflow: auto` makes it a scroll container, so the style tree declares all four sides `auto` and `overflow: visible` before a `position` variant names the two it wants. `position` is `'bottom-end'` by default and its inline half is logical (`start`/`end`), the newest toast is always nearest the screen edge, and styling is the `toaster`/`toaster.toast`/`toaster.message`/`toaster.description`/`toaster.action`/`toaster.close`/`toaster.overflow` tree, where the kind is a variant on the toast.
|
|
70
70
|
|
|
71
|
-
37. **A DataGrid cell is edited by two props, and one of them judges the edit.** `column.editable` (`true`, or a predicate over the row) is the whole opt-in — `def.editable` is the grid-wide default a column falls back to — and **`def.onCellEdit({ rowKey, columnKey, row, value, oldValue })` both validates the edit and is told about it**: answer nothing to accept the value, a string to refuse it with that message, `false` to refuse it with the grid's own. It may return a **promise**, so a uniqueness check against a server is the same function; an answer to an edit the user has since abandoned is dropped rather than applied to whatever they are editing now, and a rejected promise is its message. **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean `checkbox
|
|
71
|
+
37. **A DataGrid cell is edited by two props, and one of them judges the edit.** `column.editable` (`true`, or a predicate over the row) is the whole opt-in — `def.editable` is the grid-wide default a column falls back to — and **`def.onCellEdit({ rowKey, columnKey, row, value, oldValue })` both validates the edit and is told about it**: answer nothing to accept the value, a string to refuse it with that message, `false` to refuse it with the grid's own. It may return a **promise**, so a uniqueness check against a server is the same function; an answer to an edit the user has since abandoned is dropped rather than applied to whatever they are editing now, and a rejected promise is its message. **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean `checkbox` (and is drawn as a check or a cross), an ISO date, time or date-time a `DateField` (an edit is written back in the shape the cell held: `…Z` stays UTC, a `Date` stays a `Date`), everything else `text` — so a column of numbers has a numeric keypad without being told; `column.editor` names one of the seven (`text`/`number`/`checkbox`/`select`/`date`/`time`/`datetime`) or configures it (`{ type, options?, placeholder?, step?, min?, max?, zone? }`, where `options` may be a function of the row), and `column.EditCell` is a control of your own bound to `cell.draft`, with `cell.setDraft(v)`, `cell.commitEdit()` and `cell.cancelEdit()` beside it — the same three calls the built-in four make, so a date picker is a component rather than a second copy of the commit rules. **A press chooses and a double press opens**: clicking a cell makes it the current one — the cell the arrows carry on from, marked whether the pointer or the keyboard put it there — and double-clicking an editable one opens its editor, the way a spreadsheet reads a double press; a cell that cannot be edited takes both as focus and nothing else, and a double press landing on a widget inside a cell (a tree chevron, a link in a `Cell` of your own) belongs to the widget rather than the editor. **The keyboard is APG's**: Enter or F2 opens the editor, any printable character opens it on that character (replacing the value, the way a spreadsheet reads a keystroke), Escape throws the draft away, Enter commits and hands the keyboard back to the cell, a press elsewhere commits, and **Tab commits and opens the next editable cell** — along the row and on into the rows after it, so a row of corrections is one gesture; `checkbox` and `select` commit on the change instead, since one interaction is the whole value. **Four things to know.** A **refused value keeps the editor open and the caret in it**, with the message in a `role="alert"` **in the top layer** — a bubble inside the scroller would be clipped away on the last row, which is the row a long grid is most often edited on — plus `aria-invalid` and `aria-describedby` on the field; typing clears it. **The grid does not own `data`, so an accepted value is kept as an edit _over_ the rows it was given** and is what every cell, every `Cell` renderer and every export reads from then on — which is what lets a `def.dataSource` grid be edited at all, since a value that was only reported would be gone on the next block. **`onCellEditsChange(edits, { reason })` reports the whole list, oldest first** — the stream an undo is built out of — and **`clearEdits()` on the grid's ref** is how a host says its own data has caught up; a datasource `refresh()` drops them with the blocks, because what the server says next is the newer answer. And **an edit never re-sorts or re-filters the grid**: sorting and filtering read the row's own values and only what is _displayed_ reads the edit, so a row cannot jump out from under the person typing into it. Styling is `datagrid.body.cell`'s `isEditing` and `isInvalid` variants — one ring, red when the value was refused, because two rings on the same rectangle means the editing one paints over the other — plus `datagrid.body.cell.editor` (`isPending`) and `datagrid.body.cell.error`.
|
|
72
72
|
|
|
73
73
|
38. **A DataGrid marks the cell it is on, and `def.rangeSelection` marks a block of them.** Every grid marks the cell its arrows carry on from — whether the pointer or the keyboard put it there — and the mark **survives the grid losing focus**, which is the whole point of it: a `:focus-visible` ring never matches a pointer, so a _clicked_ cell drew nothing (bug #64), and a ring made of focus leaves a copy with nothing to act on. **`Ctrl+C` needs no opt-in**: on the current cell it copies that one cell, which is the degenerate range. `def.rangeSelection: true` is the rest — **drag across cells, or hold Shift with the arrow keys** (Home/End, Ctrl+Home/End and PageUp/Down extend too), and a Shift+press extends from wherever the block was anchored; a move with no modifier collapses it onto the cell it landed on, and after a drag the ring, the tab stop and the next Shift+arrow are all the cell the drag ended on. What `Ctrl+C` writes is **tab-separated text with CRLF between the rows**, which is what Excel, Sheets and Numbers paste as columns; a field carrying a tab, a newline or a quote is quoted and its quotes doubled. **Four things to know.** The block is what reports itself: its cells carry `aria-selected` and the grid is `aria-multiselectable`, while a **lone current cell reports neither** — it has chosen nothing, and announcing "selected" on every arrow key is an announcement about nothing. The block is tinted _around_ the current cell, which keeps its ring, the way a sheet says where typing would land. **A copied figure is the one a file would carry**: the clipboard is an export, so a column's `exportValue` wins where it has one, an accepted edit wins over the row, and a group row copies its own value in the column that spans it; the copy rides the browser's own copy event, so text selected on the page still belongs to the page. And **marking cells is a drag, so text selection is not** — a grid with `rangeSelection` on gives text selection up (an open editor hands it back), a press that lands on a widget belongs to the widget, and a touch is left alone entirely, since that press is how the grid scrolls. `onRangeChange(range, { reason })` reports `{ startRow, endRow, columns, values() }` with `select`/`extend`/`clear` — indices rather than row keys, because a block may reach rows nobody has fetched, and `values()` is a call for the same reason. Styling is `datagrid.body.cell`'s `isCurrentCell` and `isInRange` variants.
|
|
74
74
|
|
|
@@ -100,4 +100,8 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
|
|
|
100
100
|
|
|
101
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
102
|
|
|
103
|
+
53. **A date, a time or a moment is an ISO string, and its shape is its kind.** `2026-10-19` is a date (no time, no zone — the same day everywhere), `14:30:00` a time, `2026-10-19T14:30:00` a wall clock with no zone, and anything with an offset (`…+03:00`, `…Z`) an instant. **Never hold a `Date`**: it is a moment in the runtime's zone, which is how a birthday becomes the day before in another country. `@box-kite/react/date` (no React, no engine, under 8 KB gz, every locale and zone from `Intl`) converts by naming the target — `toUtc(value, { from: 'Europe/Chisinau' })`, `toLocal(value)`, `toZone(value, 'America/New_York')`, `toPlainDate(value, { zone })` — and `format(value, { locale, zone, dateStyle })` writes one in the reader's words (presets, never format tokens); `parse(text, { locale, kind })` reads ISO, a `Date`, epoch ms, an HTTP date or text in the locale's order (`03/04/2026` is March in `en-US` and April in `en-GB`). `add`/`subtract` move the wall clock for days and up and the instant for hours and down, so a day later is the same time tomorrow across DST; `startOf(…, 'week')` follows the locale. **`'local'` is the reader**: the browser's zone in a browser, and on a server `configureDates({ timeZone })` or a `<DateProvider timeZone>` — read off the cookie `zoneCookie()` writes, with `readZoneCookie(header)` — while `systemZone()` is always the server's own. **The controls** — `<DateField>` (`components/dateField`: `type="date"|"time"|"datetime"`, segments in the locale's order, nothing typed is ever parsed), `<Calendar>` (`components/calendar`, `mode="range"`), `<DatePicker>`/`<DateRangePicker>` (`components/datePicker`, the top layer, a list of times every `step` minutes for `time`/`datetime`, `presets`), `<TimeZoneSelect>` (`components/timeZoneSelect`, the IANA name as its value) and `<DateTime value>` (`components/dateTime`, a `<time datetime>` in the reader's zone) — take and give that string, and **`onValueChange(value, { reason, utc, local, zoned, date, epoch, offset })`** hands the readings over beside it. A date-time's `zone` is `'local'` (default), `'utc'`, an IANA name or `'none'` (a wall clock that belongs to nobody); the value is the instant on that zone's clock (`…Z` for UTC). **They cannot cause a hydration error**: a server and a hydrating browser both render in the fallback (`UTC`, `en-US`, or what a `DateProvider` says) and the render after switches to the reader. `name` posts the canonical string and `required` stops the form; a DataGrid column takes `editor: 'date'|'time'|'datetime'`, `dateFormat` and `filterable: { type: 'date' }`, from a chunk fetched only when a grid shows a date. `<Textbox type="date">` is still the native input, whose language is the browser's rather than the page's.
|
|
104
|
+
|
|
103
105
|
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.
|
|
106
|
+
|
|
107
|
+
54. **A design is two families and a `Box.components()` object, and it is chosen in CSS.** Every built-in component is drawn in `accent` and `base` — two palette families that start as copies of `indigo` and `gray` — so `vars={Box.palette({ accent: 'orange', base: 'stone' })}` on an ancestor recolours every component inside it, by family name or with a ramp of your own (`{ 500: 'oklch(…)' }`); write `accent-*`/`base-*` in your own components when they should follow, and a named family when the colour is the content. The rest of a design is `Box.components(styles, { design: 'ember' })`: merged over the app's own and applied under `data-design="ember"`, which `<Box.Theme design="classic" designStorageKey="design">` writes on `<html>` and `Box.useDesign()` switches — chosen by an attribute rather than by context, so a pre-paint script that copies the stored name onto `<html>` is all a prerendered page needs, and server and browser render the same classes. **Three things to know.** A design merges and cannot delete a key, so it takes something back off with that prop's `none` (`shadow: 'none'`, `bgGradient: 'none'` — never `bgImage: 'none'`, which loses `background-image` to `bgGradient`). A design nested inside another stops the outer one, but `vars` inherit, so a `data-design="classic"` preview inside an Ember page redeclares its own `Box.palette()`. And a Box under `<Box.Components value>` resolves against that registry and no named design — `Box.createComponents()` is still the answer for a subtree whose design React decides. A part extends like a component does (`trigger: { extends: 'button' }`, `'dialog.title'`), and `letterSpacing` takes the tracking names `tighter`…`widest` in `em`.
|
package/AGENTS.md
CHANGED
|
@@ -100,7 +100,7 @@ Every value there is a prop, and every prop value becomes one class the engine i
|
|
|
100
100
|
|
|
101
101
|
36. **A message you send rather than render is `<Toaster>` plus `toast()`.** `<Toaster />` once near the root of the app (`components/toaster`), and after that `toast.success('Saved')` from anywhere at all — an event handler, a fetch, a router guard, a module with no React in it. The store behind it is framework-free, so a call made **before** the viewport mounts is queued rather than lost, and the component only draws what is there. The surface is `toast(message, options)` with `success`/`error`/`warning`/`info`/`loading` beside it, `toast.promise(p, { loading, success, error })` (one toast for the whole call, and the promise comes back untouched), `toast.update(id, message, options)` and `toast.dismiss(id?)`; an `options.id` already on screen is an **update**, which is what makes a promise one toast rather than two. A toast takes a `description`, one `action` (`{ label, onClick }`, which dismisses the toast it answered unless `closeOnClick: false`), a `duration` in milliseconds (`Infinity` waits, and `loading` already does), and `onDismiss(reason)` with `timeout`/`close`/`action`/`imperative`. **The limit is a queue, not a cap**: past `limit` (default 3) a toast waits **with its timer unstarted**, so nothing expires that was never on screen, and a counter at the far end of the stack says how many are waiting. **Four things to know, all measured in Chrome 153.** The viewport is a **polite live region that exists before there is anything in it** — a region inserted together with its content is not reliably announced — and an error toast is `role="alert"`, the one announcement pattern every screen reader implements; nothing else carries a region of its own, because the nearest region to a change is the one that speaks. It is in the **top layer** with no portal (`popover="manual"`, shown as it mounts), so it inherits the theme and the direction around it, and it takes **no pointer events at all** while the toasts take them back — a corner-sized fixed strip would otherwise swallow every press in its gaps. **Timers stop on hover, on focus and in a background tab** and pick up where they left off, which is WCAG 2.2.1 and the only reason a toast may carry a control at all; a toast **never takes focus**, so `F6` (or `hotkey`, which takes `'alt+t'` or `false`) is what moves focus to the stack, Tab walks it, and Escape dismisses the toast focus is in and hands focus back once the stack is empty. And the viewport has to answer the UA's own `[popover]` rule: `inset: 0` leaves a corner-pinned stack over-constrained — `top`/`left` win and it sits in the wrong corner — and `overflow: auto` makes it a scroll container, so the style tree declares all four sides `auto` and `overflow: visible` before a `position` variant names the two it wants. `position` is `'bottom-end'` by default and its inline half is logical (`start`/`end`), the newest toast is always nearest the screen edge, and styling is the `toaster`/`toaster.toast`/`toaster.message`/`toaster.description`/`toaster.action`/`toaster.close`/`toaster.overflow` tree, where the kind is a variant on the toast.
|
|
102
102
|
|
|
103
|
-
37. **A DataGrid cell is edited by two props, and one of them judges the edit.** `column.editable` (`true`, or a predicate over the row) is the whole opt-in — `def.editable` is the grid-wide default a column falls back to — and **`def.onCellEdit({ rowKey, columnKey, row, value, oldValue })` both validates the edit and is told about it**: answer nothing to accept the value, a string to refuse it with that message, `false` to refuse it with the grid's own. It may return a **promise**, so a uniqueness check against a server is the same function; an answer to an edit the user has since abandoned is dropped rather than applied to whatever they are editing now, and a rejected promise is its message. **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean `checkbox
|
|
103
|
+
37. **A DataGrid cell is edited by two props, and one of them judges the edit.** `column.editable` (`true`, or a predicate over the row) is the whole opt-in — `def.editable` is the grid-wide default a column falls back to — and **`def.onCellEdit({ rowKey, columnKey, row, value, oldValue })` both validates the edit and is told about it**: answer nothing to accept the value, a string to refuse it with that message, `false` to refuse it with the grid's own. It may return a **promise**, so a uniqueness check against a server is the same function; an answer to an edit the user has since abandoned is dropped rather than applied to whatever they are editing now, and a rejected promise is its message. **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean `checkbox` (and is drawn as a check or a cross), an ISO date, time or date-time a `DateField` (an edit is written back in the shape the cell held: `…Z` stays UTC, a `Date` stays a `Date`), everything else `text` — so a column of numbers has a numeric keypad without being told; `column.editor` names one of the seven (`text`/`number`/`checkbox`/`select`/`date`/`time`/`datetime`) or configures it (`{ type, options?, placeholder?, step?, min?, max?, zone? }`, where `options` may be a function of the row), and `column.EditCell` is a control of your own bound to `cell.draft`, with `cell.setDraft(v)`, `cell.commitEdit()` and `cell.cancelEdit()` beside it — the same three calls the built-in four make, so a date picker is a component rather than a second copy of the commit rules. **A press chooses and a double press opens**: clicking a cell makes it the current one — the cell the arrows carry on from, marked whether the pointer or the keyboard put it there — and double-clicking an editable one opens its editor, the way a spreadsheet reads a double press; a cell that cannot be edited takes both as focus and nothing else, and a double press landing on a widget inside a cell (a tree chevron, a link in a `Cell` of your own) belongs to the widget rather than the editor. **The keyboard is APG's**: Enter or F2 opens the editor, any printable character opens it on that character (replacing the value, the way a spreadsheet reads a keystroke), Escape throws the draft away, Enter commits and hands the keyboard back to the cell, a press elsewhere commits, and **Tab commits and opens the next editable cell** — along the row and on into the rows after it, so a row of corrections is one gesture; `checkbox` and `select` commit on the change instead, since one interaction is the whole value. **Four things to know.** A **refused value keeps the editor open and the caret in it**, with the message in a `role="alert"` **in the top layer** — a bubble inside the scroller would be clipped away on the last row, which is the row a long grid is most often edited on — plus `aria-invalid` and `aria-describedby` on the field; typing clears it. **The grid does not own `data`, so an accepted value is kept as an edit _over_ the rows it was given** and is what every cell, every `Cell` renderer and every export reads from then on — which is what lets a `def.dataSource` grid be edited at all, since a value that was only reported would be gone on the next block. **`onCellEditsChange(edits, { reason })` reports the whole list, oldest first** — the stream an undo is built out of — and **`clearEdits()` on the grid's ref** is how a host says its own data has caught up; a datasource `refresh()` drops them with the blocks, because what the server says next is the newer answer. And **an edit never re-sorts or re-filters the grid**: sorting and filtering read the row's own values and only what is _displayed_ reads the edit, so a row cannot jump out from under the person typing into it. Styling is `datagrid.body.cell`'s `isEditing` and `isInvalid` variants — one ring, red when the value was refused, because two rings on the same rectangle means the editing one paints over the other — plus `datagrid.body.cell.editor` (`isPending`) and `datagrid.body.cell.error`.
|
|
104
104
|
|
|
105
105
|
38. **A DataGrid marks the cell it is on, and `def.rangeSelection` marks a block of them.** Every grid marks the cell its arrows carry on from — whether the pointer or the keyboard put it there — and the mark **survives the grid losing focus**, which is the whole point of it: a `:focus-visible` ring never matches a pointer, so a _clicked_ cell drew nothing (bug #64), and a ring made of focus leaves a copy with nothing to act on. **`Ctrl+C` needs no opt-in**: on the current cell it copies that one cell, which is the degenerate range. `def.rangeSelection: true` is the rest — **drag across cells, or hold Shift with the arrow keys** (Home/End, Ctrl+Home/End and PageUp/Down extend too), and a Shift+press extends from wherever the block was anchored; a move with no modifier collapses it onto the cell it landed on, and after a drag the ring, the tab stop and the next Shift+arrow are all the cell the drag ended on. What `Ctrl+C` writes is **tab-separated text with CRLF between the rows**, which is what Excel, Sheets and Numbers paste as columns; a field carrying a tab, a newline or a quote is quoted and its quotes doubled. **Four things to know.** The block is what reports itself: its cells carry `aria-selected` and the grid is `aria-multiselectable`, while a **lone current cell reports neither** — it has chosen nothing, and announcing "selected" on every arrow key is an announcement about nothing. The block is tinted _around_ the current cell, which keeps its ring, the way a sheet says where typing would land. **A copied figure is the one a file would carry**: the clipboard is an export, so a column's `exportValue` wins where it has one, an accepted edit wins over the row, and a group row copies its own value in the column that spans it; the copy rides the browser's own copy event, so text selected on the page still belongs to the page. And **marking cells is a drag, so text selection is not** — a grid with `rangeSelection` on gives text selection up (an open editor hands it back), a press that lands on a widget belongs to the widget, and a touch is left alone entirely, since that press is how the grid scrolls. `onRangeChange(range, { reason })` reports `{ startRow, endRow, columns, values() }` with `select`/`extend`/`clear` — indices rather than row keys, because a block may reach rows nobody has fetched, and `values()` is a call for the same reason. Styling is `datagrid.body.cell`'s `isCurrentCell` and `isInRange` variants.
|
|
106
106
|
|
|
@@ -132,4 +132,8 @@ Every value there is a prop, and every prop value becomes one class the engine i
|
|
|
132
132
|
|
|
133
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
134
|
|
|
135
|
+
53. **A date, a time or a moment is an ISO string, and its shape is its kind.** `2026-10-19` is a date (no time, no zone — the same day everywhere), `14:30:00` a time, `2026-10-19T14:30:00` a wall clock with no zone, and anything with an offset (`…+03:00`, `…Z`) an instant. **Never hold a `Date`**: it is a moment in the runtime's zone, which is how a birthday becomes the day before in another country. `@box-kite/react/date` (no React, no engine, under 8 KB gz, every locale and zone from `Intl`) converts by naming the target — `toUtc(value, { from: 'Europe/Chisinau' })`, `toLocal(value)`, `toZone(value, 'America/New_York')`, `toPlainDate(value, { zone })` — and `format(value, { locale, zone, dateStyle })` writes one in the reader's words (presets, never format tokens); `parse(text, { locale, kind })` reads ISO, a `Date`, epoch ms, an HTTP date or text in the locale's order (`03/04/2026` is March in `en-US` and April in `en-GB`). `add`/`subtract` move the wall clock for days and up and the instant for hours and down, so a day later is the same time tomorrow across DST; `startOf(…, 'week')` follows the locale. **`'local'` is the reader**: the browser's zone in a browser, and on a server `configureDates({ timeZone })` or a `<DateProvider timeZone>` — read off the cookie `zoneCookie()` writes, with `readZoneCookie(header)` — while `systemZone()` is always the server's own. **The controls** — `<DateField>` (`components/dateField`: `type="date"|"time"|"datetime"`, segments in the locale's order, nothing typed is ever parsed), `<Calendar>` (`components/calendar`, `mode="range"`), `<DatePicker>`/`<DateRangePicker>` (`components/datePicker`, the top layer, a list of times every `step` minutes for `time`/`datetime`, `presets`), `<TimeZoneSelect>` (`components/timeZoneSelect`, the IANA name as its value) and `<DateTime value>` (`components/dateTime`, a `<time datetime>` in the reader's zone) — take and give that string, and **`onValueChange(value, { reason, utc, local, zoned, date, epoch, offset })`** hands the readings over beside it. A date-time's `zone` is `'local'` (default), `'utc'`, an IANA name or `'none'` (a wall clock that belongs to nobody); the value is the instant on that zone's clock (`…Z` for UTC). **They cannot cause a hydration error**: a server and a hydrating browser both render in the fallback (`UTC`, `en-US`, or what a `DateProvider` says) and the render after switches to the reader. `name` posts the canonical string and `required` stops the form; a DataGrid column takes `editor: 'date'|'time'|'datetime'`, `dateFormat` and `filterable: { type: 'date' }`, from a chunk fetched only when a grid shows a date. `<Textbox type="date">` is still the native input, whose language is the browser's rather than the page's.
|
|
136
|
+
|
|
135
137
|
Full reference: `docs/props.md` and `BOX_KITE_AI_CONTEXT.md`, in this package.
|
|
138
|
+
|
|
139
|
+
54. **A design is two families and a `Box.components()` object, and it is chosen in CSS.** Every built-in component is drawn in `accent` and `base` — two palette families that start as copies of `indigo` and `gray` — so `vars={Box.palette({ accent: 'orange', base: 'stone' })}` on an ancestor recolours every component inside it, by family name or with a ramp of your own (`{ 500: 'oklch(…)' }`); write `accent-*`/`base-*` in your own components when they should follow, and a named family when the colour is the content. The rest of a design is `Box.components(styles, { design: 'ember' })`: merged over the app's own and applied under `data-design="ember"`, which `<Box.Theme design="classic" designStorageKey="design">` writes on `<html>` and `Box.useDesign()` switches — chosen by an attribute rather than by context, so a pre-paint script that copies the stored name onto `<html>` is all a prerendered page needs, and server and browser render the same classes. **Three things to know.** A design merges and cannot delete a key, so it takes something back off with that prop's `none` (`shadow: 'none'`, `bgGradient: 'none'` — never `bgImage: 'none'`, which loses `background-image` to `bgGradient`). A design nested inside another stops the outer one, but `vars` inherit, so a `data-design="classic"` preview inside an Ember page redeclares its own `Box.palette()`. And a Box under `<Box.Components value>` resolves against that registry and no named design — `Box.createComponents()` is still the answer for a subtree whose design React decides. A part extends like a component does (`trigger: { extends: 'button' }`, `'dialog.title'`), and `letterSpacing` takes the tracking names `tighter`…`widest` in `em`.
|
package/BOX_AI_CONTEXT.md
CHANGED
|
@@ -307,6 +307,19 @@ Any colour value takes a slash and a percentage:
|
|
|
307
307
|
- A token the palette does not have, or a percentage outside 0–100, produces **no rule and no class
|
|
308
308
|
name** — the same silence every unmatched value gets.
|
|
309
309
|
|
|
310
|
+
**Two more families, `accent` and `base`, are what every built-in component is drawn in.** They start as
|
|
311
|
+
copies of `indigo` and `gray`, so nothing changes until one is pointed somewhere. `Box.palette()` returns the
|
|
312
|
+
`vars` record that does it, for an element and its subtree — a family by name, or a ramp of your own:
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
<Box vars={Box.palette({ accent: 'orange', base: 'stone' })} /> // every Button inside is orange
|
|
316
|
+
<Box vars={Box.palette({ accent: { 500: 'oklch(68% .19 42)' } })} /> // a step left out keeps its default
|
|
317
|
+
<Box theme={{ dark: { vars: Box.palette({ accent: 'amber' }) } }} /> // nests like any vars
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Write `accent-*`/`base-*` in your own components when they should follow a design, and a named family when the
|
|
321
|
+
colour is the content (a status, a chart series). A class built from them reads `bgColor-accent-500`.
|
|
322
|
+
|
|
310
323
|
### Gradients, shadows and rings
|
|
311
324
|
|
|
312
325
|
A gradient is a **value**, written as a record — so its stops are palette tokens, it takes the opacity
|
|
@@ -330,7 +343,10 @@ modifier, it is themed, and two elements asking for the same one share a class.
|
|
|
330
343
|
- A stop is any colour value — a token, `blue-500/40`, a system colour, `var(--chart-1)` — or a
|
|
331
344
|
`[colour, position]` pair. **Two stops minimum**, and the record is judged whole: one bad stop, one
|
|
332
345
|
unknown key, two kinds at once, and the whole value emits no rule and no class name.
|
|
333
|
-
- It writes `background-image`, so `bgGradient` and `bgImage` are the same property.
|
|
346
|
+
- It writes `background-image`, so `bgGradient` and `bgImage` are the same property. `bgGradient` is declared
|
|
347
|
+
later and wins, so a variant or a design takes a gradient off with `bgGradient: 'none'`, never `bgImage: 'none'`.
|
|
348
|
+
- `letterSpacing` is pixels as a number, and Tailwind's tracking names in `em` — `'tighter'` (-0.05em), `'tight'`,
|
|
349
|
+
`'normal'`, `'wide'`, `'wider'`, `'widest'` (0.1em) — which is what a display heading wants at every size.
|
|
334
350
|
|
|
335
351
|
**Four shadows stack**, because each sets its own custom property and all four write one composed
|
|
336
352
|
`box-shadow`. A ring and an elevation coexist instead of the last one winning:
|
|
@@ -1094,6 +1110,40 @@ const harbor = Box.createComponents({ button: { styles: { borderRadius: 0 } } },
|
|
|
1094
1110
|
- **A Server Component cannot read the provider.** A client component under it can.
|
|
1095
1111
|
- **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
1112
|
|
|
1113
|
+
### Box.components(styles, { design }) — A Design Chosen in CSS
|
|
1114
|
+
|
|
1115
|
+
A registry is chosen by React context, so a prerendered page can only switch to it after hydration — a flash, and a
|
|
1116
|
+
hydration mismatch if the client renders it first. A **named design** is chosen by an attribute instead: its styles are
|
|
1117
|
+
merged over the app's components and applied wherever an ancestor carries `data-design="<name>"`, so a stored choice
|
|
1118
|
+
written onto `<html>` before the first paint is the whole fix.
|
|
1119
|
+
|
|
1120
|
+
```tsx
|
|
1121
|
+
Box.components({ button: { styles: { borderRadius: 2.5 } }, dialog: { styles: { borderRadius: 5, p: 7 } } }, { design: 'ember' });
|
|
1122
|
+
|
|
1123
|
+
<Box.Theme
|
|
1124
|
+
use="global"
|
|
1125
|
+
design="classic"
|
|
1126
|
+
designStorageKey="design"
|
|
1127
|
+
globalStyles={{ dataAttr: { 'design=ember': { vars: Box.palette({ accent: 'orange', base: 'stone' }) } } }}
|
|
1128
|
+
>
|
|
1129
|
+
<App />
|
|
1130
|
+
</Box.Theme>;
|
|
1131
|
+
|
|
1132
|
+
const [design, setDesign] = Box.useDesign(); // setDesign(null) goes back to the design prop
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
- **Both designs are in the CSS.** A Box whose component a design changes carries the design's whole class list beside its
|
|
1136
|
+
own, each rule one class more specific — so every state the app's design styles (a theme, a hover, a variant) is
|
|
1137
|
+
outranked by the same state of the design. A component the design leaves alone costs nothing.
|
|
1138
|
+
- **To take something back off, write its `none`** (`shadow: 'none'`, `bgGradient: 'none'`): a design merges, it cannot delete a key.
|
|
1139
|
+
- **A design nested in another stops the outer one** — a `data-design="classic"` preview inside an Ember page is Classic.
|
|
1140
|
+
The palette is `vars`, which inherit, so that wrapper declares its own `Box.palette()` too.
|
|
1141
|
+
- **Props still win**, and a Box under `<Box.Components value>` resolves against that registry and no named design.
|
|
1142
|
+
- `Box.Theme` writes `data-design` on `<html>` (`use="global"`) or on its wrapper, restores `designStorageKey` after
|
|
1143
|
+
hydration without writing the default over a value a pre-paint script set, and pauses transitions for the switch.
|
|
1144
|
+
`applyDesignToElement` is the framework-free half, in `@box-kite/core`.
|
|
1145
|
+
- **A part can `extends` too**: `trigger: { extends: 'button' }` or `'dialog.title'`. It used to be silently ignored.
|
|
1146
|
+
|
|
1097
1147
|
---
|
|
1098
1148
|
|
|
1099
1149
|
## Extension System
|
|
@@ -2189,6 +2239,94 @@ Box.components({
|
|
|
2189
2239
|
|
|
2190
2240
|
---
|
|
2191
2241
|
|
|
2242
|
+
## Dates and times (`@box-kite/react/date` + the date components)
|
|
2243
|
+
|
|
2244
|
+
```tsx
|
|
2245
|
+
import { format, toUtc, toLocal, toZone, toPlainDate, parse, add, today } from '@box-kite/react/date';
|
|
2246
|
+
import DateField from '@box-kite/react/components/dateField';
|
|
2247
|
+
import Calendar from '@box-kite/react/components/calendar';
|
|
2248
|
+
import DatePicker, { DateRangePicker } from '@box-kite/react/components/datePicker';
|
|
2249
|
+
import TimeZoneSelect from '@box-kite/react/components/timeZoneSelect';
|
|
2250
|
+
import DateTime, { DateProvider } from '@box-kite/react/components/dateTime';
|
|
2251
|
+
|
|
2252
|
+
toUtc('2026-10-19T14:30', { from: 'Europe/Chisinau' }); // '2026-10-19T11:30:00Z'
|
|
2253
|
+
toLocal('2026-10-19T11:30:00Z'); // the reader's wall clock, with its offset
|
|
2254
|
+
format('2026-10-19', { locale: 'ro-MD', dateStyle: 'long' }); // '19 octombrie 2026'
|
|
2255
|
+
parse('03/04/2026', { locale: 'en-GB' }); // '2026-04-03'
|
|
2256
|
+
|
|
2257
|
+
<DateField label="Due" name="due" />;
|
|
2258
|
+
<DatePicker type="datetime" zone="utc" label="Publish at" onValueChange={(value, { utc, local }) => schedule(utc)} />;
|
|
2259
|
+
<DateRangePicker label="Stay" startName="from" endName="to" />;
|
|
2260
|
+
<DateTime value={order.createdAt} />;
|
|
2261
|
+
```
|
|
2262
|
+
|
|
2263
|
+
**Every value is an ISO string, and its shape is its kind** — never a `Date`, which is always a moment in
|
|
2264
|
+
the runtime's zone:
|
|
2265
|
+
|
|
2266
|
+
| Kind | Written as | Means |
|
|
2267
|
+
| ---------- | ------------------------------------------ | --------------------------------------------------------- |
|
|
2268
|
+
| `date` | `2026-10-19` | A day: no time, no zone, the same everywhere. |
|
|
2269
|
+
| `time` | `14:30:00` (`.sss` when there are any) | A time of day. |
|
|
2270
|
+
| `datetime` | `2026-10-19T14:30:00` | A wall clock with no zone — an alarm, a timetable. |
|
|
2271
|
+
| `instant` | `2026-10-19T14:30:00+03:00`, or `…Z` (UTC) | One moment, written as one zone sees it. Anything stored. |
|
|
2272
|
+
|
|
2273
|
+
A zone is `'local'` (the reader), `'utc'`, `'system'` (the runtime), an IANA name or a fixed offset.
|
|
2274
|
+
**`'local'` on a server** is `configureDates({ timeZone })` or the nearest `<DateProvider timeZone>`;
|
|
2275
|
+
`systemZone()` is always the server's own. The browser writes its zone into a cookie with
|
|
2276
|
+
`document.cookie = zoneCookie()`, and a server reads it back with `readZoneCookie(request.headers.get('cookie'))`.
|
|
2277
|
+
|
|
2278
|
+
| Function (`@box-kite/react/date`) | What it does |
|
|
2279
|
+
| ------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
2280
|
+
| `toUtc(v, { from })` / `toLocal(v)` / `toZone(v, zone)` | The moment in UTC / on the reader's clock / on another zone's, with offset. |
|
|
2281
|
+
| `toPlainDate` / `toPlainTime` / `toPlainDateTime` / `toInstant` | `(v, { zone })` — the day, time or wall clock a value shows in `zone`. |
|
|
2282
|
+
| `toDate` / `fromDate` / `toEpoch` | The bridge to an API that insists on a `Date`. |
|
|
2283
|
+
| `combine(date, time, { zone })` | A date and a time: a date-time, or with `zone` the instant. |
|
|
2284
|
+
| `today(zone)` / `now(zone)` / `currentTime(zone)` | In the reader's zone unless told. |
|
|
2285
|
+
| `format(v, { locale, zone, dateStyle, timeStyle, … })` | `Intl` options, never format tokens. `''` for a non-value. |
|
|
2286
|
+
| `formatRange` / `formatRelative` / `zoneName` | `Oct 19 – 23, 2026`, `3 minutes ago`, `EEST`. |
|
|
2287
|
+
| `parse(text, { locale, kind, zone })` | ISO, `Date`, epoch, HTTP date, or text in the locale's order. |
|
|
2288
|
+
| `add` / `subtract` / `startOf` / `endOf` / `difference` | Calendar units on the wall clock, clock units on the instant. |
|
|
2289
|
+
| `compare` / `isBefore` / `isAfter` / `isEqual` / `isSame` / `clamp` | Across kinds and zones. |
|
|
2290
|
+
| `details(v, { zone })` | Every reading at once: `utc`, `zoned`, `local`, `offset`, `date`, `epoch`. |
|
|
2291
|
+
| `weekInfo` / `hourCycleOf` / `monthNames` / `weekdayNames` | The locale, from `Intl`. |
|
|
2292
|
+
| `timeZones` / `offsetOf` / `isValidZone` / `normalizeZone` | The runtime's zones. |
|
|
2293
|
+
| `configureDates` / `localZone` / `systemZone` / `zoneCookie` / `readZoneCookie` | Who the reader is. |
|
|
2294
|
+
|
|
2295
|
+
**DST** follows Temporal: a wall-clock time a zone skips or repeats takes `disambiguation` —
|
|
2296
|
+
`'compatible'` (default: the later reading of a skipped time, the earlier of a repeated one), `'earlier'`,
|
|
2297
|
+
`'later'` or `'reject'` (`null`).
|
|
2298
|
+
|
|
2299
|
+
**The controls take and give the same string, and `onValueChange(value, details)` hands over the readings:**
|
|
2300
|
+
`details` is `{ reason, event, utc, local, zoned, date, epoch, zone, offset }`, all `null` while the value
|
|
2301
|
+
is. `reason` is `'keyboard'`, `'paste'`, `'select'`, `'preset'`, `'clear'` or `'reset'`.
|
|
2302
|
+
|
|
2303
|
+
| Component (`components/…`) | What it is |
|
|
2304
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2305
|
+
| `DateField` (`dateField`) | Segments (`role="spinbutton"`) in the locale's order. `type`, `zone`, `granularity`, `hourCycle`, `min`/`max`, `isDateUnavailable`, `name`, `required`, `label`. |
|
|
2306
|
+
| `Calendar` (`calendar`) | APG's grid keys. `mode="range"` → `{ start, end }`, `months`, `weekNumbers`, `min`/`max`, `isDateUnavailable`, `firstDayOfWeek`. |
|
|
2307
|
+
| `DatePicker` (`datePicker`) | A `DateField` with a calendar (`date`), a list of times every `step` minutes (`time`) or both (`datetime`) in the top layer; `presets`, `clearable`, `closeOnSelect`. |
|
|
2308
|
+
| `DateRangePicker` (`datePicker`) | Two fields over a range calendar; `startName`/`endName`, `months` (2), `presets` of `{ start, end }`. |
|
|
2309
|
+
| `TimeZoneSelect` | A `Combobox` over every zone, searchable by place, region or offset; the value is the IANA name. |
|
|
2310
|
+
| `DateTime` (`dateTime`) | `<time datetime>` in the reader's zone and language; `format`, `relative`, `zone`, `locale`. Also exports `DateProvider`. |
|
|
2311
|
+
|
|
2312
|
+
**A date-time's `zone`** is whose wall clock it is typed on: `'local'` (default), `'utc'`, an IANA name, or
|
|
2313
|
+
`'none'` (a wall clock with no zone, value `2026-10-19T14:30:00`). The value is the instant on that clock
|
|
2314
|
+
(`…+03:00`, or `…Z` for UTC), and a zone that is not the reader's is named after the time.
|
|
2315
|
+
|
|
2316
|
+
**Four things to know.** **Nothing typed is ever parsed**: `03/04/2026` is two days depending on who typed
|
|
2317
|
+
it, so each segment is a spin button and only a paste is read as text. **No hydration error, ever**: a
|
|
2318
|
+
server and a hydrating browser both render in the fallback (`UTC`/`en-US`, or a `DateProvider`'s), and the
|
|
2319
|
+
render after hydration switches to the reader — so pass the cookie's zone to a `DateProvider` for a first
|
|
2320
|
+
paint that is already right. **`name` posts the canonical string** (and `required` is a real input the
|
|
2321
|
+
browser validates), so a server reads `2026-10-19` whatever the reader's locale. And in a **DataGrid**, a
|
|
2322
|
+
column takes `editor: 'date' | 'time' | 'datetime'` (inferred from an ISO string or a `Date`),
|
|
2323
|
+
`dateFormat: 'date' | 'time' | 'datetime' | 'relative' | Intl options` and `filterable: { type: 'date' }`
|
|
2324
|
+
(`on`/`before`/`after`/`between`, each day a `DatePicker`); those parts are a chunk the grid fetches only when it shows a date.
|
|
2325
|
+
Styling: `dateField` (extends `textbox`; `segment`, `literal`, `zone`, `label`; `compact`, `invalid`,
|
|
2326
|
+
`disabled`), `calendar` (`nav`, `title`, `weekday`, `weekNumber`, `cell` with the range band, `day` with
|
|
2327
|
+
`today`/`selected`/`outside`/`blocked`/`unavailable`, `zoomItem`) and `datePicker` (`trigger`, `clear`,
|
|
2328
|
+
`presets`, `preset`, `times`, `time`, `part`); the panel is a `popover`.
|
|
2329
|
+
|
|
2192
2330
|
## Toaster (`toast()` + `<Toaster>`)
|
|
2193
2331
|
|
|
2194
2332
|
```tsx
|
|
@@ -2476,7 +2614,7 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
|
|
|
2476
2614
|
4. **fontSize divider is 16** (not 4). `fontSize={14}` → 14px
|
|
2477
2615
|
5. **Spacing divider is 4**. `p={4}` → 16px (1rem)
|
|
2478
2616
|
6. **Border width and lineHeight are direct px**. `b={1}` → 1px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px
|
|
2479
|
-
7. **Colors are Tailwind's OKLCH palette**: `'gray-500'`, `'blue-600'` — 26 families × 11 steps, and an opacity modifier on any of them: `bgColor="blue-500/40"` (a `color-mix`, not `opacity`)
|
|
2617
|
+
7. **Colors are Tailwind's OKLCH palette**: `'gray-500'`, `'blue-600'` — 26 families × 11 steps, and an opacity modifier on any of them: `bgColor="blue-500/40"` (a `color-mix`, not `opacity`). The built-in components are drawn in two more, `accent` and `base`, which `Box.palette()` repoints
|
|
2480
2618
|
8. **Breakpoints are mobile-first**: base → sm → md → lg → xl → xxl
|
|
2481
2619
|
9. **Theme styles nest**: `theme={{ dark: { hover: { ... } } }}`
|
|
2482
2620
|
10. **HTML attributes go in `props` prop**: `<Link props={{ href: '/about' }}>` not `<Link href>`. `data-*` and `aria-*` go there too — a `data-state` written at the top level typechecks and is then dropped
|
|
@@ -2796,11 +2934,13 @@ the same function — and an answer to an edit the user has since abandoned is d
|
|
|
2796
2934
|
```
|
|
2797
2935
|
|
|
2798
2936
|
- **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean
|
|
2799
|
-
`checkbox
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2937
|
+
`checkbox` (drawn as a check or a cross, read out as `localeText.booleanTrue`/`booleanFalse`), an ISO
|
|
2938
|
+
date, time or date-time a `DateField` (an edit keeps the shape the cell held: `…Z` stays UTC, a `Date`
|
|
2939
|
+
a `Date`), everything else `text` — unless `column.editor` names one of the seven
|
|
2940
|
+
(`text`/`number`/`checkbox`/`select`/`date`/`time`/`datetime`) or configures it
|
|
2941
|
+
(`{ type, options?, placeholder?, step?, min?, max?, zone? }`; `options` may be a function of the row).
|
|
2942
|
+
`column.EditCell` is a control of your own, bound to `cell.draft` with `cell.setDraft(v)`,
|
|
2943
|
+
`cell.commitEdit()` and `cell.cancelEdit()` — the same three calls the built-in seven make.
|
|
2804
2944
|
- **Pointer:** a press makes a cell the current one — it takes the ring, whether the pointer or the
|
|
2805
2945
|
keyboard put it there — and a **double press opens the editor** on an editable cell. A cell that cannot
|
|
2806
2946
|
be edited takes both as focus and nothing else, and a double press landing on a widget inside a cell (a
|