@box-kite/react 2.3.0 → 2.3.1

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.
@@ -14,7 +14,7 @@ globs: '**/*.{ts,tsx,jsx}'
14
14
  7. **Border width and lineHeight are direct px**: `b={1}` → 1px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px
15
15
  8. **A gradient fill is a value, not an attribute**: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"` — so it can be themed and hovered. **A dashboard shape is `components/chart`**: `<Sparkline data={[…]}/>`, `<ProgressRing value={0.6}/>`, `<Gauge>`, `<MiniDonut>` — Box props, no chart library
16
16
  9. **A CSS variable is a prop too**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` on the element and everything inside it — the answer for markup this library does not render. **A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>`** (`components/chart`), which declares `--chart-1..6` in both themes and one `--color-<series>` per series, so `<Line stroke="var(--color-revenue)"/>` is all the chart ever says about colour. A record names its own paints, as anything `fill` takes: `series={{ done: 'ui-primary', open: 'ui-primary/35' }}` works with a `Box.extend()` token too.
17
- 10. **SVG lengths have no divider and no unit**: `strokeWidth={2}` → `stroke-width: 2` (user units), same for `strokeDasharray`/`strokeDashoffset` and the geometry props `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y`. `<Rect width={40} height={40}>` and `<Path d="M…">` take those as the SVG attributes they are — the Box props of those names mean something else
17
+ 10. **SVG lengths have no divider**: `strokeWidth={2}` → `stroke-width: 2` (user units), same for `strokeDasharray`/`strokeDashoffset`, and the geometry props `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y` write the number as `px`, which is a user unit (`r={20}` → `r: 20px`) — Firefox and Safari drop a bare number there. `<Rect width={40} height={40}>` and `<Path d="M…">` take those as the SVG attributes they are — the Box props of those names mean something else
18
18
  11. **HTML attributes go in `props` prop**: `<Link props={{ href: '/about' }}>` not `<Link href>`
19
19
  12. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
20
20
  13. **An animation is a prop**: `animation="spin|pulse|bounce|ping"` (keyframes included, and they stop under reduced motion on their own), `Box.keyframes({ name: { from, to } })` for your own — its steps are Box props. **Times are milliseconds**: `animationDuration={1100}`, `transitionDelay={150}`. `transition` takes a group (`colors`, `transform`, `opacity`, `shadow`, `size`, `filter`), and `translateX`/`translateY`/`rotate`/`scale` are longhands that compose. **A spring is two props**: `transitionTimingFunction="spring-bouncy"` with `transitionDuration="spring-bouncy"` (also `spring`, `spring-gentle`, `spring-snappy`; `Box.spring({ stiffness, damping })` for your own)
@@ -99,3 +99,5 @@ Full reference: `src/BOX_KITE_AI_CONTEXT.md` or invoke `/box-kite` skill.
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
100
 
101
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.
102
+
103
+ 56. **An SVG shape turns about the viewBox's corner, not its own centre.** Its `transform-box` is `view-box`, so `rotate={45}` alone swings it round `0 0`: write `<G rotate={45} transformBox="fill-box" transformOrigin="center">`. `transformOrigin` takes a keyword (`'top left'`), a fraction or a percentage, a ÷4 number, or positions as CSS writes them (`'48px 48px'` — a px is a user unit). The `transform` attribute stays for a matrix or an angle that is data. Two more props an interactive drawing needs: `pointerEvents="visibleStroke"` makes an edge's stroke its hit area (over a wide transparent one), and `touchAction="none"` on the pressed element keeps a finger's drag from panning the page.
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: box-kite
3
- description: "@box-kite/react (Box Kite) expert — the runtime CSS-in-JS library whose Box component takes 235 typed CSS props and writes the CSS itself. Use for any work with Box props, the Flex/Grid/Button/Textbox/Dropdown/DataGrid components, themes, Box.extend() or Box.components() — and read it before writing props, because the prop names collide with Tailwind's and Chakra's while the numbers mean different things. Also covers installing and updating the package."
3
+ description: "@box-kite/react (Box Kite) expert — the runtime CSS-in-JS library whose Box component takes CSS properties as typed props and writes the CSS itself. Use for any work with Box props, the Flex/Grid/Button/Textbox/Dropdown/DataGrid components, themes, Box.extend() or Box.components() — and read it before writing props, because the prop names collide with Tailwind's and Chakra's while the numbers mean different things. Also covers installing and updating the package."
4
4
  license: MIT
5
5
  ---
6
6
 
7
7
  # @box-kite/react
8
8
 
9
- Runtime CSS-in-JS. `Box` takes 235 typed CSS props and generates the CSS for them at runtime — no stylesheet, no class name to invent, and two elements written with the same value share one class.
9
+ Runtime CSS-in-JS. `Box` takes CSS properties as typed props and generates the CSS for them at runtime — no stylesheet, no class name to invent, and two elements written with the same value share one class.
10
10
 
11
11
  ## This library is not in your training data
12
12
 
@@ -21,16 +21,16 @@ It was renamed at 1.0.0 (September 2026) and its prop surface nearly doubled on
21
21
 
22
22
  The divider is per prop, so the same number is a different length depending on which prop it is written on — `p={4}` is 16px and `b={4}` is 4px. Every row here is what the engine emitted for that example, not what a doc claims about it:
23
23
 
