@box-kite/react 2.2.17 → 2.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.claude/rules/box-kite-rules.md +3 -3
  2. package/.claude/skills/box-kite/SKILL.md +3 -3
  3. package/.claude/skills/box-kite/references/components.md +1 -1
  4. package/.claude/skills/box-kite/references/styling.md +1 -1
  5. package/.cursor/rules/box-kite.mdc +3 -3
  6. package/AGENTS.md +3 -3
  7. package/BOX_AI_CONTEXT.md +2 -2
  8. package/BOX_KITE_AI_CONTEXT.md +2 -2
  9. package/behavior.cjs +1 -1
  10. package/behavior.mjs +62 -62
  11. package/catalog.cjs +1 -1
  12. package/catalog.mjs +3 -3
  13. package/client.cjs +1 -1
  14. package/client.mjs +15 -15
  15. package/components/accordion.cjs +1 -1
  16. package/components/accordion.mjs +1 -1
  17. package/components/calendar.cjs +1 -1
  18. package/components/calendar.mjs +1 -1
  19. package/components/combobox.cjs +1 -1
  20. package/components/combobox.mjs +1 -1
  21. package/components/dashboard.cjs +1 -1
  22. package/components/dashboard.mjs +1 -1
  23. package/components/dataGrid.cjs +2 -2
  24. package/components/dataGrid.mjs +11 -11
  25. package/components/dateField.cjs +1 -1
  26. package/components/dateField.mjs +1 -1
  27. package/components/datePicker.cjs +1 -1
  28. package/components/datePicker.mjs +2 -5
  29. package/components/dialog.cjs +1 -1
  30. package/components/dialog.mjs +108 -101
  31. package/components/form.cjs +1 -1
  32. package/components/form.mjs +1 -1
  33. package/components/menu.cjs +1 -1
  34. package/components/menu.mjs +1 -1
  35. package/components/overlay.cjs +1 -1
  36. package/components/overlay.mjs +4 -4
  37. package/components/popover.cjs +1 -1
  38. package/components/popover.mjs +1 -1
  39. package/components/radioGroup.cjs +1 -1
  40. package/components/radioGroup.mjs +1 -1
  41. package/components/slider.cjs +1 -1
  42. package/components/slider.mjs +1 -1
  43. package/components/sortableList.cjs +1 -1
  44. package/components/sortableList.mjs +1 -1
  45. package/components/swipeItem.cjs +1 -1
  46. package/components/swipeItem.mjs +11 -11
  47. package/components/tabs.cjs +1 -1
  48. package/components/tabs.mjs +1 -1
  49. package/components/toaster.cjs +1 -1
  50. package/components/toaster.mjs +133 -115
  51. package/components/tooltip.cjs +1 -1
  52. package/components/tooltip.mjs +1 -1
  53. package/components/virtualList.cjs +1 -1
  54. package/components/virtualList.mjs +1 -1
  55. package/interactionListening.cjs +1 -1
  56. package/interactionListening.mjs +3 -3
  57. package/layer.cjs +1 -1
  58. package/layer.mjs +17 -14
  59. package/package.json +2 -2
  60. package/placement.cjs +1 -1
  61. package/placement.mjs +10 -10
  62. package/platform.cjs +1 -1
  63. package/platform.mjs +18 -11
  64. package/react-shared.cjs +1 -1
  65. package/react-shared.mjs +1 -1
  66. package/scrolling.cjs +1 -1
  67. package/scrolling.mjs +1 -1
  68. package/utils/dom/domUtils.d.ts +7 -0
@@ -28,7 +28,7 @@ globs: '**/*.{ts,tsx,jsx}'
28
28
 
29
29
  20. **An opacity is part of the colour, not a second prop**: `bgColor="blue-500/40"`, `borderColor="black/10"`, `color="currentColor/60"` — a slash and a percentage on any colour value, on every colour prop and on a `vars` entry. It compiles to `color-mix(in oklab, var(--blue-500) 40%, transparent)`, so the token stays a variable (still themed, still one shared class) — unlike `opacity`, which fades the element, its text and its children. **A token carries its own value as the fallback** — `bgColor="blue-500"` is `var(--blue-500,oklch(62.3% .214 259.8))`, and `var(--blue-500)` in these rules is that, shortened — so nothing is declared in `:root`: your own stylesheet reads a token through a variable of yours (`vars={{ brand: 'violet-500' }}` on an ancestor), while a hand-written `var(--violet-500)` inside a Box value (`fill`, `vars`, `css`, a `Box.extend()` variable) gets the fallback too. The palette itself is **Tailwind 4.3's, in OKLCH**: twenty-six families (the five neutrals, `mauve`/`mist`/`olive`/`taupe`, and seventeen hues) of eleven steps, `50`–`950`. A token the palette does not have, or a percentage outside 0–100, produces no rule and no class name; a variable from `Box.extend()` takes the modifier too
30
30
 
31
- 21. **A gradient is a value, and a shadow is a stack.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction `t`/`tr`/`r`/… or an angle in degrees, `radial` a shape, `conic` a start angle), `colors` are the stops in order, and a stop is any colour value: a token, `blue-500/40`, `var(--chart-1)`, or a `[colour, position]` pair. `at` centres a radial or conic one, `interpolate="oklch"` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. Two stops minimum, and the record is judged **whole** — one bad stop or one unknown key and the value emits nothing. It writes `background-image`, so it and `bgImage` are the same property. **Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` on Tailwind's scale, or the presets `small`/`medium`/`large`), `insetShadow` (`xxs`/`xs`/`sm`), and `ring`/`insetRing`, which are a **width in px** (`ring={2}`) that follows `borderRadius` and costs no layout, unlike `outline`. Each takes a colour of its own (`shadowColor`, `ringColor`, …) which shows nothing until the layer is painted; `none` — or `0` on a ring — clears just that layer. `textShadow` is `xxs`…`lg` with `textShadowColor`, and `transition="shadow"` covers both properties
31
+ 21. **A gradient is a value, and a shadow is a stack.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction `t`/`tr`/`r`/… or an angle in degrees, `radial` a shape, `conic` a start angle), `colors` are the stops in order, and a stop is any colour value: a token, `blue-500/40`, `var(--chart-1)`, a `Box.extend()` colour, or a `[colour, position]` pair. `at` centres a radial or conic one, `interpolate="oklch"` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. Two stops minimum, and the record is judged **whole** — one bad stop or one unknown key and the value emits nothing. It writes `background-image`, so it and `bgImage` are the same property. **Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` on Tailwind's scale, or the presets `small`/`medium`/`large`), `insetShadow` (`xxs`/`xs`/`sm`), and `ring`/`insetRing`, which are a **width in px** (`ring={2}`) that follows `borderRadius` and costs no layout, unlike `outline`. Each takes a colour of its own (`shadowColor`, `ringColor`, …) which shows nothing until the layer is painted; `none` — or `0` on a ring — clears just that layer. `textShadow` is `xxs`…`lg` with `textShadowColor`, and `transition="shadow"` covers both properties
32
32
 
33
33
  22. **Nine filters stack too, and a mask is a gradient.** `blur`, `brightness`, `contrast`, `grayscale`, `hueRotate`, `invert`, `saturate`, `sepia` and `dropShadow` each set their own layer of one composed `filter`, so a blur and a desaturation coexist. **A number is the function's own unit** — `brightness={110}` is a percentage, `hueRotate={90}` degrees, `blur={3}` pixels — and `blur` takes Tailwind's scale as well (`xs` 4px … `xxxl` 64px). `dropShadow` (`xs`…`xxl`, with `dropShadowColor`) is cast by the **shape**, where `shadow` draws a rectangle; `none` clears one function. The nine `backdrop*` props are the same functions behind the element — `backdropBlur="sm" backdropSaturate={180}` over something translucent is glassmorphism — and they and the older `backdropFilter` are the same property, so use one. **`maskImage` takes the `bgGradient` record** and reads its alpha: `maskImage={{ linear: 'b', colors: ['black', 'transparent'] }}` fades an edge, `url(#id)` masks by a shape. **`bgClip="text"` needs `color="transparent"`** beside it, and together they are how a gradient becomes type
34
34
 
@@ -48,7 +48,7 @@ globs: '**/*.{ts,tsx,jsx}'
48
48
 
49
49
  30. **Every floating layer is in the top layer now, and none of them is portalled.** `Overlay` — and so `Tooltip`, the `Dropdown` popup and the DataGrid column menu, which are all built on it — renders where it is declared and carries `popover="manual"`, shown as soon as it mounts. `manual`, not `auto`, because a layer owns no dismissal and light dismiss is a pattern: `<Popover>` is the `auto` one (rule 29), and `Tooltip` and `Dropdown` each keep the stricter dismissal they already implement — a tooltip's Escape has to _last_ (WCAG 1.4.13), and a JS-toggled trigger fights light dismiss. Four things follow, all measured in Chrome 152. The layer beats a `z-index: 9999` sibling and a transformed ancestor, where a plain `position: fixed` layer at the same coordinates loses both. It **inherits** the theme, the custom properties and the direction around it, so a local `Box.Theme` finally reaches inside a dropdown and there is no `dir` to copy across. The tab order follows the markup, so a layer declared after its trigger is what Tab reaches next. And a press inside it is a press inside whatever popover it was _declared_ in, so **a `Dropdown` inside a `Popover` panel no longer dismisses the panel** — the bug a portal caused by moving the popup to the body, where every press in it read as outside. **The one cost, measured:** a layer in the top layer **keeps the side it chose when it opened** — Chrome re-evaluates `position-try-fallbacks` on scroll for an ordinary positioned element and never for a top-layer one, so a layer left open while the page scrolls does not flip when its side runs out of room and slides past the viewport edge instead. Every _open_ picks the right side, since a layer that mounts when it opens is laid out for the first time then; only a scroll _while_ open is affected, nothing but leaving and re-entering the top layer re-arms it, and the portal fallback (which measures) does not have it. It applies to `<Popover>` too. Close a layer if the page can scroll far underneath it. The trap is the other side of the same coin: **never declare a layer inside its trigger.** It used to be legal because the portal moved it before a browser saw it, and now a `role="listbox"` or a `role="menu"` inside a `<button>` is unreachable content — put it beside the trigger instead. `<Presence>` still owns the mount, so a closed dropdown renders none of its options and the exit still runs (a top-layer element transitions normally); where the browser has no Popover API the portal comes back, with everything it used to cost.
50
50
 
