@box-kite/react 2.2.16 → 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.
- package/.claude/rules/box-kite-rules.md +3 -3
- package/.claude/skills/box-kite/SKILL.md +3 -3
- package/.claude/skills/box-kite/references/components.md +1 -1
- package/.claude/skills/box-kite/references/styling.md +1 -1
- package/.cursor/rules/box-kite.mdc +3 -3
- package/AGENTS.md +3 -3
- package/BOX_AI_CONTEXT.md +2 -2
- package/BOX_KITE_AI_CONTEXT.md +2 -2
- package/behavior.cjs +1 -1
- package/behavior.mjs +62 -62
- package/catalog.cjs +1 -1
- package/catalog.mjs +3 -3
- package/client.cjs +1 -1
- package/client.mjs +15 -15
- package/components/accordion.cjs +1 -1
- package/components/accordion.mjs +1 -1
- package/components/calendar.cjs +1 -1
- package/components/calendar.mjs +1 -1
- package/components/combobox.cjs +1 -1
- package/components/combobox.mjs +1 -1
- package/components/dashboard.cjs +1 -1
- package/components/dashboard.mjs +1 -1
- package/components/dataGrid.cjs +2 -2
- package/components/dataGrid.mjs +11 -11
- package/components/dateField.cjs +1 -1
- package/components/dateField.mjs +1 -1
- package/components/datePicker.cjs +1 -1
- package/components/datePicker.mjs +198 -198
- package/components/dialog.cjs +1 -1
- package/components/dialog.mjs +140 -132
- package/components/form.cjs +1 -1
- package/components/form.mjs +1 -1
- package/components/menu.cjs +1 -1
- package/components/menu.mjs +120 -119
- package/components/overlay.cjs +1 -1
- package/components/overlay.mjs +5 -4
- package/components/popover.cjs +1 -1
- package/components/popover.mjs +50 -49
- package/components/presence.cjs +1 -1
- package/components/presence.mjs +1 -1
- package/components/radioGroup.cjs +1 -1
- package/components/radioGroup.mjs +1 -1
- package/components/semantics.d.ts +29 -29
- package/components/slider.cjs +1 -1
- package/components/slider.mjs +1 -1
- package/components/sortableList.cjs +1 -1
- package/components/sortableList.mjs +1 -1
- package/components/swipeItem.cjs +1 -1
- package/components/swipeItem.mjs +12 -12
- package/components/tabs.cjs +1 -1
- package/components/tabs.mjs +2 -2
- package/components/toaster.cjs +1 -1
- package/components/toaster.mjs +133 -115
- package/components/tooltip.cjs +1 -1
- package/components/tooltip.mjs +1 -1
- package/components/virtualList.cjs +1 -1
- package/components/virtualList.mjs +1 -1
- package/docs/props.md +2 -2
- package/interactionListening.cjs +1 -1
- package/interactionListening.mjs +3 -3
- package/layer.cjs +1 -1
- package/layer.mjs +17 -14
- package/motion.cjs +1 -1
- package/motion.mjs +30 -20
- package/package.json +40 -40
- package/placement.cjs +1 -1
- package/placement.mjs +10 -10
- package/platform.cjs +1 -1
- package/platform.mjs +18 -11
- package/react/animation/useHeldContent.d.ts +6 -0
- package/react/animation/usePresence.d.ts +4 -0
- package/react-shared.cjs +1 -1
- package/react-shared.mjs +1 -1
- package/scrolling.cjs +1 -1
- package/scrolling.mjs +1 -1
- 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.
|
package/BOX_KITE_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.
|
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.
|
|
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 _}});
|