24
- | Prop | Which props | Written | Emitted |
25
- | ------------------- | ---------------------------------------------------------- | -------------------------- | ---------------------------- |
26
- | `p` | spacing — `p`/`m`/`gap`/`px`/`py`/`pt`… all of them | `p={4}` | `padding: 1rem` |
27
- | `width` | sizing — `width`/`height` and the four `min`/`max` props | `width={4}` | `width: 1rem` |
28
- | `borderRadius` | the corner radius, on the spacing scale | `borderRadius={4}` | `border-radius: 1rem` |
29
- | `b` | border width — `b`/`bx`/`by`/`bt`/`bs`… | `b={4}` | `border-width: 4px` |
30
- | `fontSize` | the one that is ÷16 | `fontSize={14}` | `font-size: 0.875rem` |
31
- | `lineHeight` | and `letterSpacing` | `lineHeight={24}` | `line-height: 24px` |
32
- | `strokeWidth` | SVG lengths — and the geometry props `cx`/`cy`/`r`/`x`/`y` | `strokeWidth={4}` | `stroke-width: 4` |
33
- | `animationDuration` | every animation and transition time | `animationDuration={1100}` | `animation-duration: 1100ms` |
24
+ | Prop | Which props | Written | Emitted |
25
+ | ------------------- | ---------------------------------------------------------- | -------------------------- | ---------------------------------------- |
26
+ | `p` | spacing — `p`/`m`/`gap`/`px`/`py`/`pt`… all of them | `p={4}` | `padding: 1rem` |
27
+ | `width` | sizing — `width`/`height` and the four `min`/`max` props | `width={4}` | `width: 1rem` |
28
+ | `borderRadius` | the corner radius, on the spacing scale | `borderRadius={4}` | `border-radius: 1rem` |
29
+ | `b` | border width — `b`/`bx`/`by`/`bt`/`bs`… | `b={4}` | `border-width: 4px` |
30
+ | `fontSize` | the one that is ÷16 | `fontSize={14}` | `font-size: 0.875rem` |
31
+ | `lineHeight` | and `letterSpacing` | `lineHeight={24}` | `line-height: 24px` |
32
+ | `strokeWidth` | SVG lengths — and the geometry props `cx`/`cy`/`r`/`x`/`y` | `strokeWidth={4}` | `--boxStrokeWidth: 4px; stroke-width: 4` |
33
+ | `animationDuration` | every animation and transition time | `animationDuration={1100}` | `animation-duration: 1100ms` |
34
34
 
35
35
  ## Read this next
36
36
 
@@ -40,7 +40,7 @@ The divider is per prop, so the same number is a different length depending on w
40
40
  | [`references/components.md`](references/components.md) — Components reference | choosing a component, or wiring a Dropdown, a Select or a DataGrid |
41
41
  | [`references/extending.md`](references/extending.md) — Extending Box | a value used twice, or a prop that does not exist yet |
42
42
  | [`references/patterns.md`](references/patterns.md) — Patterns and the rest of the surface | server rendering, the behaviour hooks, portals, form controls, the tooltip |
43
- | `node_modules/@box-kite/react/docs/props.md` | one prop exactly: all 235, the CSS each writes, one measured example |
43
+ | `node_modules/@box-kite/react/docs/props.md` | one prop exactly: every one, the CSS each writes, one measured example |
44
44
  | `node_modules/@box-kite/react/BOX_KITE_AI_CONTEXT.md` | the long-form reference, when a reference above is not enough |
45
45
 
46
46
  With the package not installed, the prop reference is https://www.box-kite.dev/props.md, and https://www.box-kite.dev/llms.txt indexes every documentation page as markdown beside it.
@@ -87,7 +87,7 @@ Detect the package manager from the lock file rather than asking:
87
87
  7. **Border width and lineHeight are direct px**: `b={1}` → 1px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px
88
88
  8. **A gradient fill is a value, not an attribute**: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"` — so it can be themed and hovered. **A dashboard shape is `components/chart`**: `<Sparkline data={[…]}/>`, `<ProgressRing value={0.6}/>`, `<Gauge>`, `<MiniDonut>` — Box props, no chart library
89
89
  9. **A CSS variable is a prop too**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` on the element and everything inside it — the answer for markup this library does not render. **A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>`** (`components/chart`), which declares `--chart-1..6` in both themes and one `--color-<series>` per series, so `<Line stroke="var(--color-revenue)"/>` is all the chart ever says about colour. A record names its own paints, as anything `fill` takes: `series={{ done: 'ui-primary', open: 'ui-primary/35' }}` works with a `Box.extend()` token too.
90
- 10. **SVG lengths have no divider and no unit**: `strokeWidth={2}` → `stroke-width: 2` (user units), same for `strokeDasharray`/`strokeDashoffset` and the geometry props `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y`. `<Rect width={40} height={40}>` and `<Path d="M…">` take those as the SVG attributes they are — the Box props of those names mean something else
90
+ 10. **SVG lengths have no divider**: `strokeWidth={2}` → `stroke-width: 2` (user units), same for `strokeDasharray`/`strokeDashoffset`, and the geometry props `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y` write the number as `px`, which is a user unit (`r={20}` → `r: 20px`) — Firefox and Safari drop a bare number there. `<Rect width={40} height={40}>` and `<Path d="M…">` take those as the SVG attributes they are — the Box props of those names mean something else
91
91
  11. **HTML attributes go in `props` prop**: `<Link props={{ href: '/about' }}>` not `<Link href>`
92
92
  12. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
93
93
  13. **An animation is a prop**: `animation="spin|pulse|bounce|ping"` (keyframes included, and they stop under reduced motion on their own), `Box.keyframes({ name: { from, to } })` for your own — its steps are Box props. **Times are milliseconds**: `animationDuration={1100}`, `transitionDelay={150}`. `transition` takes a group (`colors`, `transform`, `opacity`, `shadow`, `size`, `filter`), and `translateX`/`translateY`/`rotate`/`scale` are longhands that compose. **A spring is two props**: `transitionTimingFunction="spring-bouncy"` with `transitionDuration="spring-bouncy"` (also `spring`, `spring-gentle`, `spring-snappy`; `Box.spring({ stiffness, damping })` for your own)
@@ -172,3 +172,5 @@ Full reference: `references/` beside this file, and `docs/props.md` inside the p
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
173
 
174
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.
175
+
176
+ 56. **An SVG shape turns about the viewBox's corner, not its own centre.** Its `transform-box` is `view-box`, so `rotate={45}` alone swings it round `0 0`: write `<G rotate={45} transformBox="fill-box" transformOrigin="center">`. `transformOrigin` takes a keyword (`'top left'`), a fraction or a percentage, a ÷4 number, or positions as CSS writes them (`'48px 48px'` — a px is a user unit). The `transform` attribute stays for a matrix or an angle that is data. Two more props an interactive drawing needs: `pointerEvents="visibleStroke"` makes an edge's stroke its hit area (over a wide transparent one), and `touchAction="none"` on the pressed element keeps a finger's drag from panning the page.
@@ -15,9 +15,9 @@ Every prop by category, the six kinds of nesting, and the theme system. Read thi
15
15
 