51
- 31. **A modal is `<Dialog>`, and the platform is the whole pattern.** `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>` (`components/dialog`) is a real `<dialog>` shown with `showModal()`, which supplies **the top layer, a `::backdrop`, an inert page behind it, Escape, focus containment and focus return** — so the focus trap, the `aria-hidden` sweep and the `z-index` strategy every other library ships are all absent here. `<AlertDialog>` (a named export of the same module) is `role="alertdialog"`, always modal and never dismissed by a press outside, because a decision that can be clicked away is one the user did not make; Escape still closes it, and `initialFocus` is what puts focus on the least destructive action, which is APG's rule for it. **Rendering a `Dialog.Title` is what names the dialog** — it writes `aria-labelledby`, and a `Dialog.Description` writes `aria-describedby`; neither attribute is written when the part is absent, so `label`/`labelledBy`/`describedBy` are the answer for a dialog that shows no heading. It takes `modal` (default `true` — `false` is `show()`: no backdrop, nothing inert, and the UA positions it against its containing block), `dismissible`, `lockScroll` (default: whatever `modal` is) and `onOpenChange(open, { reason })` with the same four reasons `<Popover>` uses, `imperative` covering a `close()` call and a `<form method="dialog">` submit. **Four things to know, all measured in Chrome 152.** The dialog is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>` — same as `<Popover>`. A **close cannot be refused**: `cancel` is cancelable but the browser has already closed the dialog by the time `onOpenChange` runs, so `dismissible={false}` is how a decision is made unavoidable. A press outside is `closedby="any"`, written for you — and where the browser has not got it yet the press is measured against the dialog's own box, because **for a modal dialog the backdrop _is_ the dialog element** and every containment test calls a press on it inside. And the platform does **not** stop the page scrolling, which is why `lockScroll` exists at all: it is an `overflow: hidden` class on `<html>`, held by a counter so an inner dialog closing does not unlock the page under an outer one.
51
+ 31. **A modal is `<Dialog>`, and the platform is the whole pattern.** `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>` (`components/dialog`) is a real `<dialog>` shown with `showModal()`, which supplies **the top layer, a `::backdrop`, an inert page behind it, Escape, focus containment and focus return** — so the focus trap, the `aria-hidden` sweep and the `z-index` strategy every other library ships are all absent here. `<AlertDialog>` (a named export of the same module) is `role="alertdialog"`, always modal and never dismissed by a press outside, because a decision that can be clicked away is one the user did not make; Escape still closes it, and `initialFocus` is what puts focus on the least destructive action, which is APG's rule for it. **Rendering a `Dialog.Title` is what names the dialog** — it writes `aria-labelledby`, and a `Dialog.Description` writes `aria-describedby`; neither attribute is written when the part is absent, so `label`/`labelledBy`/`describedBy` are the answer for a dialog that shows no heading. It takes `modal` (default `true` — `false` is `show()`: no backdrop, nothing inert, and the UA positions it against its containing block), `dismissible`, `lockScroll` (default: whatever `modal` is) and `onOpenChange(open, { reason })` with the same four reasons `<Popover>` uses, `imperative` covering a `close()` call and a `<form method="dialog">` submit. **Four things to know, all measured in Chrome 152.** The dialog is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>` — same as `<Popover>`. A **close cannot be refused**: `cancel` is cancelable but the browser has already closed the dialog by the time `onOpenChange` runs, so `dismissible={false}` is how a decision is made unavoidable. **Everything outside an open modal is inert**, a top-layer layer included: a `<Toaster>` moves its stack inside the dialog for that reason, but a `Tooltip` or `Dropdown` declared outside it and left open as it opens stays inert until it closes. A press outside is `closedby="any"`, written for you — and where the browser has not got it yet the press is measured against the dialog's own box, because **for a modal dialog the backdrop _is_ the dialog element** and every containment test calls a press on it inside. And the platform does **not** stop the page scrolling, which is why `lockScroll` exists at all: it is an `overflow: hidden` class on `<html>`, held by a counter so an inner dialog closing does not unlock the page under an outer one.
52
52
 
