@box-kite/react 2.2.18 → 2.3.0
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 -1
- package/.claude/skills/box-kite/SKILL.md +3 -1
- package/.claude/skills/box-kite/references/patterns.md +1 -1
- package/.claude/skills/box-kite/references/styling.md +1 -1
- package/.cursor/rules/box-kite.mdc +3 -1
- package/AGENTS.md +3 -1
- package/BOX_AI_CONTEXT.md +38 -1
- package/BOX_KITE_AI_CONTEXT.md +38 -1
- package/behavior.cjs +1 -1
- package/behavior.mjs +1 -1
- package/box.cjs +1 -1
- package/box.d.ts +1 -0
- package/box.mjs +1 -1
- package/catalog.cjs +1 -1
- package/catalog.mjs +1 -1
- package/client.cjs +1 -1
- package/client.mjs +14 -14
- package/components/calendar.cjs +1 -1
- package/components/calendar.mjs +21 -20
- package/components/dashboard.cjs +1 -1
- package/components/dashboard.mjs +12 -12
- package/components/dataGrid.cjs +1 -1
- package/components/dataGrid.mjs +9 -9
- package/components/dateField.cjs +1 -1
- package/components/dateField.mjs +6 -6
- package/components/datePicker.cjs +1 -1
- package/components/datePicker.mjs +116 -111
- package/components/dialog.cjs +1 -1
- package/components/dialog.d.ts +1 -1
- package/components/dialog.mjs +1 -1
- package/components/dropdown.cjs +1 -1
- package/components/dropdown.mjs +8 -3
- package/components/menu.cjs +1 -1
- package/components/menu.mjs +10 -10
- package/components/overlay.cjs +1 -1
- package/components/overlay.mjs +6 -6
- package/components/popover.cjs +1 -1
- package/components/popover.d.ts +8 -2
- package/components/popover.mjs +58 -55
- package/components/slider.cjs +1 -1
- package/components/slider.mjs +8 -8
- package/components/swipeItem.cjs +1 -1
- package/components/swipeItem.mjs +10 -10
- package/components/tabs.cjs +1 -1
- package/components/tabs.mjs +9 -9
- package/docs/props.md +6 -6
- package/interactionListening.cjs +1 -1
- package/interactionListening.mjs +3 -3
- package/layer.cjs +1 -1
- package/layer.mjs +1 -1
- package/package.json +2 -2
- package/placement.cjs +1 -1
- package/placement.mjs +1 -1
- package/platform.cjs +1 -1
- package/platform.mjs +16 -11
- package/react-shared.cjs +1 -1
- package/react-shared.mjs +1 -1
- package/rsc.cjs +1 -1
- package/rsc.d.ts +1 -0
- package/rsc.mjs +1 -1
- package/utils/dom/domUtils.d.ts +2 -0
|
@@ -44,7 +44,7 @@ globs: '**/*.{ts,tsx,jsx}'
|
|
|
44
44
|
|
|
45
45
|
28. **A length can come off the anchor, and `useAnchorPosition` writes the whole placement for you.** Every sizing prop takes an `anchor-size()` value and every single-side inset prop an `anchor()` one: `minWidth="anchor-size(width)"` is a popup as wide as its trigger with nothing measured, `maxHeight="anchor-size(height, 20rem)"` falls back when there is no anchor, and `top="anchor(bottom)"`/`insetStart="anchor(left)"` place a layer by hand where the 3×3 grid has no cell for it. The name inside is optional (it defaults to `positionAnchor`'s) and takes the same optional `--`; the axis names are `width`/`height`/`block`/`inline` and their `self-` twins, the edges `top`/`right`/`bottom`/`left`/`start`/`end`/`center`/`inside`/`outside` or a percentage. **The hook is `useAnchorPosition` from `@box-kite/react/anchor`**: `const { css, anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', align: 'start', offset: 2, matchWidth: true })`, spread on trigger and layer. `side` is `top`/`bottom` (block axis) or `start`/`end` (inline, so it mirrors in a right-to-left page), `align` lines the layer up with one of the anchor's edges, `offset` is the ÷4 scale, and `flip` is on by default — offering the side's own axis, the alignment's and both, because a candidate has to fit on _both_ axes and a lone `flip-block` does nothing for a layer that overflows the cross one. **The pre-built layers take the same four**: `Overlay` and `Tooltip` place with `side`/`align`/`offset`/`flip` (plus `anchor`, the element to hang off, and `matchWidth`), and the `Dropdown` popup and the DataGrid column menu are those. A gap is `offset`, not a margin or a translate. Where the browser has no anchor positioning it measures instead — same model, flip and shift — and `css` says which path ran. **Three things to know**: the layer is `position: fixed`, so it escapes every `overflow: hidden` ancestor without a portal, but _not_ a transformed one and not the page's stacking order (`Overlay` is this hook plus the top layer, and is still the answer when the layer must come out on top of everything); a flip is **sticky** — the browser keeps it until the layer is laid out afresh, so a popup that mounts when it opens always picks the side that fits while one that stays mounted keeps the side it first chose, and in the **top layer** it is never re-evaluated on scroll at all (rule 30); and the anchor's **name is an inline style rather than a prop**, because an identity is per instance and a class for it would be a rule per instance that is never freed. Which side the browser chose is `trackSide`, and it arrives too late for an entrance: reading the used `position-area` _is_ the style resolution `@starting-style` computes from, so only an exit can depend on it.
|
|
46
46
|
|
|
47
|
-
29. **A panel with content in it is `<Popover>`, and it needs no portal.** `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` (`components/popover`) is the platform's Popover API: the panel is in the browser's **top layer**, so it paints above every stacking context and outside every clipped ancestor, and **light dismiss (Escape, an outside press) and focus return are the browser's** rather than something to wire up. Note the shape — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`, whose `content` is the description and whose children are the trigger. It takes `useAnchorPosition`'s four placement props (`side`/`align`/`offset`/`flip`, plus `matchWidth`), and it supplies what the attribute does not: `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus
|
|
47
|
+
29. **A panel with content in it is `<Popover>`, and it needs no portal.** `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` (`components/popover`) is the platform's Popover API: the panel is in the browser's **top layer**, so it paints above every stacking context and outside every clipped ancestor, and **light dismiss (Escape, an outside press) and focus return are the browser's** rather than something to wire up. Note the shape — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`, whose `content` is the description and whose children are the trigger. It takes `useAnchorPosition`'s four placement props (`side`/`align`/`offset`/`flip`, plus `matchWidth`), and it supplies what the attribute does not: `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus onto the panel's first control on open — a link field selected on focus is copyable at once — or onto `initialFocus` (a ref) where the first control is the wrong landing, and `onOpenChange(open, { reason })` with `trigger`/`escape`/`outside-pointer`/`imperative`. **Four things to know, all measured.** The **trigger has to be a button**, because the toggle is the browser's own `popovertarget` — and never add an `onClick` toggle of your own: light dismiss closes on `pointerdown`, so the `click` after it reads "closed" and reopens the panel, and pressing the trigger would never close it. The **panel is always rendered**, closed being `display: none` rather than unmounted, so the exit is a CSS transition (`transitionBehavior="allow-discrete"`, already in its component styles) and **never `<Presence>`** — gate expensive children yourself with `{open ? … : null}`. A **close cannot be refused**: `beforetoggle` is cancelable opening and not closing, so a controlled `<Popover open>` hears about a dismissal only afterwards and keeping `open` true re-shows the panel. And because nothing is portalled, the panel keeps the theme, the custom properties, the direction and the tab order it was declared in — which `Overlay` gets the same way since rule 30. Use `Overlay` only for positioning with no pattern on it; where the browser has no Popover API `<Popover>` falls back to a portalled one, plus `useDismiss` and `useFocusReturn`.
|
|
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
|
|
|
@@ -97,3 +97,5 @@ globs: '**/*.{ts,tsx,jsx}'
|
|
|
97
97
|
Full reference: `src/BOX_KITE_AI_CONTEXT.md` or invoke `/box-kite` skill.
|
|
98
98
|
|
|
99
99
|
54. **A design is two families and a `Box.components()` object, and it is chosen in CSS.** Every built-in component is drawn in `accent` and `base` — two palette families that start as copies of `indigo` and `gray` — so `vars={Box.palette({ accent: 'orange', base: 'stone' })}` on an ancestor recolours every component inside it, by family name or with a ramp of your own (`{ 500: 'oklch(…)' }`); write `accent-*`/`base-*` in your own components when they should follow, and a named family when the colour is the content. The rest of a design is `Box.components(styles, { design: 'ember' })`: merged over the app's own and applied under `data-design="ember"`, which `<Box.Theme design="classic" designStorageKey="design">` writes on `<html>` and `Box.useDesign()` switches — chosen by an attribute rather than by context, so a pre-paint script that copies the stored name onto `<html>` is all a prerendered page needs, and server and browser render the same classes. **Three things to know.** A design merges and cannot delete a key, so it takes something back off with that prop's `none` (`shadow: 'none'`, `bgGradient: 'none'` — never `bgImage: 'none'`, which loses `background-image` to `bgGradient`). A design nested inside another stops the outer one, but `vars` inherit, so a `data-design="classic"` preview inside an Ember page redeclares its own `Box.palette()`. And a Box under `<Box.Components value>` resolves against that registry and no named design — `Box.createComponents()` is still the answer for a subtree whose design React decides. A part extends like a component does (`trigger: { extends: 'button' }`, `'dialog.title'`), and `letterSpacing` takes the tracking names `tighter`…`widest` in `em`.
|
|
100
|
+
|
|
101
|
+
55. **Every component is drawn at one of three sizes — `compact`, `normal` (the default) and `large` — and a size is two CSS variables.** A field or a button (`Button`, `Textbox`, `Dropdown`/`Select`, `Combobox`/`TimeZoneSelect`, `DateField`/`DatePicker`) is `height="control"` and `fontSize="control"`: 32, 40 or 48px tall, 13, 14 or 16px text. An item or a row (a menu item, a dropdown option, a tab, an accordion header, a calendar day, a time in a picker) is that height as a `minHeight`, so a long label still wraps. A checkbox, a radio, a switch and a slider scale whole, and a progress bar changes its thickness. **`variant="compact"` or `variant="large"` on any of them sizes it and everything inside it** — `<Menu variant="compact">` makes every item compact, `<DatePicker variant="large">` its calendar too, because the size is `--control-height`/`--control-font-size` and they inherit; `variant="normal"` puts the default back inside a sized subtree. **What the three sizes are is the project's, declared once**: `vars={Box.sizes({ compact: 8, normal: 9, large: 11 })}` on the app's root (a number is the ÷4 scale for the height, `{ height, fontSize }` sets both, a string is CSS) — that line is how a project matches shadcn's 36px — and `vars={Box.sizes({ size: 'compact' })}` draws a whole subtree compact. **Never size one control by hand**: `height={9}` on a single button wins as any prop does, and that is exactly how a row ends up with three heights; padding no longer sizes a control at all (`py` changes nothing, `height="auto"` hands it back). Your own control joins the standard with `height="control"` (`minHeight` where it wraps) and `fontSize="control"`; `'control-compact'`/`'control-normal'`/`'control-large'` name one size outright. `variant="square"` on a `Button` is an icon button as wide as it is tall: `<Flex gap={2}><Textbox …/><Button variant={{ secondary: true, square: true }} props={{ 'aria-label': 'Copy link' }}><Icon size={4}><Copy /></Icon></Button></Flex>`. A `DataGrid` is drawn compact.
|
|
@@ -117,7 +117,7 @@ Detect the package manager from the lock file rather than asking:
|
|
|
117
117
|
|
|
118
118
|
28. **A length can come off the anchor, and `useAnchorPosition` writes the whole placement for you.** Every sizing prop takes an `anchor-size()` value and every single-side inset prop an `anchor()` one: `minWidth="anchor-size(width)"` is a popup as wide as its trigger with nothing measured, `maxHeight="anchor-size(height, 20rem)"` falls back when there is no anchor, and `top="anchor(bottom)"`/`insetStart="anchor(left)"` place a layer by hand where the 3×3 grid has no cell for it. The name inside is optional (it defaults to `positionAnchor`'s) and takes the same optional `--`; the axis names are `width`/`height`/`block`/`inline` and their `self-` twins, the edges `top`/`right`/`bottom`/`left`/`start`/`end`/`center`/`inside`/`outside` or a percentage. **The hook is `useAnchorPosition` from `@box-kite/react/anchor`**: `const { css, anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', align: 'start', offset: 2, matchWidth: true })`, spread on trigger and layer. `side` is `top`/`bottom` (block axis) or `start`/`end` (inline, so it mirrors in a right-to-left page), `align` lines the layer up with one of the anchor's edges, `offset` is the ÷4 scale, and `flip` is on by default — offering the side's own axis, the alignment's and both, because a candidate has to fit on _both_ axes and a lone `flip-block` does nothing for a layer that overflows the cross one. **The pre-built layers take the same four**: `Overlay` and `Tooltip` place with `side`/`align`/`offset`/`flip` (plus `anchor`, the element to hang off, and `matchWidth`), and the `Dropdown` popup and the DataGrid column menu are those. A gap is `offset`, not a margin or a translate. Where the browser has no anchor positioning it measures instead — same model, flip and shift — and `css` says which path ran. **Three things to know**: the layer is `position: fixed`, so it escapes every `overflow: hidden` ancestor without a portal, but _not_ a transformed one and not the page's stacking order (`Overlay` is this hook plus the top layer, and is still the answer when the layer must come out on top of everything); a flip is **sticky** — the browser keeps it until the layer is laid out afresh, so a popup that mounts when it opens always picks the side that fits while one that stays mounted keeps the side it first chose, and in the **top layer** it is never re-evaluated on scroll at all (rule 30); and the anchor's **name is an inline style rather than a prop**, because an identity is per instance and a class for it would be a rule per instance that is never freed. Which side the browser chose is `trackSide`, and it arrives too late for an entrance: reading the used `position-area` _is_ the style resolution `@starting-style` computes from, so only an exit can depend on it.
|
|
119
119
|
|
|
120
|
-
29. **A panel with content in it is `<Popover>`, and it needs no portal.** `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` (`components/popover`) is the platform's Popover API: the panel is in the browser's **top layer**, so it paints above every stacking context and outside every clipped ancestor, and **light dismiss (Escape, an outside press) and focus return are the browser's** rather than something to wire up. Note the shape — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`, whose `content` is the description and whose children are the trigger. It takes `useAnchorPosition`'s four placement props (`side`/`align`/`offset`/`flip`, plus `matchWidth`), and it supplies what the attribute does not: `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus
|
|
120
|
+
29. **A panel with content in it is `<Popover>`, and it needs no portal.** `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` (`components/popover`) is the platform's Popover API: the panel is in the browser's **top layer**, so it paints above every stacking context and outside every clipped ancestor, and **light dismiss (Escape, an outside press) and focus return are the browser's** rather than something to wire up. Note the shape — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`, whose `content` is the description and whose children are the trigger. It takes `useAnchorPosition`'s four placement props (`side`/`align`/`offset`/`flip`, plus `matchWidth`), and it supplies what the attribute does not: `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus onto the panel's first control on open — a link field selected on focus is copyable at once — or onto `initialFocus` (a ref) where the first control is the wrong landing, and `onOpenChange(open, { reason })` with `trigger`/`escape`/`outside-pointer`/`imperative`. **Four things to know, all measured.** The **trigger has to be a button**, because the toggle is the browser's own `popovertarget` — and never add an `onClick` toggle of your own: light dismiss closes on `pointerdown`, so the `click` after it reads "closed" and reopens the panel, and pressing the trigger would never close it. The **panel is always rendered**, closed being `display: none` rather than unmounted, so the exit is a CSS transition (`transitionBehavior="allow-discrete"`, already in its component styles) and **never `<Presence>`** — gate expensive children yourself with `{open ? … : null}`. A **close cannot be refused**: `beforetoggle` is cancelable opening and not closing, so a controlled `<Popover open>` hears about a dismissal only afterwards and keeping `open` true re-shows the panel. And because nothing is portalled, the panel keeps the theme, the custom properties, the direction and the tab order it was declared in — which `Overlay` gets the same way since rule 30. Use `Overlay` only for positioning with no pattern on it; where the browser has no Popover API `<Popover>` falls back to a portalled one, plus `useDismiss` and `useFocusReturn`.
|
|
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
|
|
|
@@ -170,3 +170,5 @@ Detect the package manager from the lock file rather than asking:
|
|
|
170
170
|
Full reference: `references/` beside this file, and `docs/props.md` inside the package.
|
|
171
171
|
|
|
172
172
|
54. **A design is two families and a `Box.components()` object, and it is chosen in CSS.** Every built-in component is drawn in `accent` and `base` — two palette families that start as copies of `indigo` and `gray` — so `vars={Box.palette({ accent: 'orange', base: 'stone' })}` on an ancestor recolours every component inside it, by family name or with a ramp of your own (`{ 500: 'oklch(…)' }`); write `accent-*`/`base-*` in your own components when they should follow, and a named family when the colour is the content. The rest of a design is `Box.components(styles, { design: 'ember' })`: merged over the app's own and applied under `data-design="ember"`, which `<Box.Theme design="classic" designStorageKey="design">` writes on `<html>` and `Box.useDesign()` switches — chosen by an attribute rather than by context, so a pre-paint script that copies the stored name onto `<html>` is all a prerendered page needs, and server and browser render the same classes. **Three things to know.** A design merges and cannot delete a key, so it takes something back off with that prop's `none` (`shadow: 'none'`, `bgGradient: 'none'` — never `bgImage: 'none'`, which loses `background-image` to `bgGradient`). A design nested inside another stops the outer one, but `vars` inherit, so a `data-design="classic"` preview inside an Ember page redeclares its own `Box.palette()`. And a Box under `<Box.Components value>` resolves against that registry and no named design — `Box.createComponents()` is still the answer for a subtree whose design React decides. A part extends like a component does (`trigger: { extends: 'button' }`, `'dialog.title'`), and `letterSpacing` takes the tracking names `tighter`…`widest` in `em`.
|
|
173
|
+
|
|
174
|
+
55. **Every component is drawn at one of three sizes — `compact`, `normal` (the default) and `large` — and a size is two CSS variables.** A field or a button (`Button`, `Textbox`, `Dropdown`/`Select`, `Combobox`/`TimeZoneSelect`, `DateField`/`DatePicker`) is `height="control"` and `fontSize="control"`: 32, 40 or 48px tall, 13, 14 or 16px text. An item or a row (a menu item, a dropdown option, a tab, an accordion header, a calendar day, a time in a picker) is that height as a `minHeight`, so a long label still wraps. A checkbox, a radio, a switch and a slider scale whole, and a progress bar changes its thickness. **`variant="compact"` or `variant="large"` on any of them sizes it and everything inside it** — `<Menu variant="compact">` makes every item compact, `<DatePicker variant="large">` its calendar too, because the size is `--control-height`/`--control-font-size` and they inherit; `variant="normal"` puts the default back inside a sized subtree. **What the three sizes are is the project's, declared once**: `vars={Box.sizes({ compact: 8, normal: 9, large: 11 })}` on the app's root (a number is the ÷4 scale for the height, `{ height, fontSize }` sets both, a string is CSS) — that line is how a project matches shadcn's 36px — and `vars={Box.sizes({ size: 'compact' })}` draws a whole subtree compact. **Never size one control by hand**: `height={9}` on a single button wins as any prop does, and that is exactly how a row ends up with three heights; padding no longer sizes a control at all (`py` changes nothing, `height="auto"` hands it back). Your own control joins the standard with `height="control"` (`minHeight` where it wraps) and `fontSize="control"`; `'control-compact'`/`'control-normal'`/`'control-large'` name one size outright. `variant="square"` on a `Button` is an icon button as wide as it is tall: `<Flex gap={2}><Textbox …/><Button variant={{ secondary: true, square: true }} props={{ 'aria-label': 'Copy link' }}><Icon size={4}><Copy /></Icon></Button></Flex>`. A `DataGrid` is drawn compact.
|
|
@@ -19,7 +19,7 @@ The recipes, plus the parts of the library that are not props: server rendering,
|
|
|
19
19
|
**Config**: `Box.configure({ sink: 'cssom' | 'textContent' | 'string' | 'element', classNames: 'hashed' | 'readable' | 'stable' })` — before the first render.
|
|
20
20
|
**Floating layers**: `Overlay` (`components/overlay`) renders its children where it is declared, in the browser's **top layer** (`popover="manual"`, shown on mount) — so it beats every stacking context, escapes every clipped and transformed ancestor, and still inherits the theme, the custom properties and the direction around it. `manual` because it owns no dismissal, no ARIA and no open state. `Tooltip`, the `Dropdown` popup and the DataGrid column menu are all this. **Never declare one inside its trigger** — a listbox or a menu inside a `<button>` is unreachable content, and the portal used to hide that by moving it. Where the browser has no Popover API it is portalled into `#box-kite-portal` instead, which is the old behaviour. Reach for it only when you want positioning with no pattern on it; a panel with content in it is `Popover`.
|
|
21
21
|
**Form controls** (`components/checkbox`, `components/switch`, `components/radioGroup`, `components/radioButton`): real native inputs, so focus, the checked state and form submission come from the platform. `label` is the prop that matters — it renders the text inside a `<label>` wrapping the input, so there is no `htmlFor`/`id` pair and the whole row is clickable; `labelProps` styles that label, every other Box prop styles the control. `<Checkbox label="Accept the terms" name="terms" />`, `<Checkbox label="Select all" indeterminate />` (sets the DOM property **and** `aria-checked="mixed"`), `<Switch label="Email notifications" name="notify" />` (`role="switch"`; Space and Enter both toggle, Enter does not submit the form). `<RadioGroup label="Plan" name="plan" defaultValue="free" onValueChange={(v, { reason }) => …}>` with `<RadioGroup.Item value="free" label="Free" />` children: `role="radiogroup"` named by the label, one shared `name` (generated if omitted), `orientation` `vertical`|`horizontal`, arrows move **and** select with wrapping and disabled items skipped, and Tab enters/leaves the set once — the platform's own tab order is left alone. Style tree `radioGroup` (a `horizontal` variant) > `label`. `RadioButton` alone is a plain radio and still renders on a server. `checked`/`disabled`/`indeterminate` are each a state _and_ a pseudo-class style prop: pass a boolean, or `[state, styles]` for both.
|
|
22
|
-
**Popover** (`components/popover`): a panel on the platform's Popover API, and the answer whenever a layer has content a user interacts with. `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`. **No portal**: the panel is in the browser's top layer, so it beats every stacking context and clipped ancestor while keeping the theme, custom properties, direction and tab order it was declared in. Light dismiss (Escape, outside press) and focus return are the browser's; the component adds `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus
|
|
22
|
+
**Popover** (`components/popover`): a panel on the platform's Popover API, and the answer whenever a layer has content a user interacts with. `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`. **No portal**: the panel is in the browser's top layer, so it beats every stacking context and clipped ancestor while keeping the theme, custom properties, direction and tab order it was declared in. Light dismiss (Escape, outside press) and focus return are the browser's; the component adds `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus onto its first control (`autoFocus`, or `initialFocus` for another), the `side`/`align`/`offset`/`flip`/`matchWidth` placement props, and `onOpenChange(open, { reason })` with `trigger`|`escape`|`outside-pointer`|`imperative`. Traps: the trigger must be a **button** (the toggle is the browser's `popovertarget`) and must never carry an `onClick` toggle of your own — light dismiss closes on `pointerdown`, so the `click` after it reopens the panel; the panel is **always rendered** (closed is `display: none`), so the exit is `transitionBehavior="allow-discrete"` and never `<Presence>`, and expensive children want `{open ? … : null}`; and a **close cannot be refused**, since `beforetoggle` is cancelable opening only. Without the Popover API it falls back to a portalled `Overlay` + `useDismiss` + `useFocusReturn`.
|
|
23
23
|
**Dialog / AlertDialog** (`components/dialog`): a real `<dialog>` shown with `showModal()`, so **the top layer, the `::backdrop`, an inert page, Escape, focus containment and focus return are the platform's** — no focus trap, no `aria-hidden` sweep, no `z-index`. `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>`. Rendering a `Dialog.Title` writes `aria-labelledby` and a `Dialog.Description` writes `aria-describedby`; neither attribute exists when the part is absent, so a dialog with no visible heading wants `label`/`labelledBy`/`describedBy`. Props: `trigger` (optional — open it from anywhere), `open`/`defaultOpen`/`onOpenChange(open, { reason })` with `trigger`|`escape`|`outside-pointer`|`imperative`, `modal` (default `true`; `false` is `show()` — no backdrop, nothing inert, positioned against its containing block), `dismissible`, `lockScroll` (default: `modal`) and `initialFocus`. `AlertDialog` is a named export of the same module: `role="alertdialog"`, always modal, never dismissed by a press outside, and `initialFocus` on the least destructive action (APG). Traps, all measured: the dialog is **always rendered** (closed is `display: none`), so the exit is a CSS transition and never `<Presence>`; a **close cannot be refused** (`cancel` fires after the fact — use `dismissible={false}`); a press outside is `closedby="any"`, and on a browser without it the press is measured against the dialog's box, because **for a modal dialog the backdrop is the dialog element**; and the platform does not stop the page scrolling, which is what `lockScroll` is for — an `overflow: hidden` class on `<html>`, counted so nested dialogs cannot unlock it early.
|
|
24
24
|
|
|
25
25
|
**Menu** (`components/menu`): APG's menu button on the same Popover API, and a submenu is a popover nested inside 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>`. Parts: `Menu.Item`, `Menu.CheckboxItem` (`checked`/`defaultChecked`/`onCheckedChange`), `Menu.RadioGroup` (`value`/`defaultValue`/`onValueChange`) + `Menu.RadioItem value`, `Menu.Group label` (which names the group through `aria-labelledby`), `Menu.Separator`, and `Menu.Sub label`, which renders its own `menuitem` with `aria-haspopup="menu"` and `aria-expanded` plus the menu beside it. Props on the menu: `trigger`, `open`/`defaultOpen`/`onOpenChange(open, { reason })` with `select`|`tab` beside `trigger`|`escape`|`outside-pointer`|`imperative`, `label`/`labelledBy` (left out, the menu takes its trigger's name — APG's rule), and the four placement props `side`/`align`/`offset`/`flip`, which `Menu.Sub` takes too (`side="end"`, `offset={0}` by default, so a submenu abuts its menu). The platform supplies the top layer, light dismiss — Escape closing the innermost menu first — and the toggle; the component supplies the roles, the keys (Down/Up wrapping, Home/End, typeahead where a letter again cycles, Right/Left for a submenu in **reading order**, so they swap in a right-to-left menu) and the focus moves the browser does not make: into the first item on open, and back onto a submenu's own item when it closes. Four things to know: a `disabled` item is `aria-disabled` and **stays focusable**, as APG asks; `Menu.Item` closes the menu on select while a checkbox or radio item does not, and `closeOnSelect` swaps either default; the menu is **always rendered** (closed is `display: none`), so the exit is a CSS transition and never `<Presence>`, and expensive items want gating with `{open ? … : null}`; and it inherits the top layer's one cost, keeping the side it opened on (see `Overlay`). The semantic `<menu>` element is `MenuList` from `components/semantics` now, since two exports named `Menu` are one import away from the wrong component.
|
|
@@ -6,7 +6,7 @@ Every prop by category, the six kinds of nesting, and the theme system. Read thi
|
|
|
6
6
|
|
|
7
7
|
**Spacing**: `p`/`px`/`py`/`pt`/`pr`/`pb`/`pl`/`ps`/`pe`, `m`/`mx`/`my`/`mt`/`mr`/`mb`/`ml`/`ms`/`me`, `gap` — the axis pair is already logical (`px` is `padding-inline`) and `ps`/`pe`, `ms`/`me` are its two sides
|
|
8
8
|
**Layout**: `display`, `d` (flex-direction), `wrap`, `ai` (align-items), `jc` (justify-content), `justifyItems`/`placeItems`/`placeContent`/`alignContent`/`alignSelf`/`justifySelf`, `flex`/`grow`/`shrink`, `container`/`containerName`/`containerType` (a query container — see `cq`). Every alignment prop also takes the **overflow-safe** forms — `'safe center'`, `'safe start'`, `'safe end'` and the `'unsafe …'` twins: `safe center` centres until the content stops fitting and then aligns to `start`, where plain `center` overflows both edges and the half above the scrollable origin cannot be reached at all. `aspectRatio` is `'square'` (1/1), `'video'` (16/9), a compact ratio (`'4/3'`), a number, or `'auto'`
|
|
9
|
-
**Sizing**: `width`/`height` — number (÷4=rem), `'auto'`, `'fit'` (100%), `'fit-screen'` (100vw/vh), fractions (`'1/2'`…), `'33%'`. `minWidth`/`maxWidth`/`minHeight`/`maxHeight` same. All accept `"5%"`.
|
|
9
|
+
**Sizing**: `width`/`height` — number (÷4=rem), `'auto'`, `'fit'` (100%), `'fit-screen'` (100vw/vh), fractions (`'1/2'`…), `'33%'`. `minWidth`/`maxWidth`/`minHeight`/`maxHeight` same. All accept `"5%"`. `height`/`minHeight` (and `fontSize`) also take `'control'` — the size standard in force, `var(--control-height,…)`, 40px by default — and `'control-compact'`/`'control-normal'`/`'control-large'`. Every built-in component is drawn at `compact` (32px), `normal` or `large` (48px) via `variant`, a size inherits, and a project declares its three once with `vars={Box.sizes({ compact: 8, normal: 9, large: 11 })}` on its root.
|
|
10
10
|
**Colors**: `bgColor`/`color`/`borderColor`/`outlineColor`/`fill`/`stroke` — Tailwind's OKLCH palette, 26 families × 11 steps `'50'`..`'950'` (slate/gray/zinc/neutral/stone/mauve/mist/olive/taupe/red/orange/amber/yellow/lime/green/emerald/teal/cyan/sky/blue/indigo/violet/purple/fuchsia/pink/rose), plus `'white'`/`'black'`/`'transparent'`/`'currentColor'`. **Opacity modifier on any colour value**: `bgColor="blue-500/40"` → `color-mix(in oklab, var(--blue-500) 40%, transparent)` — the mix wraps the _variable_, so it stays themed and shared; unlike `opacity` it fades one declaration, not the element. Works in `vars` and on a `Box.extend()` variable (`"brand/30"`); an unknown token or a percentage outside 0–100 emits nothing. Two more families, `accent` and `base` (copies of `indigo` and `gray`), are what every built-in component is drawn in; `vars={Box.palette({ accent: 'orange', base: 'stone' })}` repoints them for a subtree, by family name or with a ramp of your own
|
|
11
11
|
**Borders**: `b`/`bx`/`by`/`bt`/`br`/`bb`/`bl`/`bs`/`be` (px — `bs`/`be` are the logical sides), `borderRadius` (÷4) and its nine physical corners plus six logical ones (`borderRadiusStart`/`borderRadiusEnd`, and `borderRadiusStartStart`/`StartEnd`/`EndEnd`/`EndStart` — **block axis first, inline axis second**), `borderStyle`
|
|
12
12
|
**Text**: `fontSize` (÷16), `fontWeight`, `lineHeight` (px), `textAlign` (`'start'`/`'end'` follow the text direction where `'left'`/`'right'` do not)/`textDecoration`/`textTransform`/`whiteSpace`/`textOverflow`, `textWrap`
|
|
@@ -52,7 +52,7 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
|
|
|
52
52
|
|
|
53
53
|
28. **A length can come off the anchor, and `useAnchorPosition` writes the whole placement for you.** Every sizing prop takes an `anchor-size()` value and every single-side inset prop an `anchor()` one: `minWidth="anchor-size(width)"` is a popup as wide as its trigger with nothing measured, `maxHeight="anchor-size(height, 20rem)"` falls back when there is no anchor, and `top="anchor(bottom)"`/`insetStart="anchor(left)"` place a layer by hand where the 3×3 grid has no cell for it. The name inside is optional (it defaults to `positionAnchor`'s) and takes the same optional `--`; the axis names are `width`/`height`/`block`/`inline` and their `self-` twins, the edges `top`/`right`/`bottom`/`left`/`start`/`end`/`center`/`inside`/`outside` or a percentage. **The hook is `useAnchorPosition` from `@box-kite/react/anchor`**: `const { css, anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', align: 'start', offset: 2, matchWidth: true })`, spread on trigger and layer. `side` is `top`/`bottom` (block axis) or `start`/`end` (inline, so it mirrors in a right-to-left page), `align` lines the layer up with one of the anchor's edges, `offset` is the ÷4 scale, and `flip` is on by default — offering the side's own axis, the alignment's and both, because a candidate has to fit on _both_ axes and a lone `flip-block` does nothing for a layer that overflows the cross one. **The pre-built layers take the same four**: `Overlay` and `Tooltip` place with `side`/`align`/`offset`/`flip` (plus `anchor`, the element to hang off, and `matchWidth`), and the `Dropdown` popup and the DataGrid column menu are those. A gap is `offset`, not a margin or a translate. Where the browser has no anchor positioning it measures instead — same model, flip and shift — and `css` says which path ran. **Three things to know**: the layer is `position: fixed`, so it escapes every `overflow: hidden` ancestor without a portal, but _not_ a transformed one and not the page's stacking order (`Overlay` is this hook plus the top layer, and is still the answer when the layer must come out on top of everything); a flip is **sticky** — the browser keeps it until the layer is laid out afresh, so a popup that mounts when it opens always picks the side that fits while one that stays mounted keeps the side it first chose, and in the **top layer** it is never re-evaluated on scroll at all (rule 30); and the anchor's **name is an inline style rather than a prop**, because an identity is per instance and a class for it would be a rule per instance that is never freed. Which side the browser chose is `trackSide`, and it arrives too late for an entrance: reading the used `position-area` _is_ the style resolution `@starting-style` computes from, so only an exit can depend on it.
|
|
54
54
|
|
|
55
|
-
29. **A panel with content in it is `<Popover>`, and it needs no portal.** `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` (`components/popover`) is the platform's Popover API: the panel is in the browser's **top layer**, so it paints above every stacking context and outside every clipped ancestor, and **light dismiss (Escape, an outside press) and focus return are the browser's** rather than something to wire up. Note the shape — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`, whose `content` is the description and whose children are the trigger. It takes `useAnchorPosition`'s four placement props (`side`/`align`/`offset`/`flip`, plus `matchWidth`), and it supplies what the attribute does not: `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus
|
|
55
|
+
29. **A panel with content in it is `<Popover>`, and it needs no portal.** `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` (`components/popover`) is the platform's Popover API: the panel is in the browser's **top layer**, so it paints above every stacking context and outside every clipped ancestor, and **light dismiss (Escape, an outside press) and focus return are the browser's** rather than something to wire up. Note the shape — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`, whose `content` is the description and whose children are the trigger. It takes `useAnchorPosition`'s four placement props (`side`/`align`/`offset`/`flip`, plus `matchWidth`), and it supplies what the attribute does not: `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus onto the panel's first control on open — a link field selected on focus is copyable at once — or onto `initialFocus` (a ref) where the first control is the wrong landing, and `onOpenChange(open, { reason })` with `trigger`/`escape`/`outside-pointer`/`imperative`. **Four things to know, all measured.** The **trigger has to be a button**, because the toggle is the browser's own `popovertarget` — and never add an `onClick` toggle of your own: light dismiss closes on `pointerdown`, so the `click` after it reads "closed" and reopens the panel, and pressing the trigger would never close it. The **panel is always rendered**, closed being `display: none` rather than unmounted, so the exit is a CSS transition (`transitionBehavior="allow-discrete"`, already in its component styles) and **never `<Presence>`** — gate expensive children yourself with `{open ? … : null}`. A **close cannot be refused**: `beforetoggle` is cancelable opening and not closing, so a controlled `<Popover open>` hears about a dismissal only afterwards and keeping `open` true re-shows the panel. And because nothing is portalled, the panel keeps the theme, the custom properties, the direction and the tab order it was declared in — which `Overlay` gets the same way since rule 30. Use `Overlay` only for positioning with no pattern on it; where the browser has no Popover API `<Popover>` falls back to a portalled one, plus `useDismiss` and `useFocusReturn`.
|
|
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
|
|
|
@@ -105,3 +105,5 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
|
|
|
105
105
|
Full reference: `node_modules/@box-kite/react/docs/props.md` in the project, or https://www.box-kite.dev/props.md — every prop, the CSS it writes and one measured example.
|
|
106
106
|
|
|
107
107
|
54. **A design is two families and a `Box.components()` object, and it is chosen in CSS.** Every built-in component is drawn in `accent` and `base` — two palette families that start as copies of `indigo` and `gray` — so `vars={Box.palette({ accent: 'orange', base: 'stone' })}` on an ancestor recolours every component inside it, by family name or with a ramp of your own (`{ 500: 'oklch(…)' }`); write `accent-*`/`base-*` in your own components when they should follow, and a named family when the colour is the content. The rest of a design is `Box.components(styles, { design: 'ember' })`: merged over the app's own and applied under `data-design="ember"`, which `<Box.Theme design="classic" designStorageKey="design">` writes on `<html>` and `Box.useDesign()` switches — chosen by an attribute rather than by context, so a pre-paint script that copies the stored name onto `<html>` is all a prerendered page needs, and server and browser render the same classes. **Three things to know.** A design merges and cannot delete a key, so it takes something back off with that prop's `none` (`shadow: 'none'`, `bgGradient: 'none'` — never `bgImage: 'none'`, which loses `background-image` to `bgGradient`). A design nested inside another stops the outer one, but `vars` inherit, so a `data-design="classic"` preview inside an Ember page redeclares its own `Box.palette()`. And a Box under `<Box.Components value>` resolves against that registry and no named design — `Box.createComponents()` is still the answer for a subtree whose design React decides. A part extends like a component does (`trigger: { extends: 'button' }`, `'dialog.title'`), and `letterSpacing` takes the tracking names `tighter`…`widest` in `em`.
|
|
108
|
+
|
|
109
|
+
55. **Every component is drawn at one of three sizes — `compact`, `normal` (the default) and `large` — and a size is two CSS variables.** A field or a button (`Button`, `Textbox`, `Dropdown`/`Select`, `Combobox`/`TimeZoneSelect`, `DateField`/`DatePicker`) is `height="control"` and `fontSize="control"`: 32, 40 or 48px tall, 13, 14 or 16px text. An item or a row (a menu item, a dropdown option, a tab, an accordion header, a calendar day, a time in a picker) is that height as a `minHeight`, so a long label still wraps. A checkbox, a radio, a switch and a slider scale whole, and a progress bar changes its thickness. **`variant="compact"` or `variant="large"` on any of them sizes it and everything inside it** — `<Menu variant="compact">` makes every item compact, `<DatePicker variant="large">` its calendar too, because the size is `--control-height`/`--control-font-size` and they inherit; `variant="normal"` puts the default back inside a sized subtree. **What the three sizes are is the project's, declared once**: `vars={Box.sizes({ compact: 8, normal: 9, large: 11 })}` on the app's root (a number is the ÷4 scale for the height, `{ height, fontSize }` sets both, a string is CSS) — that line is how a project matches shadcn's 36px — and `vars={Box.sizes({ size: 'compact' })}` draws a whole subtree compact. **Never size one control by hand**: `height={9}` on a single button wins as any prop does, and that is exactly how a row ends up with three heights; padding no longer sizes a control at all (`py` changes nothing, `height="auto"` hands it back). Your own control joins the standard with `height="control"` (`minHeight` where it wraps) and `fontSize="control"`; `'control-compact'`/`'control-normal'`/`'control-large'` name one size outright. `variant="square"` on a `Button` is an icon button as wide as it is tall: `<Flex gap={2}><Textbox …/><Button variant={{ secondary: true, square: true }} props={{ 'aria-label': 'Copy link' }}><Icon size={4}><Copy /></Icon></Button></Flex>`. A `DataGrid` is drawn compact.
|
package/AGENTS.md
CHANGED
|
@@ -84,7 +84,7 @@ Every value there is a prop, and every prop value becomes one class the engine i
|
|
|
84
84
|
|
|
85
85
|
28. **A length can come off the anchor, and `useAnchorPosition` writes the whole placement for you.** Every sizing prop takes an `anchor-size()` value and every single-side inset prop an `anchor()` one: `minWidth="anchor-size(width)"` is a popup as wide as its trigger with nothing measured, `maxHeight="anchor-size(height, 20rem)"` falls back when there is no anchor, and `top="anchor(bottom)"`/`insetStart="anchor(left)"` place a layer by hand where the 3×3 grid has no cell for it. The name inside is optional (it defaults to `positionAnchor`'s) and takes the same optional `--`; the axis names are `width`/`height`/`block`/`inline` and their `self-` twins, the edges `top`/`right`/`bottom`/`left`/`start`/`end`/`center`/`inside`/`outside` or a percentage. **The hook is `useAnchorPosition` from `@box-kite/react/anchor`**: `const { css, anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', align: 'start', offset: 2, matchWidth: true })`, spread on trigger and layer. `side` is `top`/`bottom` (block axis) or `start`/`end` (inline, so it mirrors in a right-to-left page), `align` lines the layer up with one of the anchor's edges, `offset` is the ÷4 scale, and `flip` is on by default — offering the side's own axis, the alignment's and both, because a candidate has to fit on _both_ axes and a lone `flip-block` does nothing for a layer that overflows the cross one. **The pre-built layers take the same four**: `Overlay` and `Tooltip` place with `side`/`align`/`offset`/`flip` (plus `anchor`, the element to hang off, and `matchWidth`), and the `Dropdown` popup and the DataGrid column menu are those. A gap is `offset`, not a margin or a translate. Where the browser has no anchor positioning it measures instead — same model, flip and shift — and `css` says which path ran. **Three things to know**: the layer is `position: fixed`, so it escapes every `overflow: hidden` ancestor without a portal, but _not_ a transformed one and not the page's stacking order (`Overlay` is this hook plus the top layer, and is still the answer when the layer must come out on top of everything); a flip is **sticky** — the browser keeps it until the layer is laid out afresh, so a popup that mounts when it opens always picks the side that fits while one that stays mounted keeps the side it first chose, and in the **top layer** it is never re-evaluated on scroll at all (rule 30); and the anchor's **name is an inline style rather than a prop**, because an identity is per instance and a class for it would be a rule per instance that is never freed. Which side the browser chose is `trackSide`, and it arrives too late for an entrance: reading the used `position-area` _is_ the style resolution `@starting-style` computes from, so only an exit can depend on it.
|
|
86
86
|
|
|
87
|
-
29. **A panel with content in it is `<Popover>`, and it needs no portal.** `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` (`components/popover`) is the platform's Popover API: the panel is in the browser's **top layer**, so it paints above every stacking context and outside every clipped ancestor, and **light dismiss (Escape, an outside press) and focus return are the browser's** rather than something to wire up. Note the shape — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`, whose `content` is the description and whose children are the trigger. It takes `useAnchorPosition`'s four placement props (`side`/`align`/`offset`/`flip`, plus `matchWidth`), and it supplies what the attribute does not: `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus
|
|
87
|
+
29. **A panel with content in it is `<Popover>`, and it needs no portal.** `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` (`components/popover`) is the platform's Popover API: the panel is in the browser's **top layer**, so it paints above every stacking context and outside every clipped ancestor, and **light dismiss (Escape, an outside press) and focus return are the browser's** rather than something to wire up. Note the shape — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`, whose `content` is the description and whose children are the trigger. It takes `useAnchorPosition`'s four placement props (`side`/`align`/`offset`/`flip`, plus `matchWidth`), and it supplies what the attribute does not: `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus onto the panel's first control on open — a link field selected on focus is copyable at once — or onto `initialFocus` (a ref) where the first control is the wrong landing, and `onOpenChange(open, { reason })` with `trigger`/`escape`/`outside-pointer`/`imperative`. **Four things to know, all measured.** The **trigger has to be a button**, because the toggle is the browser's own `popovertarget` — and never add an `onClick` toggle of your own: light dismiss closes on `pointerdown`, so the `click` after it reads "closed" and reopens the panel, and pressing the trigger would never close it. The **panel is always rendered**, closed being `display: none` rather than unmounted, so the exit is a CSS transition (`transitionBehavior="allow-discrete"`, already in its component styles) and **never `<Presence>`** — gate expensive children yourself with `{open ? … : null}`. A **close cannot be refused**: `beforetoggle` is cancelable opening and not closing, so a controlled `<Popover open>` hears about a dismissal only afterwards and keeping `open` true re-shows the panel. And because nothing is portalled, the panel keeps the theme, the custom properties, the direction and the tab order it was declared in — which `Overlay` gets the same way since rule 30. Use `Overlay` only for positioning with no pattern on it; where the browser has no Popover API `<Popover>` falls back to a portalled one, plus `useDismiss` and `useFocusReturn`.
|
|
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
|
|
|
@@ -137,3 +137,5 @@ Every value there is a prop, and every prop value becomes one class the engine i
|
|
|
137
137
|
Full reference: `docs/props.md` and `BOX_KITE_AI_CONTEXT.md`, in this package.
|
|
138
138
|
|
|
139
139
|
54. **A design is two families and a `Box.components()` object, and it is chosen in CSS.** Every built-in component is drawn in `accent` and `base` — two palette families that start as copies of `indigo` and `gray` — so `vars={Box.palette({ accent: 'orange', base: 'stone' })}` on an ancestor recolours every component inside it, by family name or with a ramp of your own (`{ 500: 'oklch(…)' }`); write `accent-*`/`base-*` in your own components when they should follow, and a named family when the colour is the content. The rest of a design is `Box.components(styles, { design: 'ember' })`: merged over the app's own and applied under `data-design="ember"`, which `<Box.Theme design="classic" designStorageKey="design">` writes on `<html>` and `Box.useDesign()` switches — chosen by an attribute rather than by context, so a pre-paint script that copies the stored name onto `<html>` is all a prerendered page needs, and server and browser render the same classes. **Three things to know.** A design merges and cannot delete a key, so it takes something back off with that prop's `none` (`shadow: 'none'`, `bgGradient: 'none'` — never `bgImage: 'none'`, which loses `background-image` to `bgGradient`). A design nested inside another stops the outer one, but `vars` inherit, so a `data-design="classic"` preview inside an Ember page redeclares its own `Box.palette()`. And a Box under `<Box.Components value>` resolves against that registry and no named design — `Box.createComponents()` is still the answer for a subtree whose design React decides. A part extends like a component does (`trigger: { extends: 'button' }`, `'dialog.title'`), and `letterSpacing` takes the tracking names `tighter`…`widest` in `em`.
|
|
140
|
+
|
|
141
|
+
55. **Every component is drawn at one of three sizes — `compact`, `normal` (the default) and `large` — and a size is two CSS variables.** A field or a button (`Button`, `Textbox`, `Dropdown`/`Select`, `Combobox`/`TimeZoneSelect`, `DateField`/`DatePicker`) is `height="control"` and `fontSize="control"`: 32, 40 or 48px tall, 13, 14 or 16px text. An item or a row (a menu item, a dropdown option, a tab, an accordion header, a calendar day, a time in a picker) is that height as a `minHeight`, so a long label still wraps. A checkbox, a radio, a switch and a slider scale whole, and a progress bar changes its thickness. **`variant="compact"` or `variant="large"` on any of them sizes it and everything inside it** — `<Menu variant="compact">` makes every item compact, `<DatePicker variant="large">` its calendar too, because the size is `--control-height`/`--control-font-size` and they inherit; `variant="normal"` puts the default back inside a sized subtree. **What the three sizes are is the project's, declared once**: `vars={Box.sizes({ compact: 8, normal: 9, large: 11 })}` on the app's root (a number is the ÷4 scale for the height, `{ height, fontSize }` sets both, a string is CSS) — that line is how a project matches shadcn's 36px — and `vars={Box.sizes({ size: 'compact' })}` draws a whole subtree compact. **Never size one control by hand**: `height={9}` on a single button wins as any prop does, and that is exactly how a row ends up with three heights; padding no longer sizes a control at all (`py` changes nothing, `height="auto"` hands it back). Your own control joins the standard with `height="control"` (`minHeight` where it wraps) and `fontSize="control"`; `'control-compact'`/`'control-normal'`/`'control-large'` name one size outright. `variant="square"` on a `Button` is an icon button as wide as it is tall: `<Flex gap={2}><Textbox …/><Button variant={{ secondary: true, square: true }} props={{ 'aria-label': 'Copy link' }}><Icon size={4}><Copy /></Icon></Button></Flex>`. A `DataGrid` is drawn compact.
|
package/BOX_AI_CONTEXT.md
CHANGED
|
@@ -1453,6 +1453,42 @@ Full reference: `docs/a11y-primitives.md`.
|
|
|
1453
1453
|
|
|
1454
1454
|
---
|
|
1455
1455
|
|
|
1456
|
+
## Sizes: compact, normal, large
|
|
1457
|
+
|
|
1458
|
+
Every built-in component is drawn at one of three sizes, and a size is two CSS variables —
|
|
1459
|
+
`--control-height` and `--control-font-size` — so it inherits. Before 2.3.0 each control's height fell out of
|
|
1460
|
+
its own padding (a text box 46px, a button 36px, a combobox 34px), and nothing lined up.
|
|
1461
|
+
|
|
1462
|
+
| Size | Variant | Height | Text | Declared by |
|
|
1463
|
+
| ------- | ------------------- | ------ | ---- | -------------------------------------------------------- |
|
|
1464
|
+
| Compact | `variant="compact"` | 32px | 13px | `--control-height-compact`/`--control-font-size-compact` |
|
|
1465
|
+
| Normal | — (the default) | 40px | 14px | `--control-height-normal`/`--control-font-size-normal` |
|
|
1466
|
+
| Large | `variant="large"` | 48px | 16px | `--control-height-large`/`--control-font-size-large` |
|
|
1467
|
+
|
|
1468
|
+
- **What follows it.** Fields and buttons (`Button`, `Textbox`, `Dropdown`/`Select`, `Combobox`/`TimeZoneSelect`,
|
|
1469
|
+
`DateField`/`DatePicker`) take the height; items and rows (menu items, dropdown and combobox options, tabs,
|
|
1470
|
+
accordion headers, calendar days, a picker's times and presets) take it as a minimum; `Textarea` takes the text
|
|
1471
|
+
size; `Checkbox`, `RadioButton`, `Switch` and `Slider` scale whole; `Progress` changes its thickness.
|
|
1472
|
+
`Popover`, `Dialog`, `Menu`, `Tabs`, `Accordion`, `Collapsible`, `RadioGroup` and `Calendar` take the variant and
|
|
1473
|
+
pass it to everything inside. A `DataGrid` is drawn compact.
|
|
1474
|
+
- **A variant sizes the subtree**: `<Menu variant="compact">` makes every item compact, and `<DatePicker variant="large">`
|
|
1475
|
+
its calendar too (the picker hands its size to the panel, which is a sibling). `variant="normal"` restores the
|
|
1476
|
+
default inside a sized subtree.
|
|
1477
|
+
- **The project's scale is declared once**, on the app's root:
|
|
1478
|
+
|
|
1479
|
+
```tsx
|
|
1480
|
+
<Box vars={Box.sizes({ compact: 8, normal: 9, large: { height: 11, fontSize: 15 } })}>{app}</Box>
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
A number is the ÷4 scale for a height and ÷16 for a text size; a string is CSS. `Box.sizes({ size: 'compact' })`
|
|
1484
|
+
draws a whole subtree at that size.
|
|
1485
|
+
|
|
1486
|
+
- **Never size one control by hand.** `height={9}` on one button wins as any prop does, which is how a row ends up
|
|
1487
|
+
with three heights. Padding no longer sizes a control: `py` changes nothing, `height="auto"` hands it back.
|
|
1488
|
+
- **Your own control joins** with `height="control"` (`minHeight` where it wraps) and `fontSize="control"`;
|
|
1489
|
+
`'control-compact'`/`'control-normal'`/`'control-large'` name one size outright.
|
|
1490
|
+
- `variant="square"` on a `Button` is an icon button as wide as it is tall, at whichever size.
|
|
1491
|
+
|
|
1456
1492
|
## Form controls: Checkbox, Switch, RadioGroup
|
|
1457
1493
|
|
|
1458
1494
|
All three render **real native inputs**, so focus, the checked state, the disabled state and form
|
|
@@ -1572,7 +1608,8 @@ positioning only and light-dismisses nothing.
|
|
|
1572
1608
|
| `offset` | `2` | The gap, on the ÷4 scale — 8px. |
|
|
1573
1609
|
| `flip` | `true` | Whether a side with no room is swapped for its opposite. |
|
|
1574
1610
|
| `matchWidth` | `false` | Whether the panel is at least as wide as its trigger. |
|
|
1575
|
-
| `autoFocus` | `true` | Whether opening moves focus
|
|
1611
|
+
| `autoFocus` | `true` | Whether opening moves focus onto the panel's first control (the panel if none). |
|
|
1612
|
+
| `initialFocus` | — | A ref to land on instead. React's `autoFocus` writes no attribute, so use this. |
|
|
1576
1613
|
|
|
1577
1614
|
What the component supplies, because the `popover` attribute supplies none of it: `role="dialog"`, the
|
|
1578
1615
|
name, and `aria-expanded`/`aria-haspopup="dialog"`/`aria-controls` on the trigger. What the **platform**
|
package/BOX_KITE_AI_CONTEXT.md
CHANGED
|
@@ -1453,6 +1453,42 @@ Full reference: `docs/a11y-primitives.md`.
|
|
|
1453
1453
|
|
|
1454
1454
|
---
|
|
1455
1455
|
|
|
1456
|
+
## Sizes: compact, normal, large
|
|
1457
|
+
|
|
1458
|
+
Every built-in component is drawn at one of three sizes, and a size is two CSS variables —
|
|
1459
|
+
`--control-height` and `--control-font-size` — so it inherits. Before 2.3.0 each control's height fell out of
|
|
1460
|
+
its own padding (a text box 46px, a button 36px, a combobox 34px), and nothing lined up.
|
|
1461
|
+
|
|
1462
|
+
| Size | Variant | Height | Text | Declared by |
|
|
1463
|
+
| ------- | ------------------- | ------ | ---- | -------------------------------------------------------- |
|
|
1464
|
+
| Compact | `variant="compact"` | 32px | 13px | `--control-height-compact`/`--control-font-size-compact` |
|
|
1465
|
+
| Normal | — (the default) | 40px | 14px | `--control-height-normal`/`--control-font-size-normal` |
|
|
1466
|
+
| Large | `variant="large"` | 48px | 16px | `--control-height-large`/`--control-font-size-large` |
|
|
1467
|
+
|
|
1468
|
+
- **What follows it.** Fields and buttons (`Button`, `Textbox`, `Dropdown`/`Select`, `Combobox`/`TimeZoneSelect`,
|
|
1469
|
+
`DateField`/`DatePicker`) take the height; items and rows (menu items, dropdown and combobox options, tabs,
|
|
1470
|
+
accordion headers, calendar days, a picker's times and presets) take it as a minimum; `Textarea` takes the text
|
|
1471
|
+
size; `Checkbox`, `RadioButton`, `Switch` and `Slider` scale whole; `Progress` changes its thickness.
|
|
1472
|
+
`Popover`, `Dialog`, `Menu`, `Tabs`, `Accordion`, `Collapsible`, `RadioGroup` and `Calendar` take the variant and
|
|
1473
|
+
pass it to everything inside. A `DataGrid` is drawn compact.
|
|
1474
|
+
- **A variant sizes the subtree**: `<Menu variant="compact">` makes every item compact, and `<DatePicker variant="large">`
|
|
1475
|
+
its calendar too (the picker hands its size to the panel, which is a sibling). `variant="normal"` restores the
|
|
1476
|
+
default inside a sized subtree.
|
|
1477
|
+
- **The project's scale is declared once**, on the app's root:
|
|
1478
|
+
|
|
1479
|
+
```tsx
|
|
1480
|
+
<Box vars={Box.sizes({ compact: 8, normal: 9, large: { height: 11, fontSize: 15 } })}>{app}</Box>
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
A number is the ÷4 scale for a height and ÷16 for a text size; a string is CSS. `Box.sizes({ size: 'compact' })`
|
|
1484
|
+
draws a whole subtree at that size.
|
|
1485
|
+
|
|
1486
|
+
- **Never size one control by hand.** `height={9}` on one button wins as any prop does, which is how a row ends up
|
|
1487
|
+
with three heights. Padding no longer sizes a control: `py` changes nothing, `height="auto"` hands it back.
|
|
1488
|
+
- **Your own control joins** with `height="control"` (`minHeight` where it wraps) and `fontSize="control"`;
|
|
1489
|
+
`'control-compact'`/`'control-normal'`/`'control-large'` name one size outright.
|
|
1490
|
+
- `variant="square"` on a `Button` is an icon button as wide as it is tall, at whichever size.
|
|
1491
|
+
|
|
1456
1492
|
## Form controls: Checkbox, Switch, RadioGroup
|
|
1457
1493
|
|
|
1458
1494
|
All three render **real native inputs**, so focus, the checked state, the disabled state and form
|
|
@@ -1572,7 +1608,8 @@ positioning only and light-dismisses nothing.
|
|
|
1572
1608
|
| `offset` | `2` | The gap, on the ÷4 scale — 8px. |
|
|
1573
1609
|
| `flip` | `true` | Whether a side with no room is swapped for its opposite. |
|
|
1574
1610
|
| `matchWidth` | `false` | Whether the panel is at least as wide as its trigger. |
|
|
1575
|
-
| `autoFocus` | `true` | Whether opening moves focus
|
|
1611
|
+
| `autoFocus` | `true` | Whether opening moves focus onto the panel's first control (the panel if none). |
|
|
1612
|
+
| `initialFocus` | — | A ref to land on instead. React's `autoFocus` writes no attribute, so use this. |
|
|
1576
1613
|
|
|
1577
1614
|
What the component supplies, because the `popover` attribute supplies none of it: `role="dialog"`, the
|
|
1578
1615
|
name, and `aria-expanded`/`aria-haspopup="dialog"`/`aria-controls` on the trigger. What the **platform**
|
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.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.
|
|
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.g(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.h(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._(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._(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 _}});
|
package/behavior.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as e, g as t, h as n, p as r, t as i } from "./platform.mjs";
|
|
2
2
|
import { n as a } from "./effects.mjs";
|
|
3
3
|
import { t as o } from "./keyboard.mjs";
|
|
4
4
|
import { useCallback as s, useEffect as c, useRef as l, useState as u } from "react";
|
package/box.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("./rolldown-runtime.cjs"),t=require("./react-shared.cjs"),n=require("./client.cjs"),r=require("./interactions.cjs");let i=require("react");i=e.t(i,1);let a=require("@box-kite/core");function o({tag:e,tagProps:t,render:n}){let[r,a]=(0,i.useState)(!1);return i.default.createElement(e,{...t,onMouseEnter:()=>a(!0),onMouseLeave:()=>a(!1)},n({isHover:r}))}function s(e,r){let{tag:a=`div`,children:s}=e,{classNames:c,styleElements:l}=n.i(e,a===`svg`),u=t.d(e,c);r&&(u.ref=r);let d=typeof s==`function`?i.default.createElement(o,{tag:a,tagProps:u,render:s}):i.default.createElement(a,u,s);return l?i.default.createElement(i.default.Fragment,null,l,d):d}var c=t.f(s,`Box`);c.extend=a.BoxExtends.extend,c.components=a.BoxExtends.components,c.palette=a.BoxExtends.palette,c.createComponents=a.BoxExtends.createComponents,c.Components=n.o,c.keyframes=a.BoxExtends.keyframes,c.spring=a.Springs.spring,c.Theme=n.n,c.useTheme=n.n.useTheme,c.useDesign=n.n.useDesign,c.getVariableValue=e=>(0,a.getDefaultEngine)().getVariableValue(e),c.configure=n.r.configure,c.viewTransition=a.startViewTransition,c.onInteraction=r.i,c.reportInteraction=r.r,c.flushInteractions=r.n;function l(e,r){return t.p(n.i(e,r?.svg===!0),e.className)}exports.default=c,exports.useBoxElement=s,exports.useClassNames=l,exports.useVisibility=n.a;
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("./rolldown-runtime.cjs"),t=require("./react-shared.cjs"),n=require("./client.cjs"),r=require("./interactions.cjs");let i=require("react");i=e.t(i,1);let a=require("@box-kite/core");function o({tag:e,tagProps:t,render:n}){let[r,a]=(0,i.useState)(!1);return i.default.createElement(e,{...t,onMouseEnter:()=>a(!0),onMouseLeave:()=>a(!1)},n({isHover:r}))}function s(e,r){let{tag:a=`div`,children:s}=e,{classNames:c,styleElements:l}=n.i(e,a===`svg`),u=t.d(e,c);r&&(u.ref=r);let d=typeof s==`function`?i.default.createElement(o,{tag:a,tagProps:u,render:s}):i.default.createElement(a,u,s);return l?i.default.createElement(i.default.Fragment,null,l,d):d}var c=t.f(s,`Box`);c.extend=a.BoxExtends.extend,c.components=a.BoxExtends.components,c.palette=a.BoxExtends.palette,c.sizes=a.BoxExtends.sizes,c.createComponents=a.BoxExtends.createComponents,c.Components=n.o,c.keyframes=a.BoxExtends.keyframes,c.spring=a.Springs.spring,c.Theme=n.n,c.useTheme=n.n.useTheme,c.useDesign=n.n.useDesign,c.getVariableValue=e=>(0,a.getDefaultEngine)().getVariableValue(e),c.configure=n.r.configure,c.viewTransition=a.startViewTransition,c.onInteraction=r.i,c.reportInteraction=r.r,c.flushInteractions=r.n;function l(e,r){return t.p(n.i(e,r?.svg===!0),e.className)}exports.default=c,exports.useBoxElement=s,exports.useClassNames=l,exports.useVisibility=n.a;
|
package/box.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ interface BoxType {
|
|
|
24
24
|
components: typeof BoxExtends.components;
|
|
25
25
|
/** The `vars` record that points the components' `accent` and `base` families at others, for an element and its subtree. */
|
|
26
26
|
palette: typeof BoxExtends.palette;
|
|
27
|
+
sizes: typeof BoxExtends.sizes;
|
|
27
28
|
/** A design for one subtree, registered nowhere: hand it to `<Box.Components value>` or `<Box.Theme components>`. */
|
|
28
29
|
createComponents: typeof BoxExtends.createComponents;
|
|
29
30
|
/** Every Box below resolves its `component` against this registry instead of the app's `Box.components()`. */
|
package/box.mjs
CHANGED
|
@@ -23,7 +23,7 @@ function v(t, n) {
|
|
|
23
23
|
return s ? d.createElement(d.Fragment, null, s, l) : l;
|
|
24
24
|
}
|
|
25
25
|
var y = t(v, "Box");
|
|
26
|
-
y.extend = p.extend, y.components = p.components, y.palette = p.palette, y.createComponents = p.createComponents, y.Components = o, y.keyframes = p.keyframes, y.spring = m.spring, y.Theme = a, y.useTheme = a.useTheme, y.useDesign = a.useDesign, y.getVariableValue = (e) => h().getVariableValue(e), y.configure = s.configure, y.viewTransition = g, y.onInteraction = c, y.reportInteraction = u, y.flushInteractions = l;
|
|
26
|
+
y.extend = p.extend, y.components = p.components, y.palette = p.palette, y.sizes = p.sizes, y.createComponents = p.createComponents, y.Components = o, y.keyframes = p.keyframes, y.spring = m.spring, y.Theme = a, y.useTheme = a.useTheme, y.useDesign = a.useDesign, y.getVariableValue = (e) => h().getVariableValue(e), y.configure = s.configure, y.viewTransition = g, y.onInteraction = c, y.reportInteraction = u, y.flushInteractions = l;
|
|
27
27
|
function b(e, t) {
|
|
28
28
|
return n(i(e, t?.svg === !0), e.className);
|
|
29
29
|
}
|