16
16
  **SVG text**: `textAnchor` (`start`/`middle`/`end` — which part of a label sits on its `x`) and `dominantBaseline` (`alphabetic`/`central`/`hanging`/… — which part sits on its `y`). `textAnchor` is inherited; `dominantBaseline` is not, so like `vectorEffect` its rule targets the element _and_ its descendants and still works on the `<svg>`.
17
17
 
18
- **SVG geometry** (SVG 2, all **user units — no divider**, or a percentage): `cx`/`cy` (`<circle>`, `<ellipse>`), `r` (`<circle>`), `rx`/`ry` (`<ellipse>` radii, `<rect>` corners; also `auto`), `x`/`y` (`<rect>`, `<image>`, `<use>`, `<foreignObject>`, nested `<svg>` — **not** `<text>`). They are real CSS, so they transition: `<Circle r={38} hover={{ r: 40 }} />` is a whole animation. Not inherited — put them on the shape. A `<rect>`'s `width`/`height` are NOT in this family (those prop names are the ÷4 layout scale), which is why `<Rect>` claims them back as its own attributes — `<Rect width={40} height={40} />`. A path's `d` stays an attribute too (no Safari support), and `<Path d="M…" />` is how you write it.
18
+ **SVG geometry** (SVG 2, all **user units — no divider**, written as `px` because Firefox and Safari drop a bare number; or a percentage): `cx`/`cy` (`<circle>`, `<ellipse>`), `r` (`<circle>`), `rx`/`ry` (`<ellipse>` radii, `<rect>` corners; also `auto`), `x`/`y` (`<rect>`, `<image>`, `<use>`, `<foreignObject>`, nested `<svg>` — **not** `<text>`). They are real CSS, so they transition: `<Circle r={38} hover={{ r: 40 }} />` is a whole animation. Not inherited — put them on the shape. A `<rect>`'s `width`/`height` are NOT in this family (those prop names are the ÷4 layout scale), which is why `<Rect>` claims them back as its own attributes — `<Rect width={40} height={40} />`. A path's `d` stays an attribute too (no Safari support), and `<Path d="M…" />` is how you write it.
19
19
 
20
- **SVG elements** (`@box-kite/react/components/svg`, 20 components, server-safe): `Svg`, `G`, `Defs`, `Path`, `Circle`, `Ellipse`, `Rect`, `Line`, `Polyline`, `Polygon`, `SvgText`, `TSpan`, `LinearGradient`, `RadialGradient`, `Stop`, `ClipPath`, `Mask`, `Use`, `SvgSymbol`, `Marker` — so a drawing never writes `tag`. Each one is a Box and takes every prop above. **Each also settles the names SVG and Box both use, for its own element**: `Path`'s `d` is path data, `Rect`'s `width`/`height` are user units, `SvgText`/`TSpan`'s `x`/`y`/`dx`/`dy` are attributes (CSS geometry does not apply to text), `RadialGradient`'s `cx`/`cy`/`r` are attributes (nor to a gradient) — while `Circle`'s `cx` stays CSS and transitions. `transform` is a prop on every shape and group. `Svg` takes `viewBox`/`preserveAspectRatio`/`width`/`height` as attributes (so no ÷4 layout `width` on it) and a `label` prop: no label means `aria-hidden`, a label means `role="img"` with that name. A paint server is a value, not an attribute: `fill="url(#sky)"`, `clipPath="url(#frame)"` — themed and hoverable like any other paint. A themed gradient stop is `<Stop stopColor="currentColor" color="amber-300" />`. `BaseSvg` is deprecated — it is `Svg` with a 24×24 preset.
20
+ **SVG elements** (`@box-kite/react/components/svg`, 20 components, server-safe): `Svg`, `G`, `Defs`, `Path`, `Circle`, `Ellipse`, `Rect`, `Line`, `Polyline`, `Polygon`, `SvgText`, `TSpan`, `LinearGradient`, `RadialGradient`, `Stop`, `ClipPath`, `Mask`, `Use`, `SvgSymbol`, `Marker` — so a drawing never writes `tag`. Each one is a Box and takes every prop above. **Each also settles the names SVG and Box both use, for its own element**: `Path`'s `d` is path data, `Rect`'s `width`/`height` are user units, `SvgText`/`TSpan`'s `x`/`y`/`dx`/`dy` are attributes (CSS geometry does not apply to text), `RadialGradient`'s `cx`/`cy`/`r` are attributes (nor to a gradient) — while `Circle`'s `cx` stays CSS and transitions. `transform` is a prop on every shape and group, for a matrix or an angle that is data; a fixed turn about a shape's own centre is `rotate={45} transformBox="fill-box" transformOrigin="center"` (an SVG shape's `transform-box` is `view-box`, so `rotate` alone swings it round the viewBox's corner). `pointerEvents="visibleStroke"` makes the stroke the hit area, `touchAction="none"` keeps a finger's drag from panning the page, `paintOrder` takes up to three keywords, and `strokeDasharray="dashed"`/`"dotted"` scale with `strokeWidth`. `Svg` takes `viewBox`/`preserveAspectRatio`/`width`/`height` as attributes (so no ÷4 layout `width` on it) and a `label` prop: no label means `aria-hidden`, a label means `role="img"` with that name. A paint server is a value, not an attribute: `fill="url(#sky)"`, `clipPath="url(#frame)"` — themed and hoverable like any other paint. A themed gradient stop is `<Stop stopColor="currentColor" color="amber-300" />`. `BaseSvg` is deprecated — it is `Svg` with a 24×24 preset.
21
21
 