53
53
  32. **A menu button is `<Menu>`, and a submenu is a popover nested in it.** `<Menu trigger={(t) => <Button {...t}>Actions</Button>}><Menu.Item onSelect={duplicate}>Duplicate</Menu.Item><Menu.Separator /><Menu.Sub label="Share"><Menu.Item>Copy link</Menu.Item></Menu.Sub></Menu>` (`components/menu`) is APG's menu button on the Popover API: `role="menu"`, `menuitem`/`menuitemcheckbox`/`menuitemradio` with `aria-checked`, `role="group"` around a titled section and `role="separator"` between them. The parts are `Menu.Item`, `Menu.CheckboxItem`, `Menu.RadioGroup` + `Menu.RadioItem`, `Menu.Group` (a `label` names the group, so its items are read as a set), `Menu.Separator` and `Menu.Sub`, which renders its own item — label, chevron, `aria-haspopup="menu"`, `aria-expanded` — and the menu beside it. **The platform's half is the top layer, light dismiss and the toggle**: Escape closes the innermost menu first, one layer per press, and a press outside closes the lot, with no portal and no `z-index` anywhere. **What is written here is the keyboard and the ARIA**: Down/Up wrap, Home/End, typeahead (a letter again cycles through the items starting with it), Right opens a submenu and Left closes it — the _reading order_, so the two swap in a right-to-left menu and the chevron turns round with them. **Four things to know.** A `disabled` item is `aria-disabled` and **stays focusable**, which is APG's rule: the `disabled` attribute would take it out of the keyboard's reach and silence it. **A command closes the menu and a state does not** — `Menu.Item` closes on select, `Menu.CheckboxItem` and `Menu.RadioItem` do not, so several boxes can be ticked in one visit, and `closeOnSelect` swaps either default. `onOpenChange(open, { reason })` adds **`select`** (an item was chosen — the transition worth telling apart) and **`tab`** to the four a layer always reports. And the menu is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>`, the items render whether or not it has been opened, and the top layer's one cost applies to it as it does to `<Popover>` (rule 30): a menu keeps the side it opened on. Placement is `side`/`align`/`offset`/`flip` on both the menu and each `Menu.Sub`, whose defaults are `side="end"` and `offset={0}` so a submenu abuts the menu it came out of. Opening a submenu moves focus into it, hover included, which is what keeps the highlight and the keyboard in one place. The semantic `<menu>` element is **`MenuList`** from `components/semantics` now — `Menu` there is deprecated, because two exports of that name are one import away from the wrong component.
54
54
 
@@ -58,7 +58,7 @@ globs: '**/*.{ts,tsx,jsx}'
58
58
 
59
59
  35. **A slider is `<Slider>`, a bar is `<Progress>`, and a number in is a number out.** `<Slider label="Volume" defaultValue={40} onValueChange={(value) => setVolume(value)} />` (`components/slider`) is APG's slider, and `defaultValue={[20, 80]}` with `thumbLabels={['Lowest', 'Highest']}` is the multi-thumb one — **the value's own shape is how many thumbs there are**, so there is no second prop saying which kind this is and no narrowing at the call site: a `number` in means a `number` back, an array means an array. A thumb may meet its neighbour and never pass it. `<Progress label="Upload" value={62} />` (`components/progress`) is the `role="progressbar"` beside it, and **it renders on a server** — no state, no effect, no measurement. **Neither is the native element, and that is the one place the platform loses**: `<input type="range">` holds a single thumb, and it and `<progress>` both draw themselves with vendor pseudo-elements (`::-webkit-slider-thumb`) that no typed prop can reach — so `name` on a `Slider` is what keeps the form working, writing one hidden input per thumb. **Four things to know.** **The position is an inline style, and it is the only one**: a thumb's offset is per _frame_ of a drag, so a class for it would be a rule per frame that is never freed — the exception `useAnchorPosition`'s anchor name and the travelling tab indicator already take, and the reason a `ProgressRing` (`components/chart`) can round its fraction into a class instead, since nobody drags a ring. **No `value` on a `Progress` is a state, not a zero**: `aria-valuenow` is omitted rather than reported as `0`, and the bar sweeps — a named duration, so it stops itself under `prefers-reduced-motion`. **A `role="slider"` has to be named**: `label` names the thumb on a single slider and the `role="group"` on a range, where `thumbLabels` names the thumbs one at a time, and `format` writes `aria-valuetext` for a value a bare number does not read as. And **it mirrors for free** — the fill and the thumbs are placed with `inset-inline-start`, so a right-to-left page draws the minimum on the right with nothing declared twice; only the sideways arrows swap (Left is the increase), and Up/Down never do, because the block axis has no reading order. `onValueChange` fires on every step and `onValueCommit` once at the end, both with `{ reason: 'pointer' | 'keyboard' }`. **A press travels and a nudge does not**: a press on the track is the one move the eye has to follow, so the thumb animates the whole way, while every other move — a drag, an arrow key, a held arrow — takes a short 60ms **`ease-out`** travel on both the thumb and the fill, which is the `tracking` variant and which both parts must carry or they come apart. Short rather than _off_, because off is exact and **steps**: a value on a grid can only be at its grid positions, so a 1-in-100 slider moves 3.2px at a time on a 320px track. Eased **out** rather than linear or eased, because a repeat restarts the transition every 33ms so only its first half is ever seen — on this curve that half is the straight part, so it glides while the key is held and still decelerates on the last transition, the only one allowed to finish; plain `ease` replays its slow-in and pulses, and `linear` rides more than a full step behind the value and stops at full speed. And an arrow key is a nudge rather than a jump, so it never takes the press travel at all — one tap spending 250ms on 3.2px was itself the lag. `step` is the real dial — `step={0}` is continuous and needs no interpolation at all. Styling is the `slider`/`slider.track`/`slider.fill`/`slider.thumb` tree and `progress`/`progress.fill`, whose `indeterminate` variant is the sweep; `disabled` is `aria-disabled` here rather than the attribute, since a div takes none and a value nobody can reach is a value nobody can read.
60
60
 
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.
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. **While a modal `Dialog` is open the stack moves inside it**, shown after it — everything outside an open modal is inert, so a toast left on the page would sit under the backdrop, unpressable and silent — and moves back out when the dialog closes; Escape on a toast there dismisses the toast, not the dialog. **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
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
 
@@ -101,7 +101,7 @@ Detect the package manager from the lock file rather than asking:
101
101
 
102
102
  20. **An opacity is part of the colour, not a second prop**: `bgColor="blue-500/40"`, `borderColor="black/10"`, `color="currentColor/60"` — a slash and a percentage on any colour value, on every colour prop and on a `vars` entry. It compiles to `color-mix(in oklab, var(--blue-500) 40%, transparent)`, so the token stays a variable (still themed, still one shared class) — unlike `opacity`, which fades the element, its text and its children. **A token carries its own value as the fallback** — `bgColor="blue-500"` is `var(--blue-500,oklch(62.3% .214 259.8))`, and `var(--blue-500)` in these rules is that, shortened — so nothing is declared in `:root`: your own stylesheet reads a token through a variable of yours (`vars={{ brand: 'violet-500' }}` on an ancestor), while a hand-written `var(--violet-500)` inside a Box value (`fill`, `vars`, `css`, a `Box.extend()` variable) gets the fallback too. The palette itself is **Tailwind 4.3's, in OKLCH**: twenty-six families (the five neutrals, `mauve`/`mist`/`olive`/`taupe`, and seventeen hues) of eleven steps, `50`–`950`. A token the palette does not have, or a percentage outside 0–100, produces no rule and no class name; a variable from `Box.extend()` takes the modifier too
103
103
 
104
- 21. **A gradient is a value, and a shadow is a stack.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction `t`/`tr`/`r`/… or an angle in degrees, `radial` a shape, `conic` a start angle), `colors` are the stops in order, and a stop is any colour value: a token, `blue-500/40`, `var(--chart-1)`, or a `[colour, position]` pair. `at` centres a radial or conic one, `interpolate="oklch"` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. Two stops minimum, and the record is judged **whole** — one bad stop or one unknown key and the value emits nothing. It writes `background-image`, so it and `bgImage` are the same property. **Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` on Tailwind's scale, or the presets `small`/`medium`/`large`), `insetShadow` (`xxs`/`xs`/`sm`), and `ring`/`insetRing`, which are a **width in px** (`ring={2}`) that follows `borderRadius` and costs no layout, unlike `outline`. Each takes a colour of its own (`shadowColor`, `ringColor`, …) which shows nothing until the layer is painted; `none` — or `0` on a ring — clears just that layer. `textShadow` is `xxs`…`lg` with `textShadowColor`, and `transition="shadow"` covers both properties
104
+ 21. **A gradient is a value, and a shadow is a stack.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction `t`/`tr`/`r`/… or an angle in degrees, `radial` a shape, `conic` a start angle), `colors` are the stops in order, and a stop is any colour value: a token, `blue-500/40`, `var(--chart-1)`, a `Box.extend()` colour, or a `[colour, position]` pair. `at` centres a radial or conic one, `interpolate="oklch"` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. Two stops minimum, and the record is judged **whole** — one bad stop or one unknown key and the value emits nothing. It writes `background-image`, so it and `bgImage` are the same property. **Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` on Tailwind's scale, or the presets `small`/`medium`/`large`), `insetShadow` (`xxs`/`xs`/`sm`), and `ring`/`insetRing`, which are a **width in px** (`ring={2}`) that follows `borderRadius` and costs no layout, unlike `outline`. Each takes a colour of its own (`shadowColor`, `ringColor`, …) which shows nothing until the layer is painted; `none` — or `0` on a ring — clears just that layer. `textShadow` is `xxs`…`lg` with `textShadowColor`, and `transition="shadow"` covers both properties
105
105
 
106
106
  22. **Nine filters stack too, and a mask is a gradient.** `blur`, `brightness`, `contrast`, `grayscale`, `hueRotate`, `invert`, `saturate`, `sepia` and `dropShadow` each set their own layer of one composed `filter`, so a blur and a desaturation coexist. **A number is the function's own unit** — `brightness={110}` is a percentage, `hueRotate={90}` degrees, `blur={3}` pixels — and `blur` takes Tailwind's scale as well (`xs` 4px … `xxxl` 64px). `dropShadow` (`xs`…`xxl`, with `dropShadowColor`) is cast by the **shape**, where `shadow` draws a rectangle; `none` clears one function. The nine `backdrop*` props are the same functions behind the element — `backdropBlur="sm" backdropSaturate={180}` over something translucent is glassmorphism — and they and the older `backdropFilter` are the same property, so use one. **`maskImage` takes the `bgGradient` record** and reads its alpha: `maskImage={{ linear: 'b', colors: ['black', 'transparent'] }}` fades an edge, `url(#id)` masks by a shape. **`bgClip="text"` needs `color="transparent"`** beside it, and together they are how a gradient becomes type
107
107
 
@@ -121,7 +121,7 @@ Detect the package manager from the lock file rather than asking:
121
121
 
122
122
  30. **Every floating layer is in the top layer now, and none of them is portalled.** `Overlay` — and so `Tooltip`, the `Dropdown` popup and the DataGrid column menu, which are all built on it — renders where it is declared and carries `popover="manual"`, shown as soon as it mounts. `manual`, not `auto`, because a layer owns no dismissal and light dismiss is a pattern: `<Popover>` is the `auto` one (rule 29), and `Tooltip` and `Dropdown` each keep the stricter dismissal they already implement — a tooltip's Escape has to _last_ (WCAG 1.4.13), and a JS-toggled trigger fights light dismiss. Four things follow, all measured in Chrome 152. The layer beats a `z-index: 9999` sibling and a transformed ancestor, where a plain `position: fixed` layer at the same coordinates loses both. It **inherits** the theme, the custom properties and the direction around it, so a local `Box.Theme` finally reaches inside a dropdown and there is no `dir` to copy across. The tab order follows the markup, so a layer declared after its trigger is what Tab reaches next. And a press inside it is a press inside whatever popover it was _declared_ in, so **a `Dropdown` inside a `Popover` panel no longer dismisses the panel** — the bug a portal caused by moving the popup to the body, where every press in it read as outside. **The one cost, measured:** a layer in the top layer **keeps the side it chose when it opened** — Chrome re-evaluates `position-try-fallbacks` on scroll for an ordinary positioned element and never for a top-layer one, so a layer left open while the page scrolls does not flip when its side runs out of room and slides past the viewport edge instead. Every _open_ picks the right side, since a layer that mounts when it opens is laid out for the first time then; only a scroll _while_ open is affected, nothing but leaving and re-entering the top layer re-arms it, and the portal fallback (which measures) does not have it. It applies to `<Popover>` too. Close a layer if the page can scroll far underneath it. The trap is the other side of the same coin: **never declare a layer inside its trigger.** It used to be legal because the portal moved it before a browser saw it, and now a `role="listbox"` or a `role="menu"` inside a `<button>` is unreachable content — put it beside the trigger instead. `<Presence>` still owns the mount, so a closed dropdown renders none of its options and the exit still runs (a top-layer element transitions normally); where the browser has no Popover API the portal comes back, with everything it used to cost.
123
123
 
124
- 31. **A modal is `<Dialog>`, and the platform is the whole pattern.** `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>` (`components/dialog`) is a real `<dialog>` shown with `showModal()`, which supplies **the top layer, a `::backdrop`, an inert page behind it, Escape, focus containment and focus return** — so the focus trap, the `aria-hidden` sweep and the `z-index` strategy every other library ships are all absent here. `<AlertDialog>` (a named export of the same module) is `role="alertdialog"`, always modal and never dismissed by a press outside, because a decision that can be clicked away is one the user did not make; Escape still closes it, and `initialFocus` is what puts focus on the least destructive action, which is APG's rule for it. **Rendering a `Dialog.Title` is what names the dialog** — it writes `aria-labelledby`, and a `Dialog.Description` writes `aria-describedby`; neither attribute is written when the part is absent, so `label`/`labelledBy`/`describedBy` are the answer for a dialog that shows no heading. It takes `modal` (default `true` — `false` is `show()`: no backdrop, nothing inert, and the UA positions it against its containing block), `dismissible`, `lockScroll` (default: whatever `modal` is) and `onOpenChange(open, { reason })` with the same four reasons `<Popover>` uses, `imperative` covering a `close()` call and a `<form method="dialog">` submit. **Four things to know, all measured in Chrome 152.** The dialog is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>` — same as `<Popover>`. A **close cannot be refused**: `cancel` is cancelable but the browser has already closed the dialog by the time `onOpenChange` runs, so `dismissible={false}` is how a decision is made unavoidable. A press outside is `closedby="any"`, written for you — and where the browser has not got it yet the press is measured against the dialog's own box, because **for a modal dialog the backdrop _is_ the dialog element** and every containment test calls a press on it inside. And the platform does **not** stop the page scrolling, which is why `lockScroll` exists at all: it is an `overflow: hidden` class on `<html>`, held by a counter so an inner dialog closing does not unlock the page under an outer one.
124
+ 31. **A modal is `<Dialog>`, and the platform is the whole pattern.** `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>` (`components/dialog`) is a real `<dialog>` shown with `showModal()`, which supplies **the top layer, a `::backdrop`, an inert page behind it, Escape, focus containment and focus return** — so the focus trap, the `aria-hidden` sweep and the `z-index` strategy every other library ships are all absent here. `<AlertDialog>` (a named export of the same module) is `role="alertdialog"`, always modal and never dismissed by a press outside, because a decision that can be clicked away is one the user did not make; Escape still closes it, and `initialFocus` is what puts focus on the least destructive action, which is APG's rule for it. **Rendering a `Dialog.Title` is what names the dialog** — it writes `aria-labelledby`, and a `Dialog.Description` writes `aria-describedby`; neither attribute is written when the part is absent, so `label`/`labelledBy`/`describedBy` are the answer for a dialog that shows no heading. It takes `modal` (default `true` — `false` is `show()`: no backdrop, nothing inert, and the UA positions it against its containing block), `dismissible`, `lockScroll` (default: whatever `modal` is) and `onOpenChange(open, { reason })` with the same four reasons `<Popover>` uses, `imperative` covering a `close()` call and a `<form method="dialog">` submit. **Four things to know, all measured in Chrome 152.** The dialog is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>` — same as `<Popover>`. A **close cannot be refused**: `cancel` is cancelable but the browser has already closed the dialog by the time `onOpenChange` runs, so `dismissible={false}` is how a decision is made unavoidable. **Everything outside an open modal is inert**, a top-layer layer included: a `<Toaster>` moves its stack inside the dialog for that reason, but a `Tooltip` or `Dropdown` declared outside it and left open as it opens stays inert until it closes. A press outside is `closedby="any"`, written for you — and where the browser has not got it yet the press is measured against the dialog's own box, because **for a modal dialog the backdrop _is_ the dialog element** and every containment test calls a press on it inside. And the platform does **not** stop the page scrolling, which is why `lockScroll` exists at all: it is an `overflow: hidden` class on `<html>`, held by a counter so an inner dialog closing does not unlock the page under an outer one.
125
125
 
126
126
  32. **A menu button is `<Menu>`, and a submenu is a popover nested in it.** `<Menu trigger={(t) => <Button {...t}>Actions</Button>}><Menu.Item onSelect={duplicate}>Duplicate</Menu.Item><Menu.Separator /><Menu.Sub label="Share"><Menu.Item>Copy link</Menu.Item></Menu.Sub></Menu>` (`components/menu`) is APG's menu button on the Popover API: `role="menu"`, `menuitem`/`menuitemcheckbox`/`menuitemradio` with `aria-checked`, `role="group"` around a titled section and `role="separator"` between them. The parts are `Menu.Item`, `Menu.CheckboxItem`, `Menu.RadioGroup` + `Menu.RadioItem`, `Menu.Group` (a `label` names the group, so its items are read as a set), `Menu.Separator` and `Menu.Sub`, which renders its own item — label, chevron, `aria-haspopup="menu"`, `aria-expanded` — and the menu beside it. **The platform's half is the top layer, light dismiss and the toggle**: Escape closes the innermost menu first, one layer per press, and a press outside closes the lot, with no portal and no `z-index` anywhere. **What is written here is the keyboard and the ARIA**: Down/Up wrap, Home/End, typeahead (a letter again cycles through the items starting with it), Right opens a submenu and Left closes it — the _reading order_, so the two swap in a right-to-left menu and the chevron turns round with them. **Four things to know.** A `disabled` item is `aria-disabled` and **stays focusable**, which is APG's rule: the `disabled` attribute would take it out of the keyboard's reach and silence it. **A command closes the menu and a state does not** — `Menu.Item` closes on select, `Menu.CheckboxItem` and `Menu.RadioItem` do not, so several boxes can be ticked in one visit, and `closeOnSelect` swaps either default. `onOpenChange(open, { reason })` adds **`select`** (an item was chosen — the transition worth telling apart) and **`tab`** to the four a layer always reports. And the menu is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>`, the items render whether or not it has been opened, and the top layer's one cost applies to it as it does to `<Popover>` (rule 30): a menu keeps the side it opened on. Placement is `side`/`align`/`offset`/`flip` on both the menu and each `Menu.Sub`, whose defaults are `side="end"` and `offset={0}` so a submenu abuts the menu it came out of. Opening a submenu moves focus into it, hover included, which is what keeps the highlight and the keyboard in one place. The semantic `<menu>` element is **`MenuList`** from `components/semantics` now — `Menu` there is deprecated, because two exports of that name are one import away from the wrong component.
127
127
 
@@ -131,7 +131,7 @@ Detect the package manager from the lock file rather than asking:
131
131
 
132
132
  35. **A slider is `<Slider>`, a bar is `<Progress>`, and a number in is a number out.** `<Slider label="Volume" defaultValue={40} onValueChange={(value) => setVolume(value)} />` (`components/slider`) is APG's slider, and `defaultValue={[20, 80]}` with `thumbLabels={['Lowest', 'Highest']}` is the multi-thumb one — **the value's own shape is how many thumbs there are**, so there is no second prop saying which kind this is and no narrowing at the call site: a `number` in means a `number` back, an array means an array. A thumb may meet its neighbour and never pass it. `<Progress label="Upload" value={62} />` (`components/progress`) is the `role="progressbar"` beside it, and **it renders on a server** — no state, no effect, no measurement. **Neither is the native element, and that is the one place the platform loses**: `<input type="range">` holds a single thumb, and it and `<progress>` both draw themselves with vendor pseudo-elements (`::-webkit-slider-thumb`) that no typed prop can reach — so `name` on a `Slider` is what keeps the form working, writing one hidden input per thumb. **Four things to know.** **The position is an inline style, and it is the only one**: a thumb's offset is per _frame_ of a drag, so a class for it would be a rule per frame that is never freed — the exception `useAnchorPosition`'s anchor name and the travelling tab indicator already take, and the reason a `ProgressRing` (`components/chart`) can round its fraction into a class instead, since nobody drags a ring. **No `value` on a `Progress` is a state, not a zero**: `aria-valuenow` is omitted rather than reported as `0`, and the bar sweeps — a named duration, so it stops itself under `prefers-reduced-motion`. **A `role="slider"` has to be named**: `label` names the thumb on a single slider and the `role="group"` on a range, where `thumbLabels` names the thumbs one at a time, and `format` writes `aria-valuetext` for a value a bare number does not read as. And **it mirrors for free** — the fill and the thumbs are placed with `inset-inline-start`, so a right-to-left page draws the minimum on the right with nothing declared twice; only the sideways arrows swap (Left is the increase), and Up/Down never do, because the block axis has no reading order. `onValueChange` fires on every step and `onValueCommit` once at the end, both with `{ reason: 'pointer' | 'keyboard' }`. **A press travels and a nudge does not**: a press on the track is the one move the eye has to follow, so the thumb animates the whole way, while every other move — a drag, an arrow key, a held arrow — takes a short 60ms **`ease-out`** travel on both the thumb and the fill, which is the `tracking` variant and which both parts must carry or they come apart. Short rather than _off_, because off is exact and **steps**: a value on a grid can only be at its grid positions, so a 1-in-100 slider moves 3.2px at a time on a 320px track. Eased **out** rather than linear or eased, because a repeat restarts the transition every 33ms so only its first half is ever seen — on this curve that half is the straight part, so it glides while the key is held and still decelerates on the last transition, the only one allowed to finish; plain `ease` replays its slow-in and pulses, and `linear` rides more than a full step behind the value and stops at full speed. And an arrow key is a nudge rather than a jump, so it never takes the press travel at all — one tap spending 250ms on 3.2px was itself the lag. `step` is the real dial — `step={0}` is continuous and needs no interpolation at all. Styling is the `slider`/`slider.track`/`slider.fill`/`slider.thumb` tree and `progress`/`progress.fill`, whose `indeterminate` variant is the sweep; `disabled` is `aria-disabled` here rather than the attribute, since a div takes none and a value nobody can reach is a value nobody can read.
133
133
 
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.
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. **While a modal `Dialog` is open the stack moves inside it**, shown after it — everything outside an open modal is inert, so a toast left on the page would sit under the backdrop, unpressable and silent — and moves back out when the dialog closes; Escape on a toast there dismisses the toast, not the dialog. **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
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
 
@@ -224,7 +224,7 @@ a region inserted together with its content is not reliably announced — and an
224
224
  `role="alert"`, which is assertive; nothing else carries a region of its own, because the nearest one to a
225
225
  change is the one that speaks. It is in the **top layer with no portal**, so it inherits the theme and the
226
226
  direction around it, and it takes **no pointer events** while the toasts take them back, or a corner-sized
227
- fixed strip would swallow every press in its gaps. **Timers stop on hover, on focus and in a background
227
+ fixed strip would swallow every press in its gaps. **While a modal `Dialog` is open the stack moves inside it**, shown after it — everything outside an open modal is inert, so a toast left on the page would sit under the backdrop, unpressable and silent — and moves back out when the dialog closes; Escape on a toast there dismisses the toast, not the dialog. **Timers stop on hover, on focus and in a background
228
228
  tab** and resume where they left off (WCAG 2.2.1 — the only reason a toast may carry a control). And a
229
229
  toast **never takes focus**: `F6` moves focus to the stack, Tab walks it, and Escape dismisses the toast
230
230
  focus is in, handing focus back once the stack is empty.
@@ -116,7 +116,7 @@ themed, takes the opacity modifier and shares one class. The key names the kind
116
116
  `{ linear: 'r', colors: ['blue-500', 'pink-500'] }` (a direction `t`/`tr`/`r`/`br`/`b`/`bl`/`l`/`tl`, or a number of
117
117
  degrees — `{ linear: 135, … }`), `{ radial: 'circle' | 'ellipse' | true, at: 'top left', … }`, `{ conic: 45 | true, … }`.
118
118
  Exactly one kind; `at` centres a radial or conic gradient, never a linear one. A stop is any colour value —
119
- `'blue-500/40'`, `'transparent'`, `'var(--chart-1)'` — or a `[colour, position]` pair (`['sky-500', '20%']`), **two
119
+ `'blue-500/40'`, `'transparent'`, `'var(--chart-1)'`, a `Box.extend()` colour — or a `[colour, position]` pair (`['sky-500', '20%']`), **two
120
120
  minimum**. `interpolate` names the space: `'srgb'`, `'hsl'`, `'oklab'`, `'oklch'`, `'hsl-longer'`, `'oklch-longer'` —
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,
@@ -36,7 +36,7 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
36
36
 
37
37
  20. **An opacity is part of the colour, not a second prop**: `bgColor="blue-500/40"`, `borderColor="black/10"`, `color="currentColor/60"` — a slash and a percentage on any colour value, on every colour prop and on a `vars` entry. It compiles to `color-mix(in oklab, var(--blue-500) 40%, transparent)`, so the token stays a variable (still themed, still one shared class) — unlike `opacity`, which fades the element, its text and its children. **A token carries its own value as the fallback** — `bgColor="blue-500"` is `var(--blue-500,oklch(62.3% .214 259.8))`, and `var(--blue-500)` in these rules is that, shortened — so nothing is declared in `:root`: your own stylesheet reads a token through a variable of yours (`vars={{ brand: 'violet-500' }}` on an ancestor), while a hand-written `var(--violet-500)` inside a Box value (`fill`, `vars`, `css`, a `Box.extend()` variable) gets the fallback too. The palette itself is **Tailwind 4.3's, in OKLCH**: twenty-six families (the five neutrals, `mauve`/`mist`/`olive`/`taupe`, and seventeen hues) of eleven steps, `50`–`950`. A token the palette does not have, or a percentage outside 0–100, produces no rule and no class name; a variable from `Box.extend()` takes the modifier too
38
38
 
39
- 21. **A gradient is a value, and a shadow is a stack.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction `t`/`tr`/`r`/… or an angle in degrees, `radial` a shape, `conic` a start angle), `colors` are the stops in order, and a stop is any colour value: a token, `blue-500/40`, `var(--chart-1)`, or a `[colour, position]` pair. `at` centres a radial or conic one, `interpolate="oklch"` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. Two stops minimum, and the record is judged **whole** — one bad stop or one unknown key and the value emits nothing. It writes `background-image`, so it and `bgImage` are the same property. **Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` on Tailwind's scale, or the presets `small`/`medium`/`large`), `insetShadow` (`xxs`/`xs`/`sm`), and `ring`/`insetRing`, which are a **width in px** (`ring={2}`) that follows `borderRadius` and costs no layout, unlike `outline`. Each takes a colour of its own (`shadowColor`, `ringColor`, …) which shows nothing until the layer is painted; `none` — or `0` on a ring — clears just that layer. `textShadow` is `xxs`…`lg` with `textShadowColor`, and `transition="shadow"` covers both properties
39
+ 21. **A gradient is a value, and a shadow is a stack.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction `t`/`tr`/`r`/… or an angle in degrees, `radial` a shape, `conic` a start angle), `colors` are the stops in order, and a stop is any colour value: a token, `blue-500/40`, `var(--chart-1)`, a `Box.extend()` colour, or a `[colour, position]` pair. `at` centres a radial or conic one, `interpolate="oklch"` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. Two stops minimum, and the record is judged **whole** — one bad stop or one unknown key and the value emits nothing. It writes `background-image`, so it and `bgImage` are the same property. **Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` on Tailwind's scale, or the presets `small`/`medium`/`large`), `insetShadow` (`xxs`/`xs`/`sm`), and `ring`/`insetRing`, which are a **width in px** (`ring={2}`) that follows `borderRadius` and costs no layout, unlike `outline`. Each takes a colour of its own (`shadowColor`, `ringColor`, …) which shows nothing until the layer is painted; `none` — or `0` on a ring — clears just that layer. `textShadow` is `xxs`…`lg` with `textShadowColor`, and `transition="shadow"` covers both properties
40
40
 
41
41
  22. **Nine filters stack too, and a mask is a gradient.** `blur`, `brightness`, `contrast`, `grayscale`, `hueRotate`, `invert`, `saturate`, `sepia` and `dropShadow` each set their own layer of one composed `filter`, so a blur and a desaturation coexist. **A number is the function's own unit** — `brightness={110}` is a percentage, `hueRotate={90}` degrees, `blur={3}` pixels — and `blur` takes Tailwind's scale as well (`xs` 4px … `xxxl` 64px). `dropShadow` (`xs`…`xxl`, with `dropShadowColor`) is cast by the **shape**, where `shadow` draws a rectangle; `none` clears one function. The nine `backdrop*` props are the same functions behind the element — `backdropBlur="sm" backdropSaturate={180}` over something translucent is glassmorphism — and they and the older `backdropFilter` are the same property, so use one. **`maskImage` takes the `bgGradient` record** and reads its alpha: `maskImage={{ linear: 'b', colors: ['black', 'transparent'] }}` fades an edge, `url(#id)` masks by a shape. **`bgClip="text"` needs `color="transparent"`** beside it, and together they are how a gradient becomes type
42
42
 
@@ -56,7 +56,7 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
56
56
 
57
57
  30. **Every floating layer is in the top layer now, and none of them is portalled.** `Overlay` — and so `Tooltip`, the `Dropdown` popup and the DataGrid column menu, which are all built on it — renders where it is declared and carries `popover="manual"`, shown as soon as it mounts. `manual`, not `auto`, because a layer owns no dismissal and light dismiss is a pattern: `<Popover>` is the `auto` one (rule 29), and `Tooltip` and `Dropdown` each keep the stricter dismissal they already implement — a tooltip's Escape has to _last_ (WCAG 1.4.13), and a JS-toggled trigger fights light dismiss. Four things follow, all measured in Chrome 152. The layer beats a `z-index: 9999` sibling and a transformed ancestor, where a plain `position: fixed` layer at the same coordinates loses both. It **inherits** the theme, the custom properties and the direction around it, so a local `Box.Theme` finally reaches inside a dropdown and there is no `dir` to copy across. The tab order follows the markup, so a layer declared after its trigger is what Tab reaches next. And a press inside it is a press inside whatever popover it was _declared_ in, so **a `Dropdown` inside a `Popover` panel no longer dismisses the panel** — the bug a portal caused by moving the popup to the body, where every press in it read as outside. **The one cost, measured:** a layer in the top layer **keeps the side it chose when it opened** — Chrome re-evaluates `position-try-fallbacks` on scroll for an ordinary positioned element and never for a top-layer one, so a layer left open while the page scrolls does not flip when its side runs out of room and slides past the viewport edge instead. Every _open_ picks the right side, since a layer that mounts when it opens is laid out for the first time then; only a scroll _while_ open is affected, nothing but leaving and re-entering the top layer re-arms it, and the portal fallback (which measures) does not have it. It applies to `<Popover>` too. Close a layer if the page can scroll far underneath it. The trap is the other side of the same coin: **never declare a layer inside its trigger.** It used to be legal because the portal moved it before a browser saw it, and now a `role="listbox"` or a `role="menu"` inside a `<button>` is unreachable content — put it beside the trigger instead. `<Presence>` still owns the mount, so a closed dropdown renders none of its options and the exit still runs (a top-layer element transitions normally); where the browser has no Popover API the portal comes back, with everything it used to cost.
58
58
 
59
- 31. **A modal is `<Dialog>`, and the platform is the whole pattern.** `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>` (`components/dialog`) is a real `<dialog>` shown with `showModal()`, which supplies **the top layer, a `::backdrop`, an inert page behind it, Escape, focus containment and focus return** — so the focus trap, the `aria-hidden` sweep and the `z-index` strategy every other library ships are all absent here. `<AlertDialog>` (a named export of the same module) is `role="alertdialog"`, always modal and never dismissed by a press outside, because a decision that can be clicked away is one the user did not make; Escape still closes it, and `initialFocus` is what puts focus on the least destructive action, which is APG's rule for it. **Rendering a `Dialog.Title` is what names the dialog** — it writes `aria-labelledby`, and a `Dialog.Description` writes `aria-describedby`; neither attribute is written when the part is absent, so `label`/`labelledBy`/`describedBy` are the answer for a dialog that shows no heading. It takes `modal` (default `true` — `false` is `show()`: no backdrop, nothing inert, and the UA positions it against its containing block), `dismissible`, `lockScroll` (default: whatever `modal` is) and `onOpenChange(open, { reason })` with the same four reasons `<Popover>` uses, `imperative` covering a `close()` call and a `<form method="dialog">` submit. **Four things to know, all measured in Chrome 152.** The dialog is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>` — same as `<Popover>`. A **close cannot be refused**: `cancel` is cancelable but the browser has already closed the dialog by the time `onOpenChange` runs, so `dismissible={false}` is how a decision is made unavoidable. A press outside is `closedby="any"`, written for you — and where the browser has not got it yet the press is measured against the dialog's own box, because **for a modal dialog the backdrop _is_ the dialog element** and every containment test calls a press on it inside. And the platform does **not** stop the page scrolling, which is why `lockScroll` exists at all: it is an `overflow: hidden` class on `<html>`, held by a counter so an inner dialog closing does not unlock the page under an outer one.
59
+ 31. **A modal is `<Dialog>`, and the platform is the whole pattern.** `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>` (`components/dialog`) is a real `<dialog>` shown with `showModal()`, which supplies **the top layer, a `::backdrop`, an inert page behind it, Escape, focus containment and focus return** — so the focus trap, the `aria-hidden` sweep and the `z-index` strategy every other library ships are all absent here. `<AlertDialog>` (a named export of the same module) is `role="alertdialog"`, always modal and never dismissed by a press outside, because a decision that can be clicked away is one the user did not make; Escape still closes it, and `initialFocus` is what puts focus on the least destructive action, which is APG's rule for it. **Rendering a `Dialog.Title` is what names the dialog** — it writes `aria-labelledby`, and a `Dialog.Description` writes `aria-describedby`; neither attribute is written when the part is absent, so `label`/`labelledBy`/`describedBy` are the answer for a dialog that shows no heading. It takes `modal` (default `true` — `false` is `show()`: no backdrop, nothing inert, and the UA positions it against its containing block), `dismissible`, `lockScroll` (default: whatever `modal` is) and `onOpenChange(open, { reason })` with the same four reasons `<Popover>` uses, `imperative` covering a `close()` call and a `<form method="dialog">` submit. **Four things to know, all measured in Chrome 152.** The dialog is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>` — same as `<Popover>`. A **close cannot be refused**: `cancel` is cancelable but the browser has already closed the dialog by the time `onOpenChange` runs, so `dismissible={false}` is how a decision is made unavoidable. **Everything outside an open modal is inert**, a top-layer layer included: a `<Toaster>` moves its stack inside the dialog for that reason, but a `Tooltip` or `Dropdown` declared outside it and left open as it opens stays inert until it closes. A press outside is `closedby="any"`, written for you — and where the browser has not got it yet the press is measured against the dialog's own box, because **for a modal dialog the backdrop _is_ the dialog element** and every containment test calls a press on it inside. And the platform does **not** stop the page scrolling, which is why `lockScroll` exists at all: it is an `overflow: hidden` class on `<html>`, held by a counter so an inner dialog closing does not unlock the page under an outer one.
60
60
 
61
61
  32. **A menu button is `<Menu>`, and a submenu is a popover nested in it.** `<Menu trigger={(t) => <Button {...t}>Actions</Button>}><Menu.Item onSelect={duplicate}>Duplicate</Menu.Item><Menu.Separator /><Menu.Sub label="Share"><Menu.Item>Copy link</Menu.Item></Menu.Sub></Menu>` (`components/menu`) is APG's menu button on the Popover API: `role="menu"`, `menuitem`/`menuitemcheckbox`/`menuitemradio` with `aria-checked`, `role="group"` around a titled section and `role="separator"` between them. The parts are `Menu.Item`, `Menu.CheckboxItem`, `Menu.RadioGroup` + `Menu.RadioItem`, `Menu.Group` (a `label` names the group, so its items are read as a set), `Menu.Separator` and `Menu.Sub`, which renders its own item — label, chevron, `aria-haspopup="menu"`, `aria-expanded` — and the menu beside it. **The platform's half is the top layer, light dismiss and the toggle**: Escape closes the innermost menu first, one layer per press, and a press outside closes the lot, with no portal and no `z-index` anywhere. **What is written here is the keyboard and the ARIA**: Down/Up wrap, Home/End, typeahead (a letter again cycles through the items starting with it), Right opens a submenu and Left closes it — the _reading order_, so the two swap in a right-to-left menu and the chevron turns round with them. **Four things to know.** A `disabled` item is `aria-disabled` and **stays focusable**, which is APG's rule: the `disabled` attribute would take it out of the keyboard's reach and silence it. **A command closes the menu and a state does not** — `Menu.Item` closes on select, `Menu.CheckboxItem` and `Menu.RadioItem` do not, so several boxes can be ticked in one visit, and `closeOnSelect` swaps either default. `onOpenChange(open, { reason })` adds **`select`** (an item was chosen — the transition worth telling apart) and **`tab`** to the four a layer always reports. And the menu is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>`, the items render whether or not it has been opened, and the top layer's one cost applies to it as it does to `<Popover>` (rule 30): a menu keeps the side it opened on. Placement is `side`/`align`/`offset`/`flip` on both the menu and each `Menu.Sub`, whose defaults are `side="end"` and `offset={0}` so a submenu abuts the menu it came out of. Opening a submenu moves focus into it, hover included, which is what keeps the highlight and the keyboard in one place. The semantic `<menu>` element is **`MenuList`** from `components/semantics` now — `Menu` there is deprecated, because two exports of that name are one import away from the wrong component.
62
62
 
@@ -66,7 +66,7 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
66
66
 
67
67
  35. **A slider is `<Slider>`, a bar is `<Progress>`, and a number in is a number out.** `<Slider label="Volume" defaultValue={40} onValueChange={(value) => setVolume(value)} />` (`components/slider`) is APG's slider, and `defaultValue={[20, 80]}` with `thumbLabels={['Lowest', 'Highest']}` is the multi-thumb one — **the value's own shape is how many thumbs there are**, so there is no second prop saying which kind this is and no narrowing at the call site: a `number` in means a `number` back, an array means an array. A thumb may meet its neighbour and never pass it. `<Progress label="Upload" value={62} />` (`components/progress`) is the `role="progressbar"` beside it, and **it renders on a server** — no state, no effect, no measurement. **Neither is the native element, and that is the one place the platform loses**: `<input type="range">` holds a single thumb, and it and `<progress>` both draw themselves with vendor pseudo-elements (`::-webkit-slider-thumb`) that no typed prop can reach — so `name` on a `Slider` is what keeps the form working, writing one hidden input per thumb. **Four things to know.** **The position is an inline style, and it is the only one**: a thumb's offset is per _frame_ of a drag, so a class for it would be a rule per frame that is never freed — the exception `useAnchorPosition`'s anchor name and the travelling tab indicator already take, and the reason a `ProgressRing` (`components/chart`) can round its fraction into a class instead, since nobody drags a ring. **No `value` on a `Progress` is a state, not a zero**: `aria-valuenow` is omitted rather than reported as `0`, and the bar sweeps — a named duration, so it stops itself under `prefers-reduced-motion`. **A `role="slider"` has to be named**: `label` names the thumb on a single slider and the `role="group"` on a range, where `thumbLabels` names the thumbs one at a time, and `format` writes `aria-valuetext` for a value a bare number does not read as. And **it mirrors for free** — the fill and the thumbs are placed with `inset-inline-start`, so a right-to-left page draws the minimum on the right with nothing declared twice; only the sideways arrows swap (Left is the increase), and Up/Down never do, because the block axis has no reading order. `onValueChange` fires on every step and `onValueCommit` once at the end, both with `{ reason: 'pointer' | 'keyboard' }`. **A press travels and a nudge does not**: a press on the track is the one move the eye has to follow, so the thumb animates the whole way, while every other move — a drag, an arrow key, a held arrow — takes a short 60ms **`ease-out`** travel on both the thumb and the fill, which is the `tracking` variant and which both parts must carry or they come apart. Short rather than _off_, because off is exact and **steps**: a value on a grid can only be at its grid positions, so a 1-in-100 slider moves 3.2px at a time on a 320px track. Eased **out** rather than linear or eased, because a repeat restarts the transition every 33ms so only its first half is ever seen — on this curve that half is the straight part, so it glides while the key is held and still decelerates on the last transition, the only one allowed to finish; plain `ease` replays its slow-in and pulses, and `linear` rides more than a full step behind the value and stops at full speed. And an arrow key is a nudge rather than a jump, so it never takes the press travel at all — one tap spending 250ms on 3.2px was itself the lag. `step` is the real dial — `step={0}` is continuous and needs no interpolation at all. Styling is the `slider`/`slider.track`/`slider.fill`/`slider.thumb` tree and `progress`/`progress.fill`, whose `indeterminate` variant is the sweep; `disabled` is `aria-disabled` here rather than the attribute, since a div takes none and a value nobody can reach is a value nobody can read.
68
68
 
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.
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. **While a modal `Dialog` is open the stack moves inside it**, shown after it — everything outside an open modal is inert, so a toast left on the page would sit under the backdrop, unpressable and silent — and moves back out when the dialog closes; Escape on a toast there dismisses the toast, not the dialog. **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
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
 
package/AGENTS.md CHANGED
@@ -68,7 +68,7 @@ Every value there is a prop, and every prop value becomes one class the engine i
68
68
 
69
69
  20. **An opacity is part of the colour, not a second prop**: `bgColor="blue-500/40"`, `borderColor="black/10"`, `color="currentColor/60"` — a slash and a percentage on any colour value, on every colour prop and on a `vars` entry. It compiles to `color-mix(in oklab, var(--blue-500) 40%, transparent)`, so the token stays a variable (still themed, still one shared class) — unlike `opacity`, which fades the element, its text and its children. **A token carries its own value as the fallback** — `bgColor="blue-500"` is `var(--blue-500,oklch(62.3% .214 259.8))`, and `var(--blue-500)` in these rules is that, shortened — so nothing is declared in `:root`: your own stylesheet reads a token through a variable of yours (`vars={{ brand: 'violet-500' }}` on an ancestor), while a hand-written `var(--violet-500)` inside a Box value (`fill`, `vars`, `css`, a `Box.extend()` variable) gets the fallback too. The palette itself is **Tailwind 4.3's, in OKLCH**: twenty-six families (the five neutrals, `mauve`/`mist`/`olive`/`taupe`, and seventeen hues) of eleven steps, `50`–`950`. A token the palette does not have, or a percentage outside 0–100, produces no rule and no class name; a variable from `Box.extend()` takes the modifier too
70
70
 
71
- 21. **A gradient is a value, and a shadow is a stack.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction `t`/`tr`/`r`/… or an angle in degrees, `radial` a shape, `conic` a start angle), `colors` are the stops in order, and a stop is any colour value: a token, `blue-500/40`, `var(--chart-1)`, or a `[colour, position]` pair. `at` centres a radial or conic one, `interpolate="oklch"` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. Two stops minimum, and the record is judged **whole** — one bad stop or one unknown key and the value emits nothing. It writes `background-image`, so it and `bgImage` are the same property. **Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` on Tailwind's scale, or the presets `small`/`medium`/`large`), `insetShadow` (`xxs`/`xs`/`sm`), and `ring`/`insetRing`, which are a **width in px** (`ring={2}`) that follows `borderRadius` and costs no layout, unlike `outline`. Each takes a colour of its own (`shadowColor`, `ringColor`, …) which shows nothing until the layer is painted; `none` — or `0` on a ring — clears just that layer. `textShadow` is `xxs`…`lg` with `textShadowColor`, and `transition="shadow"` covers both properties
71
+ 21. **A gradient is a value, and a shadow is a stack.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction `t`/`tr`/`r`/… or an angle in degrees, `radial` a shape, `conic` a start angle), `colors` are the stops in order, and a stop is any colour value: a token, `blue-500/40`, `var(--chart-1)`, a `Box.extend()` colour, or a `[colour, position]` pair. `at` centres a radial or conic one, `interpolate="oklch"` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. Two stops minimum, and the record is judged **whole** — one bad stop or one unknown key and the value emits nothing. It writes `background-image`, so it and `bgImage` are the same property. **Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` on Tailwind's scale, or the presets `small`/`medium`/`large`), `insetShadow` (`xxs`/`xs`/`sm`), and `ring`/`insetRing`, which are a **width in px** (`ring={2}`) that follows `borderRadius` and costs no layout, unlike `outline`. Each takes a colour of its own (`shadowColor`, `ringColor`, …) which shows nothing until the layer is painted; `none` — or `0` on a ring — clears just that layer. `textShadow` is `xxs`…`lg` with `textShadowColor`, and `transition="shadow"` covers both properties
72
72
 
73
73
  22. **Nine filters stack too, and a mask is a gradient.** `blur`, `brightness`, `contrast`, `grayscale`, `hueRotate`, `invert`, `saturate`, `sepia` and `dropShadow` each set their own layer of one composed `filter`, so a blur and a desaturation coexist. **A number is the function's own unit** — `brightness={110}` is a percentage, `hueRotate={90}` degrees, `blur={3}` pixels — and `blur` takes Tailwind's scale as well (`xs` 4px … `xxxl` 64px). `dropShadow` (`xs`…`xxl`, with `dropShadowColor`) is cast by the **shape**, where `shadow` draws a rectangle; `none` clears one function. The nine `backdrop*` props are the same functions behind the element — `backdropBlur="sm" backdropSaturate={180}` over something translucent is glassmorphism — and they and the older `backdropFilter` are the same property, so use one. **`maskImage` takes the `bgGradient` record** and reads its alpha: `maskImage={{ linear: 'b', colors: ['black', 'transparent'] }}` fades an edge, `url(#id)` masks by a shape. **`bgClip="text"` needs `color="transparent"`** beside it, and together they are how a gradient becomes type
74
74
 
@@ -88,7 +88,7 @@ Every value there is a prop, and every prop value becomes one class the engine i
88
88
 
89
89
  30. **Every floating layer is in the top layer now, and none of them is portalled.** `Overlay` — and so `Tooltip`, the `Dropdown` popup and the DataGrid column menu, which are all built on it — renders where it is declared and carries `popover="manual"`, shown as soon as it mounts. `manual`, not `auto`, because a layer owns no dismissal and light dismiss is a pattern: `<Popover>` is the `auto` one (rule 29), and `Tooltip` and `Dropdown` each keep the stricter dismissal they already implement — a tooltip's Escape has to _last_ (WCAG 1.4.13), and a JS-toggled trigger fights light dismiss. Four things follow, all measured in Chrome 152. The layer beats a `z-index: 9999` sibling and a transformed ancestor, where a plain `position: fixed` layer at the same coordinates loses both. It **inherits** the theme, the custom properties and the direction around it, so a local `Box.Theme` finally reaches inside a dropdown and there is no `dir` to copy across. The tab order follows the markup, so a layer declared after its trigger is what Tab reaches next. And a press inside it is a press inside whatever popover it was _declared_ in, so **a `Dropdown` inside a `Popover` panel no longer dismisses the panel** — the bug a portal caused by moving the popup to the body, where every press in it read as outside. **The one cost, measured:** a layer in the top layer **keeps the side it chose when it opened** — Chrome re-evaluates `position-try-fallbacks` on scroll for an ordinary positioned element and never for a top-layer one, so a layer left open while the page scrolls does not flip when its side runs out of room and slides past the viewport edge instead. Every _open_ picks the right side, since a layer that mounts when it opens is laid out for the first time then; only a scroll _while_ open is affected, nothing but leaving and re-entering the top layer re-arms it, and the portal fallback (which measures) does not have it. It applies to `<Popover>` too. Close a layer if the page can scroll far underneath it. The trap is the other side of the same coin: **never declare a layer inside its trigger.** It used to be legal because the portal moved it before a browser saw it, and now a `role="listbox"` or a `role="menu"` inside a `<button>` is unreachable content — put it beside the trigger instead. `<Presence>` still owns the mount, so a closed dropdown renders none of its options and the exit still runs (a top-layer element transitions normally); where the browser has no Popover API the portal comes back, with everything it used to cost.
90
90
 
91
- 31. **A modal is `<Dialog>`, and the platform is the whole pattern.** `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>` (`components/dialog`) is a real `<dialog>` shown with `showModal()`, which supplies **the top layer, a `::backdrop`, an inert page behind it, Escape, focus containment and focus return** — so the focus trap, the `aria-hidden` sweep and the `z-index` strategy every other library ships are all absent here. `<AlertDialog>` (a named export of the same module) is `role="alertdialog"`, always modal and never dismissed by a press outside, because a decision that can be clicked away is one the user did not make; Escape still closes it, and `initialFocus` is what puts focus on the least destructive action, which is APG's rule for it. **Rendering a `Dialog.Title` is what names the dialog** — it writes `aria-labelledby`, and a `Dialog.Description` writes `aria-describedby`; neither attribute is written when the part is absent, so `label`/`labelledBy`/`describedBy` are the answer for a dialog that shows no heading. It takes `modal` (default `true` — `false` is `show()`: no backdrop, nothing inert, and the UA positions it against its containing block), `dismissible`, `lockScroll` (default: whatever `modal` is) and `onOpenChange(open, { reason })` with the same four reasons `<Popover>` uses, `imperative` covering a `close()` call and a `<form method="dialog">` submit. **Four things to know, all measured in Chrome 152.** The dialog is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>` — same as `<Popover>`. A **close cannot be refused**: `cancel` is cancelable but the browser has already closed the dialog by the time `onOpenChange` runs, so `dismissible={false}` is how a decision is made unavoidable. A press outside is `closedby="any"`, written for you — and where the browser has not got it yet the press is measured against the dialog's own box, because **for a modal dialog the backdrop _is_ the dialog element** and every containment test calls a press on it inside. And the platform does **not** stop the page scrolling, which is why `lockScroll` exists at all: it is an `overflow: hidden` class on `<html>`, held by a counter so an inner dialog closing does not unlock the page under an outer one.
91
+ 31. **A modal is `<Dialog>`, and the platform is the whole pattern.** `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>` (`components/dialog`) is a real `<dialog>` shown with `showModal()`, which supplies **the top layer, a `::backdrop`, an inert page behind it, Escape, focus containment and focus return** — so the focus trap, the `aria-hidden` sweep and the `z-index` strategy every other library ships are all absent here. `<AlertDialog>` (a named export of the same module) is `role="alertdialog"`, always modal and never dismissed by a press outside, because a decision that can be clicked away is one the user did not make; Escape still closes it, and `initialFocus` is what puts focus on the least destructive action, which is APG's rule for it. **Rendering a `Dialog.Title` is what names the dialog** — it writes `aria-labelledby`, and a `Dialog.Description` writes `aria-describedby`; neither attribute is written when the part is absent, so `label`/`labelledBy`/`describedBy` are the answer for a dialog that shows no heading. It takes `modal` (default `true` — `false` is `show()`: no backdrop, nothing inert, and the UA positions it against its containing block), `dismissible`, `lockScroll` (default: whatever `modal` is) and `onOpenChange(open, { reason })` with the same four reasons `<Popover>` uses, `imperative` covering a `close()` call and a `<form method="dialog">` submit. **Four things to know, all measured in Chrome 152.** The dialog is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>` — same as `<Popover>`. A **close cannot be refused**: `cancel` is cancelable but the browser has already closed the dialog by the time `onOpenChange` runs, so `dismissible={false}` is how a decision is made unavoidable. **Everything outside an open modal is inert**, a top-layer layer included: a `<Toaster>` moves its stack inside the dialog for that reason, but a `Tooltip` or `Dropdown` declared outside it and left open as it opens stays inert until it closes. A press outside is `closedby="any"`, written for you — and where the browser has not got it yet the press is measured against the dialog's own box, because **for a modal dialog the backdrop _is_ the dialog element** and every containment test calls a press on it inside. And the platform does **not** stop the page scrolling, which is why `lockScroll` exists at all: it is an `overflow: hidden` class on `<html>`, held by a counter so an inner dialog closing does not unlock the page under an outer one.
92
92
 
93
93
  32. **A menu button is `<Menu>`, and a submenu is a popover nested in it.** `<Menu trigger={(t) => <Button {...t}>Actions</Button>}><Menu.Item onSelect={duplicate}>Duplicate</Menu.Item><Menu.Separator /><Menu.Sub label="Share"><Menu.Item>Copy link</Menu.Item></Menu.Sub></Menu>` (`components/menu`) is APG's menu button on the Popover API: `role="menu"`, `menuitem`/`menuitemcheckbox`/`menuitemradio` with `aria-checked`, `role="group"` around a titled section and `role="separator"` between them. The parts are `Menu.Item`, `Menu.CheckboxItem`, `Menu.RadioGroup` + `Menu.RadioItem`, `Menu.Group` (a `label` names the group, so its items are read as a set), `Menu.Separator` and `Menu.Sub`, which renders its own item — label, chevron, `aria-haspopup="menu"`, `aria-expanded` — and the menu beside it. **The platform's half is the top layer, light dismiss and the toggle**: Escape closes the innermost menu first, one layer per press, and a press outside closes the lot, with no portal and no `z-index` anywhere. **What is written here is the keyboard and the ARIA**: Down/Up wrap, Home/End, typeahead (a letter again cycles through the items starting with it), Right opens a submenu and Left closes it — the _reading order_, so the two swap in a right-to-left menu and the chevron turns round with them. **Four things to know.** A `disabled` item is `aria-disabled` and **stays focusable**, which is APG's rule: the `disabled` attribute would take it out of the keyboard's reach and silence it. **A command closes the menu and a state does not** — `Menu.Item` closes on select, `Menu.CheckboxItem` and `Menu.RadioItem` do not, so several boxes can be ticked in one visit, and `closeOnSelect` swaps either default. `onOpenChange(open, { reason })` adds **`select`** (an item was chosen — the transition worth telling apart) and **`tab`** to the four a layer always reports. And the menu is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>`, the items render whether or not it has been opened, and the top layer's one cost applies to it as it does to `<Popover>` (rule 30): a menu keeps the side it opened on. Placement is `side`/`align`/`offset`/`flip` on both the menu and each `Menu.Sub`, whose defaults are `side="end"` and `offset={0}` so a submenu abuts the menu it came out of. Opening a submenu moves focus into it, hover included, which is what keeps the highlight and the keyboard in one place. The semantic `<menu>` element is **`MenuList`** from `components/semantics` now — `Menu` there is deprecated, because two exports of that name are one import away from the wrong component.
94
94
 
@@ -98,7 +98,7 @@ Every value there is a prop, and every prop value becomes one class the engine i
98
98
 
99
99
  35. **A slider is `<Slider>`, a bar is `<Progress>`, and a number in is a number out.** `<Slider label="Volume" defaultValue={40} onValueChange={(value) => setVolume(value)} />` (`components/slider`) is APG's slider, and `defaultValue={[20, 80]}` with `thumbLabels={['Lowest', 'Highest']}` is the multi-thumb one — **the value's own shape is how many thumbs there are**, so there is no second prop saying which kind this is and no narrowing at the call site: a `number` in means a `number` back, an array means an array. A thumb may meet its neighbour and never pass it. `<Progress label="Upload" value={62} />` (`components/progress`) is the `role="progressbar"` beside it, and **it renders on a server** — no state, no effect, no measurement. **Neither is the native element, and that is the one place the platform loses**: `<input type="range">` holds a single thumb, and it and `<progress>` both draw themselves with vendor pseudo-elements (`::-webkit-slider-thumb`) that no typed prop can reach — so `name` on a `Slider` is what keeps the form working, writing one hidden input per thumb. **Four things to know.** **The position is an inline style, and it is the only one**: a thumb's offset is per _frame_ of a drag, so a class for it would be a rule per frame that is never freed — the exception `useAnchorPosition`'s anchor name and the travelling tab indicator already take, and the reason a `ProgressRing` (`components/chart`) can round its fraction into a class instead, since nobody drags a ring. **No `value` on a `Progress` is a state, not a zero**: `aria-valuenow` is omitted rather than reported as `0`, and the bar sweeps — a named duration, so it stops itself under `prefers-reduced-motion`. **A `role="slider"` has to be named**: `label` names the thumb on a single slider and the `role="group"` on a range, where `thumbLabels` names the thumbs one at a time, and `format` writes `aria-valuetext` for a value a bare number does not read as. And **it mirrors for free** — the fill and the thumbs are placed with `inset-inline-start`, so a right-to-left page draws the minimum on the right with nothing declared twice; only the sideways arrows swap (Left is the increase), and Up/Down never do, because the block axis has no reading order. `onValueChange` fires on every step and `onValueCommit` once at the end, both with `{ reason: 'pointer' | 'keyboard' }`. **A press travels and a nudge does not**: a press on the track is the one move the eye has to follow, so the thumb animates the whole way, while every other move — a drag, an arrow key, a held arrow — takes a short 60ms **`ease-out`** travel on both the thumb and the fill, which is the `tracking` variant and which both parts must carry or they come apart. Short rather than _off_, because off is exact and **steps**: a value on a grid can only be at its grid positions, so a 1-in-100 slider moves 3.2px at a time on a 320px track. Eased **out** rather than linear or eased, because a repeat restarts the transition every 33ms so only its first half is ever seen — on this curve that half is the straight part, so it glides while the key is held and still decelerates on the last transition, the only one allowed to finish; plain `ease` replays its slow-in and pulses, and `linear` rides more than a full step behind the value and stops at full speed. And an arrow key is a nudge rather than a jump, so it never takes the press travel at all — one tap spending 250ms on 3.2px was itself the lag. `step` is the real dial — `step={0}` is continuous and needs no interpolation at all. Styling is the `slider`/`slider.track`/`slider.fill`/`slider.thumb` tree and `progress`/`progress.fill`, whose `indeterminate` variant is the sweep; `disabled` is `aria-disabled` here rather than the attribute, since a div takes none and a value nobody can reach is a value nobody can read.
100
100
 
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.
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. **While a modal `Dialog` is open the stack moves inside it**, shown after it — everything outside an open modal is inert, so a toast left on the page would sit under the backdrop, unpressable and silent — and moves back out when the dialog closes; Escape on a toast there dismisses the toast, not the dialog. **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
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
 
package/BOX_AI_CONTEXT.md CHANGED
@@ -340,7 +340,7 @@ modifier, it is themed, and two elements asking for the same one share a class.
340
340
  `'circle'`/`'ellipse'`/`true`; `conic` takes a start angle or `true`. Exactly one of the three.
341
341
  - `at` centres a `radial` or `conic` gradient (not a `linear` one, which runs in a direction).
342
342
  - `interpolate`: `'srgb'`, `'hsl'`, `'oklab'`, `'oklch'`, `'hsl-longer'`, `'oklch-longer'`.
343
- - A stop is any colour value — a token, `blue-500/40`, a system colour, `var(--chart-1)` — or a
343
+ - A stop is any colour value — a token, `blue-500/40`, a system colour, `var(--chart-1)`, a `Box.extend()` colour — or a
344
344
  `[colour, position]` pair. **Two stops minimum**, and the record is judged whole: one bad stop, one
345
345
  unknown key, two kinds at once, and the whole value emits no rule and no class name.
346
346
  - It writes `background-image`, so `bgGradient` and `bgImage` are the same property. `bgGradient` is declared
@@ -2398,7 +2398,7 @@ that was never on screen.
2398
2398
  declared — it inherits the theme, the custom properties and the direction around it. `manual` rather
2399
2399
  than `auto` because a stack of messages owns no dismissal: a press outside has to reach the page. The
2400
2400
  viewport takes **no pointer events at all** and the toasts take them back, or a corner-sized fixed
2401
- strip would swallow every press in its gaps.
2401
+ strip would swallow every press in its gaps. **While a modal `Dialog` is open the stack moves inside it**, shown after it — everything outside an open modal is inert, so a toast left on the page would sit under the backdrop, unpressable and silent — and moves back out when the dialog closes; Escape on a toast there dismisses the toast, not the dialog.
2402
2402
  3. **Four things stop the clock**: the pointer over the stack, focus inside it, a background tab, and a
2403
2403
  dismissal. A paused timer resumes where it left off rather than restarting. WCAG 2.2.1, and the only
2404
2404
  reason a toast is allowed to carry a control at all.
@@ -340,7 +340,7 @@ modifier, it is themed, and two elements asking for the same one share a class.
340
340
  `'circle'`/`'ellipse'`/`true`; `conic` takes a start angle or `true`. Exactly one of the three.
341
341
  - `at` centres a `radial` or `conic` gradient (not a `linear` one, which runs in a direction).
342
342
  - `interpolate`: `'srgb'`, `'hsl'`, `'oklab'`, `'oklch'`, `'hsl-longer'`, `'oklch-longer'`.
343
- - A stop is any colour value — a token, `blue-500/40`, a system colour, `var(--chart-1)` — or a
343
+ - A stop is any colour value — a token, `blue-500/40`, a system colour, `var(--chart-1)`, a `Box.extend()` colour — or a
344
344
  `[colour, position]` pair. **Two stops minimum**, and the record is judged whole: one bad stop, one
345
345
  unknown key, two kinds at once, and the whole value emits no rule and no class name.
346
346
  - It writes `background-image`, so `bgGradient` and `bgImage` are the same property. `bgGradient` is declared
@@ -2398,7 +2398,7 @@ that was never on screen.
2398
2398
  declared — it inherits the theme, the custom properties and the direction around it. `manual` rather
2399
2399
  than `auto` because a stack of messages owns no dismissal: a press outside has to reach the page. The
2400
2400
  viewport takes **no pointer events at all** and the toasts take them back, or a corner-sized fixed
2401
- strip would swallow every press in its gaps.
2401
+ strip would swallow every press in its gaps. **While a modal `Dialog` is open the stack moves inside it**, shown after it — everything outside an open modal is inert, so a toast left on the page would sit under the backdrop, unpressable and silent — and moves back out when the dialog closes; Escape on a toast there dismisses the toast, not the dialog.
2402
2402
  3. **Four things stop the clock**: the pointer over the stack, focus inside it, a background tab, and a
2403
2403
  dismissal. A paused timer resumes where it left off rather than restarting. WCAG 2.2.1, and the only
2404
2404
  reason a toast is allowed to carry a control at all.
package/behavior.cjs CHANGED
@@ -1 +1 @@
1
- const e=require("./platform.cjs"),t=require("./effects.cjs"),n=require("./keyboard.cjs");let r=require("react");function i(e){let n=(0,r.useRef)(e);return t.n(()=>{n.current=e}),n}function a(e){let t=i(e);return(0,r.useCallback)((...e)=>t.current?.(...e),[t])}function o(e){let{value:t,defaultValue:n,onChange:o}=e,s=t!==void 0,[c,l]=(0,r.useState)(n),u=s?t:c,d=i(u),f=a(o);return[u,(0,r.useCallback)((e,t)=>{let n=typeof e==`function`?e(d.current):e;Object.is(n,d.current)||(s||(d.current=n),l(n),f(n,t))},[f,d,s])]}var s=[];function c(e,t){let n=t.elements();return e.elements().some(e=>n.some(t=>e!==t&&e.contains(t)))}function l(){let e=s.filter(e=>!s.some(t=>t!==e&&c(e,t)));return e[e.length-1]}function u(n){let{enabled:r=!0,onDismiss:o,inside:c,escapeKey:u=!0,outsidePointer:d=!0}=n,f=i(c),p=a(o);t.n(()=>{if(!r)return;let t={elements:()=>(f.current??[]).map(t=>e.d(t)).filter(e=>e!==null)};s.push(t);let n=new AbortController,i=(e,t)=>document.addEventListener(e,t,{signal:n.signal});return u&&i(`keydown`,e=>{e.key!==`Escape`||e.defaultPrevented||l()===t&&p(`escape`,e)}),d&&i(`pointerdown`,t=>{e.p(t,f.current??[])||p(`outside-pointer`,t)}),()=>{n.abort();let e=s.indexOf(t);e!==-1&&s.splice(e,1)}},[r,u,d,p,f])}function d(){let t=e.t()?.activeElement;return t&&t!==document.body?t:null}function f(t={}){let{enabled:n=!0,returnTo:i,preventScroll:o}=t,[s,c]=(0,r.useState)(null),[l,u]=(0,r.useState)(!1);n!==l&&(u(n),n&&c(d()));let f=a(()=>{(e.f(i)??s)?.focus({preventScroll:o})});return(0,r.useEffect)(()=>{if(n)return()=>{let e=document.activeElement;(!e||e===document.body||!document.contains(e))&&f()}},[n,f]),{returnFocus:f}}var p=10;function m(e,t){return t<0?-1:Math.max(0,Math.min(e,t))}function h(e,t,n,r,i){if(n===0)return-1;let a=e;for(let e=0;e<n;e++){if(a+=t,a<0||a>=n){if(!r)return-1;a=(a%n+n)%n}if(!i?.(a))return a}return-1}function g(e,t,n){return h(e===1?-1:t,e,t,!1,n)}function _(t){let{count:a,columns:s,columnIndexOf:c,pageSize:l=p,defaultActiveColumn:u=0,onActiveCellChange:d,orientation:f=`vertical`,loop:_=!0,activeIndex:v,defaultActiveIndex:y=0,onActiveIndexChange:b,isDisabled:x,textOf:S,focusItems:C=!0,onSelect:w}=t,[T,E]=o({value:v,defaultValue:y,onChange:b}),D=Math.min(T,a-1),O=s!==void 0,k=(0,r.useCallback)(e=>typeof s==`function`?s(e):s??0,[s]),A=(0,r.useCallback)((e,t)=>{let n=k(e);if(!c)return m(t,n-1);if(n<=0)return-1;let r=0;for(;r+1<n&&c(e,r+1)<=t;)r++;return r},[c,k]),[j,M]=(0,r.useState)(u),N=O&&D>=0?A(D,j):-1,P=(0,r.useRef)([]),F=(0,r.useRef)(new Map),I=(0,r.useRef)(new Map),L=(0,r.useRef)(new Map),R=(0,r.useRef)({query:``,at:0}),z=(0,r.useCallback)(e=>{P.current[e]?.focus()},[]),B=(0,r.useRef)(!1),V=(0,r.useCallback)((e,t)=>{B.current=!0,I.current.get(`${e}:${t}`)?.focus(),B.current=!1},[]),H=(0,r.useCallback)(()=>O?I.current.get(`${D}:${N}`)??null:P.current[D]??null,[N,D,O]),U=i({row:D,column:N}),W=i(j),G=i(d),K=i(k),q=i(A),J=i(c),Y=(0,r.useCallback)((e,t,n)=>{let r=m(e,a-1),i=Math.max(0,t),o=r<0?-1:q.current(r,i),s=U.current;return r===s.row&&o===s.column?s:(E(r,n),M(i),G.current?.({row:r,column:o},n),{row:r,column:o})},[U,q,G,a,E]),X=(0,r.useCallback)((e,t,n)=>{let r=m(e,a-1),i=r<0?-1:m(Math.max(0,t),K.current(r)-1);return Y(r,i<0?0:J.current?.(r,i)??i,n)},[J,a,Y,K]),Z=(0,r.useCallback)((e,t)=>{let n=`${e}:${t}`;return L.current.has(n)||L.current.set(n,e=>{e?I.current.set(n,e):(I.current.delete(n),L.current.delete(n))}),{ref:L.current.get(n),tabIndex:e===D&&t===N?0:-1,onFocus:()=>{B.current||X(e,t,{reason:`focus`})}}},[N,D,X]),Q=(0,r.useCallback)(e=>(F.current.has(e)||F.current.set(e,t=>{P.current[e]=t}),{ref:F.current.get(e),tabIndex:C?e===D?0:-1:void 0,onFocus:C?()=>E(e,{reason:`focus`}):void 0}),[D,C,E]),$=(0,r.useCallback)(t=>{let{row:n,column:r}=U.current,i=(e,n)=>{t.preventDefault();let r=X(e,n,{reason:`keyboard`,event:t});C&&V(r.row,r.column)},o=e=>{t.preventDefault();let n=Y(e,W.current,{reason:`keyboard`,event:t});C&&V(n.row,n.column)},s=2**53-1,c=()=>e.m(t.currentTarget)?-1:1;switch(t.key){case`ArrowRight`:return i(n,r+c());case`ArrowLeft`:return i(n,r-c());case`ArrowDown`:return o(n+1);case`ArrowUp`:return o(n-1);case`Home`:return t.ctrlKey||t.metaKey?i(0,0):i(n,0);case`End`:return t.ctrlKey||t.metaKey?i(a-1,s):i(n,s);case`PageDown`:return o(n+l);case`PageUp`:return o(n-l)}},[U,a,W,V,C,l,Y,X]);return{activeIndex:D,activeColumn:N,setActiveIndex:E,setActiveCell:X,onKeyDown:(0,r.useCallback)(t=>{if(t.defaultPrevented)return;if(O){$(t);return}let r=f!==`horizontal`,i=f!==`vertical`,o=R.current,s=n.t.isOpen(o,t.timeStamp),c=(e,n)=>{e!==-1&&e!==D&&(t.preventDefault(),E(e,{reason:n,event:t}),C&&z(e))};if(i&&(t.key===`ArrowRight`||t.key===`ArrowLeft`)){let n=t.key===`ArrowRight`!==e.m(t.currentTarget);c(h(D,n?1:-1,a,_,x),`keyboard`);return}if(t.key===`ArrowDown`&&r){c(h(D,1,a,_,x),`keyboard`);return}if(t.key===`ArrowUp`&&r){c(h(D,-1,a,_,x),`keyboard`);return}if(t.key===`Home`){c(g(1,a,x),`keyboard`);return}if(t.key===`End`){c(g(-1,a,x),`keyboard`);return}if(t.key===`Enter`||t.key===` `&&!(s&&S)){if(!w||D===-1)return;t.preventDefault(),w(D,t);return}if(S&&n.t.isPrintable(t)){let e=n.t.push(o,t.key,t.timeStamp);c(n.t.target(e,D,a,S,x),`typeahead`)}},[D,a,z,C,x,O,_,$,w,f,E,S]),itemProps:Q,cellProps:Z,activeItem:H}}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return _}});
1
+ const e=require("./platform.cjs"),t=require("./effects.cjs"),n=require("./keyboard.cjs");let r=require("react");function i(e){let n=(0,r.useRef)(e);return t.n(()=>{n.current=e}),n}function a(e){let t=i(e);return(0,r.useCallback)((...e)=>t.current?.(...e),[t])}function o(e){let{value:t,defaultValue:n,onChange:o}=e,s=t!==void 0,[c,l]=(0,r.useState)(n),u=s?t:c,d=i(u),f=a(o);return[u,(0,r.useCallback)((e,t)=>{let n=typeof e==`function`?e(d.current):e;Object.is(n,d.current)||(s||(d.current=n),l(n),f(n,t))},[f,d,s])]}var s=[];function c(e,t){let n=t.elements();return e.elements().some(e=>n.some(t=>e!==t&&e.contains(t)))}function l(){let e=s.filter(e=>!s.some(t=>t!==e&&c(e,t)));return e[e.length-1]}function u(n){let{enabled:r=!0,onDismiss:o,inside:c,escapeKey:u=!0,outsidePointer:d=!0}=n,f=i(c),p=a(o);t.n(()=>{if(!r)return;let t={elements:()=>(f.current??[]).map(t=>e.p(t)).filter(e=>e!==null)};s.push(t);let n=new AbortController,i=(e,t)=>document.addEventListener(e,t,{signal:n.signal});return u&&i(`keydown`,e=>{e.key!==`Escape`||e.defaultPrevented||l()===t&&p(`escape`,e)}),d&&i(`pointerdown`,t=>{e.h(t,f.current??[])||p(`outside-pointer`,t)}),()=>{n.abort();let e=s.indexOf(t);e!==-1&&s.splice(e,1)}},[r,u,d,p,f])}function d(){let t=e.t()?.activeElement;return t&&t!==document.body?t:null}function f(t={}){let{enabled:n=!0,returnTo:i,preventScroll:o}=t,[s,c]=(0,r.useState)(null),[l,u]=(0,r.useState)(!1);n!==l&&(u(n),n&&c(d()));let f=a(()=>{(e.m(i)??s)?.focus({preventScroll:o})});return(0,r.useEffect)(()=>{if(n)return()=>{let e=document.activeElement;(!e||e===document.body||!document.contains(e))&&f()}},[n,f]),{returnFocus:f}}var p=10;function m(e,t){return t<0?-1:Math.max(0,Math.min(e,t))}function h(e,t,n,r,i){if(n===0)return-1;let a=e;for(let e=0;e<n;e++){if(a+=t,a<0||a>=n){if(!r)return-1;a=(a%n+n)%n}if(!i?.(a))return a}return-1}function g(e,t,n){return h(e===1?-1:t,e,t,!1,n)}function _(t){let{count:a,columns:s,columnIndexOf:c,pageSize:l=p,defaultActiveColumn:u=0,onActiveCellChange:d,orientation:f=`vertical`,loop:_=!0,activeIndex:v,defaultActiveIndex:y=0,onActiveIndexChange:b,isDisabled:x,textOf:S,focusItems:C=!0,onSelect:w}=t,[T,E]=o({value:v,defaultValue:y,onChange:b}),D=Math.min(T,a-1),O=s!==void 0,k=(0,r.useCallback)(e=>typeof s==`function`?s(e):s??0,[s]),A=(0,r.useCallback)((e,t)=>{let n=k(e);if(!c)return m(t,n-1);if(n<=0)return-1;let r=0;for(;r+1<n&&c(e,r+1)<=t;)r++;return r},[c,k]),[j,M]=(0,r.useState)(u),N=O&&D>=0?A(D,j):-1,P=(0,r.useRef)([]),F=(0,r.useRef)(new Map),I=(0,r.useRef)(new Map),L=(0,r.useRef)(new Map),R=(0,r.useRef)({query:``,at:0}),z=(0,r.useCallback)(e=>{P.current[e]?.focus()},[]),B=(0,r.useRef)(!1),V=(0,r.useCallback)((e,t)=>{B.current=!0,I.current.get(`${e}:${t}`)?.focus(),B.current=!1},[]),H=(0,r.useCallback)(()=>O?I.current.get(`${D}:${N}`)??null:P.current[D]??null,[N,D,O]),U=i({row:D,column:N}),W=i(j),G=i(d),K=i(k),q=i(A),J=i(c),Y=(0,r.useCallback)((e,t,n)=>{let r=m(e,a-1),i=Math.max(0,t),o=r<0?-1:q.current(r,i),s=U.current;return r===s.row&&o===s.column?s:(E(r,n),M(i),G.current?.({row:r,column:o},n),{row:r,column:o})},[U,q,G,a,E]),X=(0,r.useCallback)((e,t,n)=>{let r=m(e,a-1),i=r<0?-1:m(Math.max(0,t),K.current(r)-1);return Y(r,i<0?0:J.current?.(r,i)??i,n)},[J,a,Y,K]),Z=(0,r.useCallback)((e,t)=>{let n=`${e}:${t}`;return L.current.has(n)||L.current.set(n,e=>{e?I.current.set(n,e):(I.current.delete(n),L.current.delete(n))}),{ref:L.current.get(n),tabIndex:e===D&&t===N?0:-1,onFocus:()=>{B.current||X(e,t,{reason:`focus`})}}},[N,D,X]),Q=(0,r.useCallback)(e=>(F.current.has(e)||F.current.set(e,t=>{P.current[e]=t}),{ref:F.current.get(e),tabIndex:C?e===D?0:-1:void 0,onFocus:C?()=>E(e,{reason:`focus`}):void 0}),[D,C,E]),$=(0,r.useCallback)(t=>{let{row:n,column:r}=U.current,i=(e,n)=>{t.preventDefault();let r=X(e,n,{reason:`keyboard`,event:t});C&&V(r.row,r.column)},o=e=>{t.preventDefault();let n=Y(e,W.current,{reason:`keyboard`,event:t});C&&V(n.row,n.column)},s=2**53-1,c=()=>e.g(t.currentTarget)?-1:1;switch(t.key){case`ArrowRight`:return i(n,r+c());case`ArrowLeft`:return i(n,r-c());case`ArrowDown`:return o(n+1);case`ArrowUp`:return o(n-1);case`Home`:return t.ctrlKey||t.metaKey?i(0,0):i(n,0);case`End`:return t.ctrlKey||t.metaKey?i(a-1,s):i(n,s);case`PageDown`:return o(n+l);case`PageUp`:return o(n-l)}},[U,a,W,V,C,l,Y,X]);return{activeIndex:D,activeColumn:N,setActiveIndex:E,setActiveCell:X,onKeyDown:(0,r.useCallback)(t=>{if(t.defaultPrevented)return;if(O){$(t);return}let r=f!==`horizontal`,i=f!==`vertical`,o=R.current,s=n.t.isOpen(o,t.timeStamp),c=(e,n)=>{e!==-1&&e!==D&&(t.preventDefault(),E(e,{reason:n,event:t}),C&&z(e))};if(i&&(t.key===`ArrowRight`||t.key===`ArrowLeft`)){let n=t.key===`ArrowRight`!==e.g(t.currentTarget);c(h(D,n?1:-1,a,_,x),`keyboard`);return}if(t.key===`ArrowDown`&&r){c(h(D,1,a,_,x),`keyboard`);return}if(t.key===`ArrowUp`&&r){c(h(D,-1,a,_,x),`keyboard`);return}if(t.key===`Home`){c(g(1,a,x),`keyboard`);return}if(t.key===`End`){c(g(-1,a,x),`keyboard`);return}if(t.key===`Enter`||t.key===` `&&!(s&&S)){if(!w||D===-1)return;t.preventDefault(),w(D,t);return}if(S&&n.t.isPrintable(t)){let e=n.t.push(o,t.key,t.timeStamp);c(n.t.target(e,D,a,S,x),`typeahead`)}},[D,a,z,C,x,O,_,$,w,f,E,S]),itemProps:Q,cellProps:Z,activeItem:H}}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return _}});