22
22
  **Icons** (`@box-kite/react/components/icon`, server-safe): `<Icon size={5} color="amber-500" label="Sunny"><Sun /></Icon>` — Box props on an icon somebody else drew, wrapping exactly one element from lucide, Tabler, react-icons, or a raw `<svg>`. It resolves the props to a class and puts that on the icon's own `<svg>`, so it knows no set's API: `size` is the ÷4 scale (`size={6}` is 24px, the default; an icon set's own `size` counts in pixels, so `size={20}` becomes `size={5}`) and it lands in the _class_, where a CSS declaration outranks the `width`/`height` attributes the set writes for itself. `strokeWidth` is likewise the ordinary Box prop, so it can change on hover or at a breakpoint. No `label` means `aria-hidden`, a `label` means `role="img"` — the same rule `Svg` follows. **Prefer `Svg` over wrapping one in `Icon`**: `Svg` takes these props directly. (Wrapping works — `Icon` asks the child which convention it follows and routes `role`/`aria-label` into `props` for a component of ours — but it is a layer nobody needs.) The hook underneath is public — `useClassNames(props)` from the main entry returns `{ className, styles }` for anything Box cannot render (a `motion.div`, a `NavLink`); render `styles` beside the element (it is defined in element mode only). **Beyond lucide**: Iconify's 300k+ icons in 200+ sets reach the same `<Icon>`, the choice being only when the icon becomes markup — paste one icon's SVG (no dependency, server-renders); `unplugin-icons` for a set at build time (`npm i -D unplugin-icons @iconify-json/<set> @svgr/core @svgr/plugin-jsx`, plugin with `{ compiler: 'jsx', jsx: 'react' }`, `/// <reference types="unplugin-icons/types/react" />` in a `.d.ts`, then `import SiGithub from '~icons/simple-icons/github'`); or `@iconify/react` when the name is data — it fetches in the browser, so it is a client component and the server sends no icon. Turbopack runs no unplugin: under Next.js 16 the build-time recipe needs `next build --webpack`.
23
23
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: @box-kite/react (Box Kite): the runtime CSS-in-JS library whose Box component takes 235 typed CSS props. Read before writing Box props, a component, a theme or an extension — the prop names collide with Tailwind and Chakra while the numbers mean different things.
2
+ description: @box-kite/react (Box Kite): the runtime CSS-in-JS library whose Box component takes CSS properties as typed props. Read before writing Box props, a component, a theme or an extension — the prop names collide with Tailwind and Chakra while the numbers mean different things.
3
3
  globs: **/*.tsx,**/*.jsx,**/*.ts
4
4
  alwaysApply: false
5
5
  ---
@@ -22,7 +22,7 @@ This library is not in your training data: it was renamed at 1.0.0 (September 20
22
22
  7. **Border width and lineHeight are direct px**: `b={1}` → 1px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px
23
23
  8. **A gradient fill is a value, not an attribute**: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"` — so it can be themed and hovered. **A dashboard shape is `components/chart`**: `<Sparkline data={[…]}/>`, `<ProgressRing value={0.6}/>`, `<Gauge>`, `<MiniDonut>` — Box props, no chart library
24
24
  9. **A CSS variable is a prop too**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` on the element and everything inside it — the answer for markup this library does not render. **A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>`** (`components/chart`), which declares `--chart-1..6` in both themes and one `--color-<series>` per series, so `<Line stroke="var(--color-revenue)"/>` is all the chart ever says about colour. A record names its own paints, as anything `fill` takes: `series={{ done: 'ui-primary', open: 'ui-primary/35' }}` works with a `Box.extend()` token too.
25
- 10. **SVG lengths have no divider and no unit**: `strokeWidth={2}` → `stroke-width: 2` (user units), same for `strokeDasharray`/`strokeDashoffset` and the geometry props `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y`. `<Rect width={40} height={40}>` and `<Path d="M…">` take those as the SVG attributes they are — the Box props of those names mean something else
25
+ 10. **SVG lengths have no divider**: `strokeWidth={2}` → `stroke-width: 2` (user units), same for `strokeDasharray`/`strokeDashoffset`, and the geometry props `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y` write the number as `px`, which is a user unit (`r={20}` → `r: 20px`) — Firefox and Safari drop a bare number there. `<Rect width={40} height={40}>` and `<Path d="M…">` take those as the SVG attributes they are — the Box props of those names mean something else
26
26
  11. **HTML attributes go in `props` prop**: `<Link props={{ href: '/about' }}>` not `<Link href>`
27
27
  12. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
28
28
  13. **An animation is a prop**: `animation="spin|pulse|bounce|ping"` (keyframes included, and they stop under reduced motion on their own), `Box.keyframes({ name: { from, to } })` for your own — its steps are Box props. **Times are milliseconds**: `animationDuration={1100}`, `transitionDelay={150}`. `transition` takes a group (`colors`, `transform`, `opacity`, `shadow`, `size`, `filter`), and `translateX`/`translateY`/`rotate`/`scale` are longhands that compose. **A spring is two props**: `transitionTimingFunction="spring-bouncy"` with `transitionDuration="spring-bouncy"` (also `spring`, `spring-gentle`, `spring-snappy`; `Box.spring({ stiffness, damping })` for your own)
@@ -107,3 +107,5 @@ Full reference: `node_modules/@box-kite/react/docs/props.md` in the project, or
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
108
 
109
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.
110
+
111
+ 56. **An SVG shape turns about the viewBox's corner, not its own centre.** Its `transform-box` is `view-box`, so `rotate={45}` alone swings it round `0 0`: write `<G rotate={45} transformBox="fill-box" transformOrigin="center">`. `transformOrigin` takes a keyword (`'top left'`), a fraction or a percentage, a ÷4 number, or positions as CSS writes them (`'48px 48px'` — a px is a user unit). The `transform` attribute stays for a matrix or an angle that is data. Two more props an interactive drawing needs: `pointerEvents="visibleStroke"` makes an edge's stroke its hit area (over a wide transparent one), and `touchAction="none"` on the pressed element keeps a finger's drag from panning the page.
package/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @box-kite/react — read this before writing any of it
2
2
 
3
- **This library is not in your training data.** It was renamed at 1.0.0 (September 2026) and its prop surface nearly doubled on the way, so a plausible-looking memory of it is a memory of a different library. Its prop names collide with Tailwind's and Chakra's while the numbers mean different things, which is the failure mode to expect: code that compiles and is laid out wrong. Read the rules below, then `docs/props.md` for the 235 props themselves.
3
+ **This library is not in your training data.** It was renamed at 1.0.0 (September 2026) and its prop surface nearly doubled on the way, so a plausible-looking memory of it is a memory of a different library. Its prop names collide with Tailwind's and Chakra's while the numbers mean different things, which is the failure mode to expect: code that compiles and is laid out wrong. Read the rules below, then `docs/props.md` for the 238 props themselves.
4
4
 
5
5
  ## Where the files are
6
6
 
@@ -54,7 +54,7 @@ Every value there is a prop, and every prop value becomes one class the engine i
54
54
  7. **Border width and lineHeight are direct px**: `b={1}` → 1px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px
55
55
  8. **A gradient fill is a value, not an attribute**: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"` — so it can be themed and hovered. **A dashboard shape is `components/chart`**: `<Sparkline data={[…]}/>`, `<ProgressRing value={0.6}/>`, `<Gauge>`, `<MiniDonut>` — Box props, no chart library
56
56
  9. **A CSS variable is a prop too**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` on the element and everything inside it — the answer for markup this library does not render. **A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>`** (`components/chart`), which declares `--chart-1..6` in both themes and one `--color-<series>` per series, so `<Line stroke="var(--color-revenue)"/>` is all the chart ever says about colour. A record names its own paints, as anything `fill` takes: `series={{ done: 'ui-primary', open: 'ui-primary/35' }}` works with a `Box.extend()` token too.
57
- 10. **SVG lengths have no divider and no unit**: `strokeWidth={2}` → `stroke-width: 2` (user units), same for `strokeDasharray`/`strokeDashoffset` and the geometry props `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y`. `<Rect width={40} height={40}>` and `<Path d="M…">` take those as the SVG attributes they are — the Box props of those names mean something else
57
+ 10. **SVG lengths have no divider**: `strokeWidth={2}` → `stroke-width: 2` (user units), same for `strokeDasharray`/`strokeDashoffset`, and the geometry props `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y` write the number as `px`, which is a user unit (`r={20}` → `r: 20px`) — Firefox and Safari drop a bare number there. `<Rect width={40} height={40}>` and `<Path d="M…">` take those as the SVG attributes they are — the Box props of those names mean something else
58
58
  11. **HTML attributes go in `props` prop**: `<Link props={{ href: '/about' }}>` not `<Link href>`
59
59
  12. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
60
60
  13. **An animation is a prop**: `animation="spin|pulse|bounce|ping"` (keyframes included, and they stop under reduced motion on their own), `Box.keyframes({ name: { from, to } })` for your own — its steps are Box props. **Times are milliseconds**: `animationDuration={1100}`, `transitionDelay={150}`. `transition` takes a group (`colors`, `transform`, `opacity`, `shadow`, `size`, `filter`), and `translateX`/`translateY`/`rotate`/`scale` are longhands that compose. **A spring is two props**: `transitionTimingFunction="spring-bouncy"` with `transitionDuration="spring-bouncy"` (also `spring`, `spring-gentle`, `spring-snappy`; `Box.spring({ stiffness, damping })` for your own)
@@ -139,3 +139,5 @@ Full reference: `docs/props.md` and `BOX_KITE_AI_CONTEXT.md`, in this package.
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
140
 
141
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.
142
+
143
+ 56. **An SVG shape turns about the viewBox's corner, not its own centre.** Its `transform-box` is `view-box`, so `rotate={45}` alone swings it round `0 0`: write `<G rotate={45} transformBox="fill-box" transformOrigin="center">`. `transformOrigin` takes a keyword (`'top left'`), a fraction or a percentage, a ÷4 number, or positions as CSS writes them (`'48px 48px'` — a px is a user unit). The `transform` attribute stays for a matrix or an angle that is data. Two more props an interactive drawing needs: `pointerEvents="visibleStroke"` makes an edge's stroke its hit area (over a wide transparent one), and `touchAction="none"` on the pressed element keeps a finger's drag from panning the page.
package/BOX_AI_CONTEXT.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @box-kite/react - AI Assistant Context
2
2
 
3
- Runtime CSS-in-JS library. `Box` component accepts 235 CSS props and generates CSS classes at runtime. Same prop values share a single class.
3
+ Runtime CSS-in-JS library. `Box` component accepts CSS properties as typed props and generates CSS classes at runtime. Same prop values share a single class.
4
4
 
5
5
  ---
6
6
 
@@ -191,7 +191,7 @@ All sizing, spacing, and positioning props also accept percentage strings: `p="5
191
191
  | `fieldSizing` | field-sizing | `'content'` — a control that grows with what is typed into it, no JavaScript measuring anything — or `'fixed'` |
192
192
  | `scrollbarGutter` / `scrollbarWidth` / `scrollbarColor` | scrollbar-gutter / -width / -color | `'stable'` / `'stable both-edges'` reserve the scrollbar's space before there is one, so nothing shifts when content overflows. `scrollbarWidth`: `'auto'`/`'thin'`/`'none'`; `scrollbarColor`: `[thumb, track]`, each a token, a `Box.extend()` colour or either with `/50` |
193
193
  | `willChange` | will-change | `'transform'`, `'opacity'`, `'filter'`, `'scroll-position'`, `'contents'`, `'auto'`. A hint with a real cost (a promoted layer holds memory), so it belongs on the few elements that animate, not on a list |
194
- | `cursor` / `pointerEvents` / `userSelect` | misc | string values. For `transition`, `animation` and the transform props see _Animation and transitions_ below |
194
+ | `cursor` / `pointerEvents` / `userSelect` / `touchAction` | misc | string values. For `transition`, `animation` and the transform props see _Animation and transitions_ below |
195
195
 
196
196
  ### Anchor positioning — a floating layer with no JavaScript
197
197
 
@@ -468,26 +468,27 @@ one, nested wherever a prop nests, rendered on a server.
468
468
 
469
469
  Twenty-three SVG properties, on any Box, plus twenty element components in `components/svg` — `Svg`, `G`, `Defs`, `Path`, `Circle`, `Ellipse`, `Rect`, `Line`, `Polyline`, `Polygon`, `SvgText`, `TSpan`, `LinearGradient`, `RadialGradient`, `Stop`, `ClipPath`, `Mask`, `Use`, `SvgSymbol`, `Marker`. **Never `<Box tag="path">`.** For an icon from somebody else's set, `<Icon>` (`components/icon`) — see below. (`BaseSvg` is deprecated: it is `Svg` with a 24×24 preset.)
470
470
 
471
- | Prop | CSS Property | Accepts |
472
- | ------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
473
- | `fill` / `stroke` | fill / stroke | any colour variable, or `'none'` |
474
- | `fillOpacity` / `strokeOpacity` | fill-opacity / stroke-opacity | `0`–`1` in tenths, same scale as `opacity` |
475
- | `fillRule` | fill-rule | `'nonzero'`, `'evenodd'` |
476
- | `strokeWidth` | stroke-width | number, **user units — no divider** (`strokeWidth={2}` → `2`) |
477
- | `strokeLinecap` | stroke-linecap | `'butt'`, `'round'`, `'square'` |
478
- | `strokeLinejoin` | stroke-linejoin | `'miter'`, `'round'`, `'bevel'` |
479
- | `strokeMiterlimit` | stroke-miterlimit | number, 1 or greater |
480
- | `strokeDasharray` | stroke-dasharray | number (`{12}` = dash 12, gap 12) or the pattern as a string (`"12 4"`) |
481
- | `strokeDashoffset` | stroke-dashoffset | number, or a percentage of the path length (`'40%'`) |
482
- | `paintOrder` | paint-order | `'normal'`, `'fill'`, `'stroke'`, `'markers'` |
483
- | `vectorEffect` | vector-effect | `'none'`, `'non-scaling-stroke'` |
484
- | `shapeRendering` | shape-rendering | `'auto'`, `'optimizeSpeed'`, `'crispEdges'`, `'geometricPrecision'` |
485
- | `textAnchor` | text-anchor | `'start'`, `'middle'`, `'end'` — which part of a label sits on its `x` |
486
- | `dominantBaseline` | dominant-baseline | `'auto'`, `'alphabetic'`, `'central'`, `'middle'`, `'hanging'`, `'text-top'`, `'text-bottom'`, `'ideographic'`, `'mathematical'` |
487
- | `cx` / `cy` | cx / cy | number in user units, or a percentage — `<circle>`, `<ellipse>` |
488
- | `r` | r | number in user units, or a percentage — `<circle>` |
489
- | `rx` / `ry` | rx / ry | number, a percentage, or `'auto'` — `<ellipse>` radii, `<rect>` corners |
490
- | `x` / `y` | x / y | number in user units, or a percentage — `<rect>`, `<image>`, `<use>`, `<foreignObject>`, nested `<svg>`. NOT `<text>` |
471
+ | Prop | CSS Property | Accepts |
472
+ | ------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
473
+ | `fill` / `stroke` | fill / stroke | any colour variable, or `'none'` |
474
+ | `fillOpacity` / `strokeOpacity` | fill-opacity / stroke-opacity | `0`–`1` in tenths, same scale as `opacity` |
475
+ | `fillRule` | fill-rule | `'nonzero'`, `'evenodd'` |
476
+ | `strokeWidth` | stroke-width | number, **user units — no divider** (`strokeWidth={2}` → `2`) |
477
+ | `strokeLinecap` | stroke-linecap | `'butt'`, `'round'`, `'square'` |
478
+ | `strokeLinejoin` | stroke-linejoin | `'miter'`, `'round'`, `'bevel'` |
479
+ | `strokeMiterlimit` | stroke-miterlimit | number, 1 or greater |
480
+ | `strokeDasharray` | stroke-dasharray | number (`{12}` = dash 12, gap 12), the pattern as a string (`"12 4"`), or `'dashed'`/`'dotted'`, which scale with `strokeWidth` |
481
+ | `strokeDashoffset` | stroke-dashoffset | number, or a percentage of the path length (`'40%'`) |
482
+ | `paintOrder` | paint-order | `'normal'`, or up to three of `'fill'`/`'stroke'`/`'markers'` in paint order (`'stroke fill markers'`) |
483
+ | `pointerEvents` | pointer-events | `'none'`, `'auto'`, `'all'`, and which part of a shape takes the press: `'visiblePainted'`, `'visibleFill'`, `'visibleStroke'`, `'visible'`, `'painted'`, `'fill'`, `'stroke'`, `'bounding-box'` (not in Firefox) |
484
+ | `vectorEffect` | vector-effect | `'none'`, `'non-scaling-stroke'` |
485
+ | `shapeRendering` | shape-rendering | `'auto'`, `'optimizeSpeed'`, `'crispEdges'`, `'geometricPrecision'` |
486
+ | `textAnchor` | text-anchor | `'start'`, `'middle'`, `'end'` — which part of a label sits on its `x` |
487
+ | `dominantBaseline` | dominant-baseline | `'auto'`, `'alphabetic'`, `'central'`, `'middle'`, `'hanging'`, `'text-top'`, `'text-bottom'`, `'ideographic'`, `'mathematical'` |
488
+ | `cx` / `cy` | cx / cy | number in user units, or a percentage — `<circle>`, `<ellipse>` |
489
+ | `r` | r | number in user units, or a percentage — `<circle>` |
490
+ | `rx` / `ry` | rx / ry | number, a percentage, or `'auto'` — `<ellipse>` radii, `<rect>` corners |
491
+ | `x` / `y` | x / y | number in user units, or a percentage — `<rect>`, `<image>`, `<use>`, `<foreignObject>`, nested `<svg>`. NOT `<text>` |
491
492
 
492
493
  ```tsx
493
494
  import { Circle, Path, Rect, Svg, SvgText } from '@box-kite/react/components/svg';
@@ -515,17 +516,18 @@ import { Circle, Path, Rect, Svg, SvgText } from '@box-kite/react/components/svg
515
516
  <Rect x={16} y={32} width={24} height={64} rx={3} fill="sky-600" />
516
517
  ```
517
518
 
518
- **Nine things to get right:**
519
+ **Ten things to get right:**
519
520
 
520
- 1. **No divider on SVG lengths.** `strokeWidth`, `strokeDasharray`, `strokeDashoffset` and `strokeMiterlimit` pass the number through unchanged — they are measured in the coordinate system the `viewBox` sets up.
521
+ 1. **No divider on SVG lengths.** `strokeWidth`, `strokeDasharray`, `strokeDashoffset` and `strokeMiterlimit` pass the number through unchanged — they are measured in the coordinate system the `viewBox` sets up. The geometry props (`cx`/`cy`/`r`/`rx`/`ry`/`x`/`y`) write that number as `px` — a user unit in SVG — because Firefox and Safari drop a bare number in them.
521
522
  2. **Inheritance does the work.** Put `fill`/`stroke`/`strokeWidth` on the `<svg>`, not on every shape. The exception is a shape carrying its own `fill=` attribute — a presentation attribute on an element beats a value inherited from its parent, so style that shape directly.
522
523
  3. **`vectorEffect` and `dominantBaseline` are not inherited**, so their rules target the element _and_ its descendants (`.cls, .cls *`). `vectorEffect="non-scaling-stroke"` on the `<svg>` keeps a hairline one pixel wide at any scale; `dominantBaseline="central"` on the `<svg>` reaches every label inside it.
523
524
  4. **Geometry belongs to the shape, and it transitions.** `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y` are real CSS in SVG 2, so a gauge or a growing bar is a pseudo-class and no JavaScript. They are not inherited and should not be — set them on the shape.
524
- 5. **Each component settles the names SVG and Box both use, for its own element.** On `Path`, `d` is path data (not `flexDirection`); on `Rect`, `width`/`height` are user units (not the ÷4 layout scale); on `SvgText` and `TSpan`, `x`/`y`/`dx`/`dy` are attributes, because the CSS geometry properties do not apply to text; on `RadialGradient`, `cx`/`cy`/`r` are attributes, because they do not apply to a gradient either — while on `Circle` the same names stay CSS and transition. `transform` is a prop on every shape and group (`transform="rotate(-90 48 48)"` carries its own centre, which the CSS `rotate` prop cannot).
525
+ 5. **Each component settles the names SVG and Box both use, for its own element.** On `Path`, `d` is path data (not `flexDirection`); on `Rect`, `width`/`height` are user units (not the ÷4 layout scale); on `SvgText` and `TSpan`, `x`/`y`/`dx`/`dy` are attributes, because the CSS geometry properties do not apply to text; on `RadialGradient`, `cx`/`cy`/`r` are attributes, because they do not apply to a gradient either — while on `Circle` the same names stay CSS and transition. `transform` is a prop on every shape and group, for a matrix or an angle that is data (`transform="rotate(-90 48 48)"`); a fixed turn about the shape's own centre is CSS (item 10).
525
526
  6. **`Svg` sizes with attributes and names itself with `label`.** `viewBox`, `preserveAspectRatio`, `width` and `height` are the SVG attributes (`width="100%"`, `width={200}`), so the ÷4 layout `width`/`height` are not available on it. No `label` means `aria-hidden` — decoration, which is what most SVG is; `label` makes it `role="img"` with that name. A role or `aria-*` of your own in `props` wins over both.
526
527
  7. **An icon from a set is not SVG you draw — it is `<Icon>`.** `<Icon size={5} color="amber-500" label="Sunny"><Sun /></Icon>` from `components/icon`, wrapping one element from lucide, Tabler, react-icons, or a raw `<svg>`. `size` is the ÷4 scale (`size={6}` is 24px, the default) and lands in the _class_, where a CSS declaration outranks the `width`/`height` attributes the icon set writes — which is why `Icon` needs to know no set's API. `strokeWidth` is the same: an ordinary Box prop, so it can change on hover. No `label` means `aria-hidden`; a `label` means `role="img"`. **Reach for `Svg` rather than wrapping one in `Icon`** — `Svg` takes these props directly. (Wrapping one does work: `Icon` asks the child which convention it follows and routes `role`/`aria-label` into `props` for a component of ours. It is still a layer nobody needs.)
527
528
  8. **A paint server is a value, not an attribute.** `fill`, `stroke` and `clipPath` take `url(#id)` (a `<LinearGradient>`, a pattern, a `<ClipPath>` the document defines) and `var(--name)` (a variable somebody else declared) beside the colour tokens — so `fill="url(#sky)"` and `clipPath="url(#frame)"`, and both can differ per theme, on `hover` and per breakpoint. Only those two shapes are accepted: anything else emits no rule at all rather than a broken declaration. A gradient stop is still themed the same way — `<Stop stopColor="currentColor" color="amber-300" />`.
528
529
  9. **An icon outside lucide comes through the same `<Icon>`.** Iconify carries 300,000+ icons in 200-plus sets, and the choice is only about _when_ the icon becomes markup. One icon: copy its SVG and paste it into an `<Icon>` — no dependency, renders on a server. A set: `unplugin-icons` compiles `~icons/<set>/<name>` into a component at build time from an `@iconify-json/<set>` devDependency (`npm i -D unplugin-icons @iconify-json/<set> @svgr/core @svgr/plugin-jsx`, the plugin with `{ compiler: 'jsx', jsx: 'react' }`, and `/// <reference types="unplugin-icons/types/react" />` in a `.d.ts`) — nothing fetched at runtime, server-renders. A name that is data (from a CMS, from a user): `@iconify/react`'s `<Icon icon="set:name"/>` inside our `<Icon>` — it fetches in the browser, so it is a client component and the server sends no icon. **Turbopack runs no unplugin**, so under Next.js 16 the build-time recipe needs `next build --webpack`; the other two work as they are.
530
+ 10. **A shape turns about the viewBox's corner unless told otherwise.** An SVG element's `transform-box` is `view-box`, so `rotate={45}` alone swings a shape round `0 0`. `<G rotate={45} transformBox="fill-box" transformOrigin="center">` turns it about its own centre (measured in Chrome, Firefox and WebKit). `transformOrigin` takes a keyword (`'center'`, `'top left'`), a fraction or a percentage, a number on the ÷4 scale, or two or three positions as CSS writes them (`'48px 48px'` — in SVG a px is a user unit). An edge you can press is `pointerEvents="visibleStroke"` over a wide transparent stroke, and a drag on a touch screen needs `touchAction="none"` on the element pressed, or the page pans instead.
529
531
 
530
532
  ---
531
533
 
@@ -534,20 +536,21 @@ import { Circle, Path, Rect, Svg, SvgText } from '@box-kite/react/components/svg
534
536
  Every Box already transitions `all` its properties over `--transitionTime` (0.25s), which is why a
535
537
  `hover` colour fades without being asked. On top of that:
536
538
 
537
- | Prop | CSS Property | Notes |
538
- | ----------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
539
- | `animation` | animation | One of four presets: `'spin'`, `'pulse'`, `'bounce'`, `'ping'`, or `'none'`. Keyframes come with the engine |
540
- | `animationName` | animation-name | A sequence registered with `Box.keyframes()`, a preset name, or a name from any stylesheet |
541
- | `animationDuration` / `animationDelay` | ms | **Milliseconds, no divider**: `animationDuration={1100}` → `1100ms` |
542
- | `animationIterationCount` | number | A number or `'infinite'` |
543
- | `animationDirection` / `animationFillMode` / `animationPlayState` | keywords | `'normal'`/`'reverse'`/`'alternate'`/`'alternate-reverse'`; `'none'`/`'forwards'`/`'backwards'`/`'both'`; `'running'`/`'paused'` |
544
- | `animationTimingFunction` / `transitionTimingFunction` | timing function | Keywords, the four springs (`'spring'`, `'spring-gentle'`, `'spring-bouncy'`, `'spring-snappy'`), or a curve: `'cubic-bezier(0.4, 0, 0.6, 1)'`, `'steps(4, end)'`, `'linear(0, 0.5, 1)'` |
545
- | `transition` | transition-property | `'all'`, `'none'`, or a group: `'colors'`, `'opacity'`, `'shadow'`, `'transform'`, `'size'`, `'filter'` |
546
- | `transitionDuration` / `transitionDelay` | ms | Milliseconds, same as the animation pair. Both duration props also take a spring name — that spring's settling time |
547
- | `transitionBehavior` | transition-behavior | `'normal'` or `'allow-discrete'`, which lets `display`, `overlay` and `content-visibility` transition — the property that makes an element animate _out_ without unmounting |
548
- | `interpolateSize` | interpolate-size | `'numeric-only'` or `'allow-keywords'`, which is what makes `height: auto` animate. Inherited, so it belongs on a container. Chromium-only; elsewhere the size snaps |
549
- | `startingStyle` | `@starting-style` | A nested block of plain props: what they start from the first time the element is styled. `startingStyle={{ opacity: 0, translateY: 2 }}` is an entrance with no JavaScript |
550
- | `translateX` / `translateY` / `rotate` / `scale` / `flip` | transform longhands | Each writes its own CSS property, so they compose. `scale={1.05}` is unitless; `rotate={45}` is degrees |
539
+ | Prop | CSS Property | Notes |
540
+ | ----------------------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
541
+ | `animation` | animation | One of four presets: `'spin'`, `'pulse'`, `'bounce'`, `'ping'`, or `'none'`. Keyframes come with the engine |
542
+ | `animationName` | animation-name | A sequence registered with `Box.keyframes()`, a preset name, or a name from any stylesheet |
543
+ | `animationDuration` / `animationDelay` | ms | **Milliseconds, no divider**: `animationDuration={1100}` → `1100ms` |
544
+ | `animationIterationCount` | number | A number or `'infinite'` |
545
+ | `animationDirection` / `animationFillMode` / `animationPlayState` | keywords | `'normal'`/`'reverse'`/`'alternate'`/`'alternate-reverse'`; `'none'`/`'forwards'`/`'backwards'`/`'both'`; `'running'`/`'paused'` |
546
+ | `animationTimingFunction` / `transitionTimingFunction` | timing function | Keywords, the four springs (`'spring'`, `'spring-gentle'`, `'spring-bouncy'`, `'spring-snappy'`), or a curve: `'cubic-bezier(0.4, 0, 0.6, 1)'`, `'steps(4, end)'`, `'linear(0, 0.5, 1)'` |
547
+ | `transition` | transition-property | `'all'`, `'none'`, or a group: `'colors'`, `'opacity'`, `'shadow'`, `'transform'`, `'size'`, `'filter'` |
548
+ | `transitionDuration` / `transitionDelay` | ms | Milliseconds, same as the animation pair. Both duration props also take a spring name — that spring's settling time |
549
+ | `transitionBehavior` | transition-behavior | `'normal'` or `'allow-discrete'`, which lets `display`, `overlay` and `content-visibility` transition — the property that makes an element animate _out_ without unmounting |
550
+ | `interpolateSize` | interpolate-size | `'numeric-only'` or `'allow-keywords'`, which is what makes `height: auto` animate. Inherited, so it belongs on a container. Chromium-only; elsewhere the size snaps |
551
+ | `startingStyle` | `@starting-style` | A nested block of plain props: what they start from the first time the element is styled. `startingStyle={{ opacity: 0, translateY: 2 }}` is an entrance with no JavaScript |
552
+ | `translateX` / `translateY` / `rotate` / `scale` / `flip` | transform longhands | Each writes its own CSS property, so they compose. `scale={1.05}` is unitless; `rotate={45}` is degrees |
553
+ | `transformOrigin` / `transformBox` | transform-origin / transform-box | The point `rotate`/`scale` turn about (`'center'`, `'top left'`, `'1/2 1/4'`, `'48px 48px'`, a ÷4 number) and the box it is measured in (`'fill-box'` for an SVG shape's own) |
551
554
 
552
555
  ```tsx
553
556
  // A preset: no registration, and it stops under prefers-reduced-motion on its own
@@ -3830,7 +3833,7 @@ allowed.tokens; // the colours, the @keyframes names and the style-tree nodes a
3830
3833
  `DataGrid`'s `def` (required — the columns and the grid-wide flags) and `data`, `DashboardGrid`'s
3831
3834
  `layout`/`defaultLayout`/`columns`, `Widget`'s `empty`, `ChartContainer`'s `series`. A column's nested
3832
3835
  `columns`, and `dataSource`/`onCellEdit`/`rowDetail`/`treeData`, stay out — they are the app's to pass.
3833
- - **The allow-list is the app's.** `catalog()` with no options is all 78 components and all 235 props —
3836
+ - **The allow-list is the app's.** `catalog()` with no options is every component and every prop —
3834
3837
  ~4.6 MB serialized. `include`, `exclude` and `styleProps` are how an app narrows it.
3835
3838
 
3836
3839
  Full reference: `docs/catalog.md`.