@box-kite/react 2.4.0 → 2.4.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.
- package/.claude/rules/box-kite-rules.md +1 -1
- package/.claude/skills/box-kite/SKILL.md +1 -1
- package/.claude/skills/box-kite/references/components.md +7 -0
- package/.claude/skills/box-kite/references/rules.md +2 -2
- package/.claude/skills/box-kite/references/styling.md +5 -3
- package/.cursor/rules/box-kite.mdc +1 -1
- package/AGENTS.md +1 -1
- package/BOX_AI_CONTEXT.md +77 -7
- package/BOX_KITE_AI_CONTEXT.md +77 -7
- package/box.cjs +1 -1
- package/box.d.ts +5 -0
- package/box.mjs +5 -5
- package/catalog.cjs +1 -1
- package/catalog.mjs +53 -18
- package/client.cjs +1 -1
- package/client.mjs +7 -7
- package/components/accordion.cjs +1 -1
- package/components/accordion.mjs +1 -1
- package/components/button.cjs +1 -1
- package/components/button.mjs +1 -1
- package/components/calendar.cjs +1 -1
- package/components/calendar.mjs +1 -1
- package/components/chart.cjs +1 -1
- package/components/chart.mjs +3 -3
- package/components/checkbox.cjs +1 -1
- package/components/checkbox.mjs +1 -1
- package/components/combobox.cjs +1 -1
- package/components/combobox.mjs +70 -70
- package/components/dashboard.cjs +1 -1
- package/components/dashboard.mjs +1 -1
- package/components/dateField.cjs +1 -1
- package/components/dateField.mjs +16 -16
- package/components/datePicker.cjs +1 -1
- package/components/datePicker.mjs +1 -1
- package/components/dateTime.cjs +1 -1
- package/components/dateTime.mjs +1 -1
- package/components/dialog.cjs +1 -1
- package/components/dialog.mjs +1 -1
- package/components/flex.cjs +1 -1
- package/components/flex.mjs +1 -1
- package/components/flow.cjs +1 -0
- package/components/flow.d.ts +56 -0
- package/components/flow.mjs +98 -0
- package/components/form.cjs +1 -1
- package/components/form.mjs +1 -1
- package/components/grid.cjs +1 -1
- package/components/grid.mjs +1 -1
- package/components/icon.cjs +1 -1
- package/components/icon.mjs +1 -1
- package/components/list.cjs +1 -1
- package/components/list.mjs +1 -1
- package/components/menu.cjs +1 -1
- package/components/menu.mjs +1 -1
- package/components/popover.cjs +1 -1
- package/components/popover.mjs +1 -1
- package/components/radioButton.cjs +1 -1
- package/components/radioButton.mjs +1 -1
- package/components/radioGroup.cjs +1 -1
- package/components/radioGroup.mjs +1 -1
- package/components/semantics.cjs +1 -1
- package/components/semantics.d.ts +32 -30
- package/components/semantics.mjs +3 -3
- package/components/skeleton.cjs +1 -1
- package/components/skeleton.mjs +1 -0
- package/components/slider.cjs +1 -1
- package/components/slider.mjs +1 -1
- package/components/sortableList.cjs +1 -1
- package/components/sortableList.mjs +1 -1
- package/components/svg.cjs +1 -1
- package/components/svg.mjs +14 -14
- package/components/swipeItem.cjs +1 -1
- package/components/swipeItem.mjs +1 -1
- package/components/tabs.cjs +1 -1
- package/components/tabs.mjs +1 -1
- package/components/textarea.cjs +1 -1
- package/components/textarea.mjs +1 -1
- package/components/textbox.cjs +1 -1
- package/components/textbox.mjs +1 -1
- package/components/toaster.cjs +1 -1
- package/components/toaster.mjs +1 -1
- package/components/tooltip.cjs +1 -1
- package/components/tooltip.mjs +1 -1
- package/components/virtualList.cjs +1 -1
- package/components/virtualList.mjs +1 -1
- package/components/visuallyHidden.cjs +1 -1
- package/components/visuallyHidden.mjs +1 -1
- package/docs/components.md +1 -0
- package/docs/props.md +1 -1
- package/package.json +43 -29
- package/react/boxClassNames.d.ts +2 -2
- package/react-shared.cjs +1 -1
- package/react-shared.mjs +42 -3
- package/rsc.cjs +1 -1
- package/rsc.d.ts +1 -0
- package/rsc.mjs +2 -2
- package/utils/catalog/catalogContracts.d.ts +0 -10
- package/utils/flow/flowPalette.d.ts +17 -0
- package/utils/flow/flowUtils.d.ts +47 -0
|
@@ -15,7 +15,7 @@ Each rule is what to write and the trap to avoid. The reasons, measurements and
|
|
|
15
15
|
6. **Spacing divider is 4**: `p={4}` → 16px (1rem).
|
|
16
16
|
7. **Border width and lineHeight are direct px**: `b={1}` → 1px, `lineHeight={24}` → 24px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px.
|
|
17
17
|
8. **A paint reference is a value, not an attribute**: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"`. A dashboard shape is `components/chart`: `<Sparkline>`, `<ProgressRing>`, `<Gauge>`, `<MiniDonut>`.
|
|
18
|
-
9. **A CSS variable is a prop**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` for the subtree. A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>` and reads `var(--color-revenue)`; `series` also takes a record of paints.
|
|
18
|
+
9. **A CSS variable is a prop**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` for the subtree. A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>` and reads `var(--color-revenue)`; `series` also takes a record of paints. A React Flow canvas goes in `<FlowContainer>` (`components/flow`) with `colorMode={flowColorMode(theme)}`.
|
|
19
19
|
10. **SVG lengths have no divider**: `strokeWidth={2}` → `stroke-width: 2`; `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y` write px (user units). On `<Rect>`, `width`/`height` are the SVG attributes; on `<Path>`, `d` is path data.
|
|
20
20
|
11. **HTML attributes go in `props`**: `<Link props={{ href: '/about' }}>`, not `<Link href>` — at the top level they typecheck and are dropped. The form controls take their own directly: `<Button type="submit" onClick={…}>`, `<Textbox name="email" type="email" value onChange>`, `<Checkbox name defaultChecked onChange>`.
|
|
21
21
|
12. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%.
|
|
@@ -89,7 +89,7 @@ Each rule is what to write and the trap to avoid. The reasons, measurements and
|
|
|
89
89
|
6. **Spacing divider is 4**: `p={4}` → 16px (1rem).
|
|
90
90
|
7. **Border width and lineHeight are direct px**: `b={1}` → 1px, `lineHeight={24}` → 24px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px.
|
|
91
91
|
8. **A paint reference is a value, not an attribute**: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"`. A dashboard shape is `components/chart`: `<Sparkline>`, `<ProgressRing>`, `<Gauge>`, `<MiniDonut>`.
|
|
92
|
-
9. **A CSS variable is a prop**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` for the subtree. A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>` and reads `var(--color-revenue)`; `series` also takes a record of paints.
|
|
92
|
+
9. **A CSS variable is a prop**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` for the subtree. A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>` and reads `var(--color-revenue)`; `series` also takes a record of paints. A React Flow canvas goes in `<FlowContainer>` (`components/flow`) with `colorMode={flowColorMode(theme)}`.
|
|
93
93
|
10. **SVG lengths have no divider**: `strokeWidth={2}` → `stroke-width: 2`; `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y` write px (user units). On `<Rect>`, `width`/`height` are the SVG attributes; on `<Path>`, `d` is path data.
|
|
94
94
|
11. **HTML attributes go in `props`**: `<Link props={{ href: '/about' }}>`, not `<Link href>` — at the top level they typecheck and are dropped. The form controls take their own directly: `<Button type="submit" onClick={…}>`, `<Textbox name="email" type="email" value onChange>`, `<Checkbox name defaultChecked onChange>`.
|
|
95
95
|
12. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%.
|
|
@@ -30,6 +30,7 @@ Which component replaces which `<Box tag>`, and the three that carry a pattern o
|
|
|
30
30
|
| a lucide/Tabler icon, styled | `<Icon>` | `components/icon` |
|
|
31
31
|
| a sparkline, ring, gauge or donut | `<Sparkline>`/`<ProgressRing>`/… | `components/chart` |
|
|
32
32
|
| a themed Recharts (or any) chart | `<ChartContainer>` | `components/chart` |
|
|
33
|
+
| a themed React Flow canvas | `<FlowContainer>` | `components/flow` |
|
|
33
34
|
|
|
34
35
|
Also: `Mark`, `Figure`, `Figcaption`, `Details`, `Summary`, `MenuList` (the semantic `<menu>`; the menu **button** is `Menu` from `components/menu`), `Time`. All from `@box-kite/react/components/...`.
|
|
35
36
|
|
|
@@ -270,6 +271,12 @@ import Select from '@box-kite/react/components/select';
|
|
|
270
271
|
**SelectDef**: `valueKey` (required), `displayKey`, `display` (`(row) => ReactNode`), `selectedDisplay` (`(rows, isOpen) => ReactNode`), `placeholder`, `selectAllText`, `emptyText`.
|
|
271
272
|
Also: `data`, `label`/`labelProps`, `value`/`defaultValue`, `multiple`, `isSearchable`, `searchPlaceholder`, `showCheckbox`, `hideIcon`, `name`, `onValueChange` (and the deprecated `onChange`), `itemsProps`, `iconProps`, `variant`, BoxProps. Same combobox pattern as Dropdown — including needing a name, and `isSearchable` switching it to the editable one.
|
|
272
273
|
|
|
274
|
+
## Form fields: ids, labels and the invalid state
|
|
275
|
+
|
|
276
|
+
**A top-level `id` lands on the control itself, once**: the `<input>` of `Textbox`, `Combobox` and `TimeZoneSelect`, the `<textarea>`, the `Select` button, the input of `Checkbox`, `Switch` and `RadioButton` — so an app's own `<label htmlFor>` names it, with no `aria-labelledby` needed. `DateField`, `DatePicker`, `Slider` and `RadioGroup` are groups of spinbuttons, thumbs or radios, which no `<label>` can name: the `id` goes on the group, and an external label names it through `labelledBy` (or use their own `label`).
|
|
277
|
+
|
|
278
|
+
**`aria-invalid` is the invalid state, on every field**: `props={{ 'aria-invalid': true }}` on `Textbox`, `Textarea`, `Select` or `Combobox` draws the frame's border in `red-500`, hover and focus included. On a `Combobox` the attribute is on the input and the border is the shell's, so the shell reads it with `:has([aria-invalid="true"])`; restyle it through `combobox`'s `has` key, the others through `ariaAttr={{ invalid: … }}`. `DateField` and `DatePicker` take an `invalid` prop instead.
|
|
279
|
+
|
|
273
280
|
## DataGrid
|
|
274
281
|
|
|
275
282
|
```tsx
|
|
@@ -18,7 +18,7 @@ Every rule in `.claude/rules/box-kite-rules.md`, with the reasons, the measureme
|
|
|
18
18
|
|
|
19
19
|
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
|
|
20
20
|
|
|
21
|
-
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.
|
|
21
|
+
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. **A React Flow canvas goes in `<FlowContainer>`** (`components/flow`) with `colorMode={flowColorMode(theme)}` on `<ReactFlow>`: it declares xyflow's `--xy-*` variables from palette roles in both themes, and a custom node spreads `flowNodeStyles` for the keyboard focus xyflow removes.
|
|
22
22
|
|
|
23
23
|
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
|
|
24
24
|
|
|
@@ -88,7 +88,7 @@ Every rule in `.claude/rules/box-kite-rules.md`, with the reasons, the measureme
|
|
|
88
88
|
|
|
89
89
|
43. **An agent's turn is `<ToolCallCard>`, `<ApprovalCard>` and `<Reasoning>`.** `<ToolCallCard name="searchOrders" status="running" input={{ query: 'refunds' }} />`, `<ApprovalCard title="Refund order 4182" onDecisionChange={(decision) => respond(decision === 'approved')} />` and `<Reasoning duration={4200}>{thought}</Reasoning>` (`components/agent`) are the three parts of a turn that are not prose: what it ran, what it wants permission to do, and what it was thinking. **The status is a word, not a colour** — `pending`/`running`/`success`/`error` each carry their own label beside the dot, because a forced-colors mode throws a tint away and a screen reader never had it — and the four map one for one onto what every runtime reports under its own spelling, so AI SDK's `input-streaming`/`input-available`/`output-available`/`output-error` is a lookup rather than a state machine. **A value is whatever the model produced, so it is formatted rather than trusted**: `AgentUtils.formatValue` (framework-free, same entry) caps the text at `valueLimit` — 20,000 characters, with a line saying how much was left — and answers a circular structure, a `BigInt` and a function, all three of which make `JSON.stringify` throw or drop them silently. **Four things to know.** `onDecisionChange(decision, { reason })` is the approval card's whole API, which is what maps it onto AI SDK 6's `needsApproval`, CopilotKit's `renderAndWaitForResponse` and AG-UI's `INTERRUPT`; the answer lands in a `role="status"` **that is in the DOM before there is anything in it**, the toaster's rule, and `busy` is the round trip to a server. The card **does not take focus** unless `autoFocus` says so, and then on _Reject_ — APG's least-destructive rule — because a turn arrives while the reader is somewhere else. A `ToolCallCard` with nothing to disclose **renders no control at all**, the header being a row of text rather than a button nobody wants to land on. And `Reasoning` is **closed by default** and opens in the same `1fr`-to-`0fr` grid an `Accordion` panel does, so nothing is measured: `streaming` only changes the words and the shimmer, and `open={streaming}` is how an app makes it follow. Styling is the `toolCall`/`approval`/`reasoning` trees, where the status is a variant on `toolCall.status`, the decision one on `approval` itself, and `toolCall.header`'s `interactive` variant is what makes a header look pressable.
|
|
90
90
|
|
|
91
|
-
44. **What it _says_ is `<StreamingText>`, `markdownComponents` and `<Skeleton>`.** `<StreamingText text={message} streaming />` (`components/agent`) takes the text so far and fades in the part that was not there a render ago — **hand it the whole message every time, not the delta**, because knowing what is new means comparing the two. What is on the page is one settled string plus the last few runs to arrive (`window`, 8 by default; `0` turns the entrance off), so a message already whole paints at once with nothing moving — a prerendered page, a transcript read back — and one still arriving costs the same at the ten-thousandth token as at the first. The entrance is `@starting-style` rather than a keyframe, so it rides `--transitionTime` and stops under `prefers-reduced-motion` on its own, and the element is deliberately **not** a live region: one announcing every token would read the message out a word at a time and again when it finished, so it carries `aria-busy` and the transcript does the announcing. **The markdown bridge is a map, not a component**: `markdownComponents` (`components/markdown`) is the `components` map `react-markdown`, Streamdown and everything on that shape already take, so the parser stays the app's and there is no stylesheet, no Tailwind `@source` line and no design tokens to declare — wrap the renderer in `<Box component="markdown">` and the nodes inherit its size and colour. It is a **constant**, and that matters while streaming: a map built inside render is a new set of component _types_ every token, which React answers by unmounting the whole message, so override by spreading at module scope (`{ ...markdownComponents, h1: MyHeading }`). Whether a URL is safe stays the renderer's `urlTransform`, since the href is parsed before a component is called. **`<Skeleton lines={3} />`** (`components/skeleton`) is the placeholder — `circle` for an avatar, the gloss a named duration so it stops itself — and it is `aria-hidden` unless given a `label`, which makes it one `role="status"`: put that on the skeleton standing for a region, never on every bar. Styling is `streamingText` (`segment`, `caret`), `skeleton` (`bar` with `short`/`circle`/`fill` — one bar fills the skeleton, so `<Skeleton height={40} />` is a block — `gloss`) and `markdown` (`heading`, `paragraph`, `link`, `list`, `item`, `quote`, `code`, `codeBlock`, `rule`, `image`, `table`, `row`, `cell`, `inline`, `checkbox`).
|
|
91
|
+
44. **What it _says_ is `<StreamingText>`, `markdownComponents` and `<Skeleton>`.** `<StreamingText text={message} streaming />` (`components/agent`) takes the text so far and fades in the part that was not there a render ago — **hand it the whole message every time, not the delta**, because knowing what is new means comparing the two. What is on the page is one settled string plus the last few runs to arrive (`window`, 8 by default; `0` turns the entrance off), so a message already whole paints at once with nothing moving — a prerendered page, a transcript read back — and one still arriving costs the same at the ten-thousandth token as at the first. The entrance is `@starting-style` rather than a keyframe, so it rides `--transitionTime` and stops under `prefers-reduced-motion` on its own, and the element is deliberately **not** a live region: one announcing every token would read the message out a word at a time and again when it finished, so it carries `aria-busy` and the transcript does the announcing. **The markdown bridge is a map, not a component**: `markdownComponents` (`components/markdown`) is the `components` map `react-markdown`, Streamdown and everything on that shape already take, so the parser stays the app's and there is no stylesheet, no Tailwind `@source` line and no design tokens to declare — wrap the renderer in `<Box component="markdown">` and the nodes inherit its size and colour. It is a **constant**, and that matters while streaming: a map built inside render is a new set of component _types_ every token, which React answers by unmounting the whole message, so override by spreading at module scope (`{ ...markdownComponents, h1: MyHeading }`). Whether a URL is safe stays the renderer's `urlTransform`, since the href is parsed before a component is called. **`<Skeleton lines={3} />`** (`components/skeleton`) is the placeholder — `circle` for an avatar, sized by `width` with the height following (40px with none: `<Skeleton circle width={6} />` is a 24px circle), the gloss a named duration so it stops itself — and it is `aria-hidden` unless given a `label`, which makes it one `role="status"`: put that on the skeleton standing for a region, never on every bar. Styling is `streamingText` (`segment`, `caret`), `skeleton` (`bar` with `short`/`circle`/`fill` — one bar fills the skeleton, so `<Skeleton height={40} />` is a block — `gloss`) and `markdown` (`heading`, `paragraph`, `link`, `list`, `item`, `quote`, `code`, `codeBlock`, `rule`, `image`, `table`, `row`, `cell`, `inline`, `checkbox`).
|
|
92
92
|
|
|
93
93
|
45. **A runtime's words for a tool call are `toolPart`, and A2UI is `a2uiToSpec`.** `@box-kite/react/interop` is this library's shapes and the agentic ecosystem's, mapped onto each other, and it **imports none of them** — an adapter that pulled in a runtime would choose one for the app. `toolPart(part)` reads a part from AI SDK, assistant-ui or CopilotKit and answers `{ kind, status, decision, input, output, error }`, where `kind` is `'call'` or `'approval'` and `status` is what `<ToolCallCard>` takes: **a decision is not a stage a call passes through, it is a question somebody answers**, which is why two components cover AI SDK's six states (`input-streaming`/`input-available`/`output-available`/`output-error` are a call, `approval-requested`/`approval-responded` are a question), assistant-ui's `requires-action` and the `executing` CopilotKit hands a `respond` with. **AG-UI reports _events_, not parts**, so it folds instead: `applyToolEvent(parts, event)`, one call per event, keyed by `toolCallId`. **A2UI is the only genuinely different shape**, a flat adjacency list of components referring to each other by id with a data model per surface, so it has real code: `a2uiApply(state, message)` folds a v0.8 or v0.9 stream (pure, and the same state back when nothing moved), `a2uiSurface(state)` picks one out and `a2uiToSpec(surface, { catalog })` walks it into the tree `<SpecRenderer>` renders — its `{ "path": "/x" }` binding is already `{ $data: '/x' }`, its `children: { componentId, path }` template is already `repeat`, and a component nothing reaches from the root is simply not arrived yet. `a2uiCatalog(catalog())` is the other direction, `catalog()` as an A2UI catalog document, where a component carries its own `id`, its type is a property and its children are **ids**. assistant-ui's `GenerativeUISpec` is `fromGenerativeUi`/`toGenerativeUi`, and the second **reports what it could not carry** (`data-binding`, `repeat`, `event`), because their nodes have nowhere to put those three. One trap, measured against zod 4.6: **a `$ref` resolves against the document, not the piece you lifted out of it**, so `z.fromJSONSchema(document.components.Flex)` throws `Reference not found: #/$defs/color` — `a2uiComponentSchema(document, name)` is that component with the definitions attached.
|
|
94
94
|
|
|
@@ -19,10 +19,12 @@ Every prop by category, the six kinds of nesting, and the theme system. Read thi
|
|
|
19
19
|
|
|
20
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
|
-
**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`.
|
|
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). The class carries the base reset, `display: block`, except on `a`, `span`, `label` and the other phrasing elements, which are `inline-block` — so a router link styled this way stays in its sentence. **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
|
|
|
24
24
|
**Charts** (`@box-kite/react/components/chart`, server-safe): `Sparkline`, `ProgressRing`, `Gauge`, `MiniDonut` — micro-primitives over the SVG components, **not a chart library** (no axes, no legends, no data transformations; theme Recharts for that). Each is an `Svg`, so `width`/`height` are the attributes, the paint is inherited by the shapes inside (default stroke `currentColor`, so `color="sky-500"` recolours one), and every prop, pseudo-class, breakpoint and theme works. `<Sparkline data={[4, 9, 6, 12]} variant="line|area|bar" min={0} max={20} />` — it fills its box (`preserveAspectRatio="none"` + `vectorEffect="non-scaling-stroke"`, so the line stays one width thick at any size), and `min`/`max` fix the axis so a column of rows is comparable. `<ProgressRing value={0.62} thickness={10} trackOpacity={0.2} />` and `<Gauge value={0.4} sweep={270} start={225} />` (degrees clockwise from twelve o'clock; `sweep={360}` is a ring) draw the value as a dash on the arc — a style prop, so it **eases between values with no animation code**; the fraction is rounded to half a percent, because a dash length lands in a class name. `<MiniDonut data={[5, 3, 2]} colors={['sky-500', 'var(--chart-2)']} />` gives each value its share of the circle. `children` is SVG drawn after the chart (an `SvgText` in the middle, a `Defs` with a gradient); `label` names it `role="img"`, and without one it is `aria-hidden` — leave a sparkline beside its own number unnamed, name one that _is_ the data. Cheap in a grid: the shape is the `d` attribute (no CSS at all, so 10,000 rows share every rule) while the paint is a class — and that paint is a style tree, `sparkline` > `line`/`area`/`bar`, `progressRing`/`gauge` > `track`/`arc`, `miniDonut` > `segment`, so one `Box.components()` override restyles every drawing in an app. A DataGrid cell renderer is a component — define it outside the render.
|
|
25
25
|
|
|
26
|
+
**Theming a React Flow canvas** — `<FlowContainer>` (`components/flow`, server-safe): declares xyflow's `--xy-*` variables from eight roles (`surface`, `panel`, `text`, `border`, `grid`, `edge`, `selection`, `handle`) in both themes; `tokens={{ selection: 'emerald-500' }}` repaints a role (every canvas: the `flowContainer` node in `Box.components()`), `motion="static"` stops Box transitions inside. Pass `colorMode={flowColorMode(theme)}` to `<ReactFlow>`. A custom node spreads `flowNodeStyles` (the keyboard focus xyflow removes) and `flowNodeSelectedStyles`; a `BaseEdge` takes `useClassNames(flowEdgeStyles, { svg: true, base: false })`.
|
|
27
|
+
|
|
26
28
|
**Theming a chart library** — `<ChartContainer>` (same entry, server-safe): the bridge for Recharts and anything
|
|
27
29
|
else that takes a colour. It declares `--chart-1` … `--chart-6` in both themes plus one `--color-<series>` per
|
|
28
30
|
series, so the chart itself names no colour at all: `series={['revenue', 'cost']}` (the palette in order) or
|
|
@@ -84,8 +86,8 @@ child's own CSS says the transition is over, then lets React remove it. `ref` go
|
|
|
84
86
|
`transition-duration`/`animation-duration` _is_ the wait — not a `transitionend` listener, which fires once per property), `props` is
|
|
85
87
|
`{ 'data-state': 'open' | 'closed' }`. Under `prefers-reduced-motion` the measured wait is `0` and the node leaves in the same commit.
|
|
86
88
|
`Tooltip`, the `Dropdown` popup and the DataGrid column menu are built on it. `interpolateSize="allow-keywords"` on a container is what makes
|
|
87
|
-
`height: auto` animate inside it (inherited; Chromium-only, elsewhere it snaps). `Box.configure({ transition: 'colors' | false })` changes
|
|
88
|
-
what the base class transitions, before the first render.
|
|
89
|
+
`height: auto` animate inside it (inherited; Chromium-only, elsewhere it snaps). `Box.configure({ transition: 'colors' | 'colors, opacity' | false })` changes
|
|
90
|
+
what the base class transitions, before the first render; anything else transitions `all` and warns.
|
|
89
91
|
|
|
90
92
|
**Scroll-driven animation & view transitions**: `animationTimeline` takes the animation's progress off a scroll position instead of a
|
|
91
93
|
clock — `'scroll()'` is the nearest scrollport's progress (`'scroll(root)'` the page's, `'scroll(self inline)'` an axis and a scroller
|
|
@@ -23,7 +23,7 @@ Each rule is what to write and the trap to avoid. The reasons, measurements and
|
|
|
23
23
|
6. **Spacing divider is 4**: `p={4}` → 16px (1rem).
|
|
24
24
|
7. **Border width and lineHeight are direct px**: `b={1}` → 1px, `lineHeight={24}` → 24px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px.
|
|
25
25
|
8. **A paint reference is a value, not an attribute**: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"`. A dashboard shape is `components/chart`: `<Sparkline>`, `<ProgressRing>`, `<Gauge>`, `<MiniDonut>`.
|
|
26
|
-
9. **A CSS variable is a prop**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` for the subtree. A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>` and reads `var(--color-revenue)`; `series` also takes a record of paints.
|
|
26
|
+
9. **A CSS variable is a prop**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` for the subtree. A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>` and reads `var(--color-revenue)`; `series` also takes a record of paints. A React Flow canvas goes in `<FlowContainer>` (`components/flow`) with `colorMode={flowColorMode(theme)}`.
|
|
27
27
|
10. **SVG lengths have no divider**: `strokeWidth={2}` → `stroke-width: 2`; `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y` write px (user units). On `<Rect>`, `width`/`height` are the SVG attributes; on `<Path>`, `d` is path data.
|
|
28
28
|
11. **HTML attributes go in `props`**: `<Link props={{ href: '/about' }}>`, not `<Link href>` — at the top level they typecheck and are dropped. The form controls take their own directly: `<Button type="submit" onClick={…}>`, `<Textbox name="email" type="email" value onChange>`, `<Checkbox name defaultChecked onChange>`.
|
|
29
29
|
12. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%.
|
package/AGENTS.md
CHANGED
|
@@ -55,7 +55,7 @@ Each rule is what to write and the trap to avoid. The reasons, measurements and
|
|
|
55
55
|
6. **Spacing divider is 4**: `p={4}` → 16px (1rem).
|
|
56
56
|
7. **Border width and lineHeight are direct px**: `b={1}` → 1px, `lineHeight={24}` → 24px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px.
|
|
57
57
|
8. **A paint reference is a value, not an attribute**: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"`. A dashboard shape is `components/chart`: `<Sparkline>`, `<ProgressRing>`, `<Gauge>`, `<MiniDonut>`.
|
|
58
|
-
9. **A CSS variable is a prop**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` for the subtree. A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>` and reads `var(--color-revenue)`; `series` also takes a record of paints.
|
|
58
|
+
9. **A CSS variable is a prop**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` for the subtree. A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>` and reads `var(--color-revenue)`; `series` also takes a record of paints. A React Flow canvas goes in `<FlowContainer>` (`components/flow`) with `colorMode={flowColorMode(theme)}`.
|
|
59
59
|
10. **SVG lengths have no divider**: `strokeWidth={2}` → `stroke-width: 2`; `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y` write px (user units). On `<Rect>`, `width`/`height` are the SVG attributes; on `<Path>`, `d` is path data.
|
|
60
60
|
11. **HTML attributes go in `props`**: `<Link props={{ href: '/about' }}>`, not `<Link href>` — at the top level they typecheck and are dropped. The form controls take their own directly: `<Button type="submit" onClick={…}>`, `<Textbox name="email" type="email" value onChange>`, `<Checkbox name defaultChecked onChange>`.
|
|
61
61
|
12. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%.
|
package/BOX_AI_CONTEXT.md
CHANGED
|
@@ -660,8 +660,15 @@ const wobble = Box.spring({ stiffness: 120, damping: 8 });
|
|
|
660
660
|
the same commit, with nothing to configure. `Tooltip`, the `Dropdown` popup and the DataGrid column
|
|
661
661
|
menu are all built on it, and each expresses its exit as a `closed` variant in its component styles — plus `up`/`closedUp` on `dropdown.items`, since a popup that opened upward has to collapse upward.
|
|
662
662
|
- **`Box.configure({ transition })`** changes what the base class transitions for the whole engine:
|
|
663
|
-
a group name,
|
|
663
|
+
a group name, a comma list of groups and property names (`'colors, opacity'`), or `false` to declare
|
|
664
|
+
nothing at all and leave transitions entirely to the props. Anything else transitions `all` and warns.
|
|
664
665
|
Call it before the first render.
|
|
666
|
+
- **An inline phrasing element is `inline-block`**, where every other Box is `block`: `a`, `span`, `label`, `time`, `mark`, `abbr`, `code`, `kbd`, `samp`, `var`, `em`,
|
|
667
|
+
`strong`, `small`, `b`, `i`, `u`, `s`, `sub`, `sup`, `q`, `cite`, `dfn`, `data`, `output`, `bdi`, `bdo`. It is
|
|
668
|
+
selected on the rendered element at the base class's specificity, so `<Link>`, `<Box tag="a">` and a
|
|
669
|
+
`useClassNames` class on a router's `<a>` all stay in their sentence, margin and padding still apply, and a
|
|
670
|
+
`display` prop or component still wins. Media (`img`, `svg`, `video`, `canvas`, `iframe`) stays block. An `inline-block` never breaks across lines, so a
|
|
671
|
+
prose link or `<Mark>` that has to wrap with the text sets `display="inline"`.
|
|
665
672
|
|
|
666
673
|
---
|
|
667
674
|
|
|
@@ -1320,6 +1327,11 @@ undefined and rendering it costs nothing, so write the line either way. Pass `{
|
|
|
1320
1327
|
second argument for an element inside an `<svg>`. `<Icon>` is this hook plus that flag — reach for
|
|
1321
1328
|
`Icon` for an icon and this only for everything else.
|
|
1322
1329
|
|
|
1330
|
+
The class carries the base reset, which is `display: block` except on an `<a>`, `<span>`, `<label>` and the
|
|
1331
|
+
other phrasing elements, which are `inline-block`: a `NavLink` styled this way stays in its sentence.
|
|
1332
|
+
`{ base: false }` leaves the reset out — for a library's own root (`<ReactFlow>`, `<MiniMap>`, a `BaseEdge`)
|
|
1333
|
+
whose margin, display and transitions are its own.
|
|
1334
|
+
|
|
1323
1335
|
### Group and Peer
|
|
1324
1336
|
|
|
1325
1337
|
```tsx
|
|
@@ -1456,6 +1468,19 @@ Full reference: `docs/a11y-primitives.md`.
|
|
|
1456
1468
|
|
|
1457
1469
|
---
|
|
1458
1470
|
|
|
1471
|
+
## Field ids, external labels and the invalid state
|
|
1472
|
+
|
|
1473
|
+
- **A top-level `id` lands on the control, once.** `<Combobox id="owner">` puts it on the `<input role="combobox">`
|
|
1474
|
+
and nowhere else, as `Textbox`, `Textarea`, `Select` (its button), `Checkbox`, `Switch`, `RadioButton` and
|
|
1475
|
+
`TimeZoneSelect` do, so `<label htmlFor="owner">` names the control. `DateField`, `DatePicker`, `Slider` and
|
|
1476
|
+
`RadioGroup` are groups no `<label>` can name: the `id` goes on the group, and `labelledBy` points it at an
|
|
1477
|
+
external label.
|
|
1478
|
+
- **`aria-invalid` draws the error border on every field.** `props={{ 'aria-invalid': true }}` turns the frame of a
|
|
1479
|
+
`Textbox`, `Textarea`, `Select` or `Combobox` `red-500`, through hover and focus, in both themes. A combobox's
|
|
1480
|
+
attribute is on its input and its border on the shell, so the shell matches `:has([aria-invalid="true"])` —
|
|
1481
|
+
restyle it as `combobox: { styles: { has: { '[aria-invalid="true"]': { … } } } }`, the others with
|
|
1482
|
+
`ariaAttr: { invalid: { … } }`. `DateField` and `DatePicker` take an `invalid` prop that does both.
|
|
1483
|
+
|
|
1459
1484
|
## Sizes: compact, normal, large
|
|
1460
1485
|
|
|
1461
1486
|
Every built-in component is drawn at one of three sizes, and a size is two CSS variables —
|
|
@@ -2660,6 +2685,51 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
|
|
|
2660
2685
|
<DataGrid data={rows} def={{ rowKey: 'id', columns: [{ key: 'trend', header: 'Last 12 months', width: 180, Cell: TrendCell }] }} />;
|
|
2661
2686
|
```
|
|
2662
2687
|
|
|
2688
|
+
### Theming a React Flow canvas — `FlowContainer`
|
|
2689
|
+
|
|
2690
|
+
`ChartContainer`'s move for diagrams. `@box-kite/react/components/flow` (server-safe, imports nothing from xyflow) is a Box
|
|
2691
|
+
that declares React Flow's `--xy-*` variables from the palette in both themes, so the canvas — background, pattern, edges,
|
|
2692
|
+
handles, built-in nodes, minimap, controls, selection box, edge labels — names no colour and follows the page:
|
|
2693
|
+
|
|
2694
|
+
```tsx
|
|
2695
|
+
import { Background, Controls, MiniMap, ReactFlow } from '@xyflow/react';
|
|
2696
|
+
import '@xyflow/react/dist/style.css';
|
|
2697
|
+
import { FlowContainer, flowColorMode } from '@box-kite/react/components/flow';
|
|
2698
|
+
|
|
2699
|
+
function Canvas() {
|
|
2700
|
+
const [theme] = Box.useTheme();
|
|
2701
|
+
|
|
2702
|
+
return (
|
|
2703
|
+
<FlowContainer height={120}>
|
|
2704
|
+
<ReactFlow nodes={nodes} edges={edges} colorMode={flowColorMode(theme)} fitView>
|
|
2705
|
+
<Background />
|
|
2706
|
+
<Controls />
|
|
2707
|
+
<MiniMap />
|
|
2708
|
+
</ReactFlow>
|
|
2709
|
+
</FlowContainer>
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
```
|
|
2713
|
+
|
|
2714
|
+
| Prop | What it is |
|
|
2715
|
+
| -------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
2716
|
+
| `tokens` | Repaints a role: `{ selection: 'emerald-500' }` for both themes, `{ surface: { light: 'base-50', dark: 'base-950' } }` per theme |
|
|
2717
|
+
| `motion` | `'static'` zeroes every Box transition inside the canvas, for nodes moved or resized every frame |
|
|
2718
|
+
|
|
2719
|
+
- **Eight roles**, each a custom property a custom node can read: `--flow-surface`, `--flow-panel`, `--flow-text`, `--flow-border`,
|
|
2720
|
+
`--flow-grid`, `--flow-edge`, `--flow-selection`, `--flow-handle`. Defaults are `base`/`accent`, so `Box.palette()` reaches them.
|
|
2721
|
+
- **The defaults are the `flowContainer` style node**, so one override repaints every canvas in an app:
|
|
2722
|
+
`Box.components({ flowContainer: { styles: { vars: { 'flow-selection': 'emerald-500' } } } })`; `tokens` is the per-canvas answer.
|
|
2723
|
+
- **One `--xy-*` variable is `vars`** (`vars={{ 'xy-edge-stroke-width': '2' }}`); a prop on `<MiniMap>`/`<Background>` still wins.
|
|
2724
|
+
- **`colorMode={flowColorMode(theme)}`** keeps React Flow's own `.dark` class in step with the page. A canvas in another theme sits
|
|
2725
|
+
in `<Box.Theme use="local">`; never `colorMode="light"` under a dark page — `.light` is no theme boundary, so the two mix.
|
|
2726
|
+
- **A custom node is invisible to the keyboard** — xyflow removes every node's focus outline. Spread `flowNodeStyles` on its
|
|
2727
|
+
root (an outline while xyflow's wrapper is `:focus-visible`) and `flowNodeSelectedStyles` when its `selected` prop is true;
|
|
2728
|
+
both are outlines in the `selection` role, so they survive forced colours. A custom edge on `BaseEdge` takes `flowEdgeStyles`
|
|
2729
|
+
through `useClassNames(flowEdgeStyles, { svg: true, base: false })`.
|
|
2730
|
+
- Colour props refuse `var()`, so a custom node reads a role through `css`:
|
|
2731
|
+
`css={{ backgroundColor: 'var(--flow-panel)', borderColor: 'var(--flow-border)', color: 'var(--flow-text)' }}`.
|
|
2732
|
+
|
|
2663
2733
|
---
|
|
2664
2734
|
|
|
2665
2735
|
## Key Reminders for AI Assistants
|
|
@@ -2677,7 +2747,7 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
|
|
|
2677
2747
|
11. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
|
|
2678
2748
|
12. **Box is memoized** with `React.memo`, and so are `Flex`, `H1`, `Button` and the rest. A component of your own that is a Box with defaults calls `useBoxElement({ p: 4, ...props }, ref)` from its own `forwardRef` rather than returning `<Box>` — one fiber instead of two
|
|
2679
2749
|
13. **`style` is top-level only** — never inside breakpoints (`sm={{ style: ... }}`), pseudo-classes, or theme objects. `css` is the one that nests: `sm={{ css: { objectPosition: 'top' } }}` is a rule
|
|
2680
|
-
14. **A CSS variable is a prop**: `vars={{ 'color-x': 'sky-500' }}` declares `--color-x` for everything inside — and a third-party chart goes in `<ChartContainer series={['revenue']}>` so it names no colour at all
|
|
2750
|
+
14. **A CSS variable is a prop**: `vars={{ 'color-x': 'sky-500' }}` declares `--color-x` for everything inside — and a third-party chart goes in `<ChartContainer series={['revenue']}>` so it names no colour at all, and a React Flow canvas goes in `<FlowContainer>` with `colorMode={flowColorMode(theme)}`
|
|
2681
2751
|
15. **All sizing props use divider 4** — `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight` are NOT direct pixels. `height={10}` = 2.5rem = 40px
|
|
2682
2752
|
16. **A state your own code sets is a nested prop too**: `dataAttr={{ 'state=open': { … } }}` → `[data-state="open"]`, `ariaAttr={{ selected: { … } }}` → `[aria-selected="true"]`, `has={{ ':checked': { … } }}`, `not={{ hover: { … } }}`, `nth={{ odd: { … } }}`. The attribute itself still goes in `props`
|
|
2683
2753
|
17. **Somebody else's state is `group`/`peer`**: `group={{ 'card/hover': { … } }}` → `.card:hover .x`, `peer={{ 'agree/checked': { … } }}` → `.agree:checked ~ .x`. The ancestor or sibling carries the class via `className`; `hoverGroup` and its four siblings are the older spelling
|
|
@@ -3786,11 +3856,11 @@ with `language` and `code`, `rule`, `image`, `tableWrap`, `table`, `row`, `cell`
|
|
|
3786
3856
|
|
|
3787
3857
|
Where content will be, while it is being fetched.
|
|
3788
3858
|
|
|
3789
|
-
| Prop | Type | What it does
|
|
3790
|
-
| -------- | ----------- |
|
|
3791
|
-
| `lines` | `number` | How many bars. Default 1, which fills the skeleton; past that the last one is short.
|
|
3792
|
-
| `circle` | `boolean` | One round bar — an avatar. `width` sets its size
|
|
3793
|
-
| `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`.
|
|
3859
|
+
| Prop | Type | What it does |
|
|
3860
|
+
| -------- | ----------- | ---------------------------------------------------------------------------------------- |
|
|
3861
|
+
| `lines` | `number` | How many bars. Default 1, which fills the skeleton; past that the last one is short. |
|
|
3862
|
+
| `circle` | `boolean` | One round bar — an avatar. `width` sets its size (40px without one), the height follows. |
|
|
3863
|
+
| `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`. |
|
|
3794
3864
|
|
|
3795
3865
|
```tsx
|
|
3796
3866
|
<Skeleton lines={3} label="Loading orders" />
|
package/BOX_KITE_AI_CONTEXT.md
CHANGED
|
@@ -660,8 +660,15 @@ const wobble = Box.spring({ stiffness: 120, damping: 8 });
|
|
|
660
660
|
the same commit, with nothing to configure. `Tooltip`, the `Dropdown` popup and the DataGrid column
|
|
661
661
|
menu are all built on it, and each expresses its exit as a `closed` variant in its component styles — plus `up`/`closedUp` on `dropdown.items`, since a popup that opened upward has to collapse upward.
|
|
662
662
|
- **`Box.configure({ transition })`** changes what the base class transitions for the whole engine:
|
|
663
|
-
a group name,
|
|
663
|
+
a group name, a comma list of groups and property names (`'colors, opacity'`), or `false` to declare
|
|
664
|
+
nothing at all and leave transitions entirely to the props. Anything else transitions `all` and warns.
|
|
664
665
|
Call it before the first render.
|
|
666
|
+
- **An inline phrasing element is `inline-block`**, where every other Box is `block`: `a`, `span`, `label`, `time`, `mark`, `abbr`, `code`, `kbd`, `samp`, `var`, `em`,
|
|
667
|
+
`strong`, `small`, `b`, `i`, `u`, `s`, `sub`, `sup`, `q`, `cite`, `dfn`, `data`, `output`, `bdi`, `bdo`. It is
|
|
668
|
+
selected on the rendered element at the base class's specificity, so `<Link>`, `<Box tag="a">` and a
|
|
669
|
+
`useClassNames` class on a router's `<a>` all stay in their sentence, margin and padding still apply, and a
|
|
670
|
+
`display` prop or component still wins. Media (`img`, `svg`, `video`, `canvas`, `iframe`) stays block. An `inline-block` never breaks across lines, so a
|
|
671
|
+
prose link or `<Mark>` that has to wrap with the text sets `display="inline"`.
|
|
665
672
|
|
|
666
673
|
---
|
|
667
674
|
|
|
@@ -1320,6 +1327,11 @@ undefined and rendering it costs nothing, so write the line either way. Pass `{
|
|
|
1320
1327
|
second argument for an element inside an `<svg>`. `<Icon>` is this hook plus that flag — reach for
|
|
1321
1328
|
`Icon` for an icon and this only for everything else.
|
|
1322
1329
|
|
|
1330
|
+
The class carries the base reset, which is `display: block` except on an `<a>`, `<span>`, `<label>` and the
|
|
1331
|
+
other phrasing elements, which are `inline-block`: a `NavLink` styled this way stays in its sentence.
|
|
1332
|
+
`{ base: false }` leaves the reset out — for a library's own root (`<ReactFlow>`, `<MiniMap>`, a `BaseEdge`)
|
|
1333
|
+
whose margin, display and transitions are its own.
|
|
1334
|
+
|
|
1323
1335
|
### Group and Peer
|
|
1324
1336
|
|
|
1325
1337
|
```tsx
|
|
@@ -1456,6 +1468,19 @@ Full reference: `docs/a11y-primitives.md`.
|
|
|
1456
1468
|
|
|
1457
1469
|
---
|
|
1458
1470
|
|
|
1471
|
+
## Field ids, external labels and the invalid state
|
|
1472
|
+
|
|
1473
|
+
- **A top-level `id` lands on the control, once.** `<Combobox id="owner">` puts it on the `<input role="combobox">`
|
|
1474
|
+
and nowhere else, as `Textbox`, `Textarea`, `Select` (its button), `Checkbox`, `Switch`, `RadioButton` and
|
|
1475
|
+
`TimeZoneSelect` do, so `<label htmlFor="owner">` names the control. `DateField`, `DatePicker`, `Slider` and
|
|
1476
|
+
`RadioGroup` are groups no `<label>` can name: the `id` goes on the group, and `labelledBy` points it at an
|
|
1477
|
+
external label.
|
|
1478
|
+
- **`aria-invalid` draws the error border on every field.** `props={{ 'aria-invalid': true }}` turns the frame of a
|
|
1479
|
+
`Textbox`, `Textarea`, `Select` or `Combobox` `red-500`, through hover and focus, in both themes. A combobox's
|
|
1480
|
+
attribute is on its input and its border on the shell, so the shell matches `:has([aria-invalid="true"])` —
|
|
1481
|
+
restyle it as `combobox: { styles: { has: { '[aria-invalid="true"]': { … } } } }`, the others with
|
|
1482
|
+
`ariaAttr: { invalid: { … } }`. `DateField` and `DatePicker` take an `invalid` prop that does both.
|
|
1483
|
+
|
|
1459
1484
|
## Sizes: compact, normal, large
|
|
1460
1485
|
|
|
1461
1486
|
Every built-in component is drawn at one of three sizes, and a size is two CSS variables —
|
|
@@ -2660,6 +2685,51 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
|
|
|
2660
2685
|
<DataGrid data={rows} def={{ rowKey: 'id', columns: [{ key: 'trend', header: 'Last 12 months', width: 180, Cell: TrendCell }] }} />;
|
|
2661
2686
|
```
|
|
2662
2687
|
|
|
2688
|
+
### Theming a React Flow canvas — `FlowContainer`
|
|
2689
|
+
|
|
2690
|
+
`ChartContainer`'s move for diagrams. `@box-kite/react/components/flow` (server-safe, imports nothing from xyflow) is a Box
|
|
2691
|
+
that declares React Flow's `--xy-*` variables from the palette in both themes, so the canvas — background, pattern, edges,
|
|
2692
|
+
handles, built-in nodes, minimap, controls, selection box, edge labels — names no colour and follows the page:
|
|
2693
|
+
|
|
2694
|
+
```tsx
|
|
2695
|
+
import { Background, Controls, MiniMap, ReactFlow } from '@xyflow/react';
|
|
2696
|
+
import '@xyflow/react/dist/style.css';
|
|
2697
|
+
import { FlowContainer, flowColorMode } from '@box-kite/react/components/flow';
|
|
2698
|
+
|
|
2699
|
+
function Canvas() {
|
|
2700
|
+
const [theme] = Box.useTheme();
|
|
2701
|
+
|
|
2702
|
+
return (
|
|
2703
|
+
<FlowContainer height={120}>
|
|
2704
|
+
<ReactFlow nodes={nodes} edges={edges} colorMode={flowColorMode(theme)} fitView>
|
|
2705
|
+
<Background />
|
|
2706
|
+
<Controls />
|
|
2707
|
+
<MiniMap />
|
|
2708
|
+
</ReactFlow>
|
|
2709
|
+
</FlowContainer>
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
```
|
|
2713
|
+
|
|
2714
|
+
| Prop | What it is |
|
|
2715
|
+
| -------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
2716
|
+
| `tokens` | Repaints a role: `{ selection: 'emerald-500' }` for both themes, `{ surface: { light: 'base-50', dark: 'base-950' } }` per theme |
|
|
2717
|
+
| `motion` | `'static'` zeroes every Box transition inside the canvas, for nodes moved or resized every frame |
|
|
2718
|
+
|
|
2719
|
+
- **Eight roles**, each a custom property a custom node can read: `--flow-surface`, `--flow-panel`, `--flow-text`, `--flow-border`,
|
|
2720
|
+
`--flow-grid`, `--flow-edge`, `--flow-selection`, `--flow-handle`. Defaults are `base`/`accent`, so `Box.palette()` reaches them.
|
|
2721
|
+
- **The defaults are the `flowContainer` style node**, so one override repaints every canvas in an app:
|
|
2722
|
+
`Box.components({ flowContainer: { styles: { vars: { 'flow-selection': 'emerald-500' } } } })`; `tokens` is the per-canvas answer.
|
|
2723
|
+
- **One `--xy-*` variable is `vars`** (`vars={{ 'xy-edge-stroke-width': '2' }}`); a prop on `<MiniMap>`/`<Background>` still wins.
|
|
2724
|
+
- **`colorMode={flowColorMode(theme)}`** keeps React Flow's own `.dark` class in step with the page. A canvas in another theme sits
|
|
2725
|
+
in `<Box.Theme use="local">`; never `colorMode="light"` under a dark page — `.light` is no theme boundary, so the two mix.
|
|
2726
|
+
- **A custom node is invisible to the keyboard** — xyflow removes every node's focus outline. Spread `flowNodeStyles` on its
|
|
2727
|
+
root (an outline while xyflow's wrapper is `:focus-visible`) and `flowNodeSelectedStyles` when its `selected` prop is true;
|
|
2728
|
+
both are outlines in the `selection` role, so they survive forced colours. A custom edge on `BaseEdge` takes `flowEdgeStyles`
|
|
2729
|
+
through `useClassNames(flowEdgeStyles, { svg: true, base: false })`.
|
|
2730
|
+
- Colour props refuse `var()`, so a custom node reads a role through `css`:
|
|
2731
|
+
`css={{ backgroundColor: 'var(--flow-panel)', borderColor: 'var(--flow-border)', color: 'var(--flow-text)' }}`.
|
|
2732
|
+
|
|
2663
2733
|
---
|
|
2664
2734
|
|
|
2665
2735
|
## Key Reminders for AI Assistants
|
|
@@ -2677,7 +2747,7 @@ function TrendCell({ cell }: { cell: CellModel<Row> }) {
|
|
|
2677
2747
|
11. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
|
|
2678
2748
|
12. **Box is memoized** with `React.memo`, and so are `Flex`, `H1`, `Button` and the rest. A component of your own that is a Box with defaults calls `useBoxElement({ p: 4, ...props }, ref)` from its own `forwardRef` rather than returning `<Box>` — one fiber instead of two
|
|
2679
2749
|
13. **`style` is top-level only** — never inside breakpoints (`sm={{ style: ... }}`), pseudo-classes, or theme objects. `css` is the one that nests: `sm={{ css: { objectPosition: 'top' } }}` is a rule
|
|
2680
|
-
14. **A CSS variable is a prop**: `vars={{ 'color-x': 'sky-500' }}` declares `--color-x` for everything inside — and a third-party chart goes in `<ChartContainer series={['revenue']}>` so it names no colour at all
|
|
2750
|
+
14. **A CSS variable is a prop**: `vars={{ 'color-x': 'sky-500' }}` declares `--color-x` for everything inside — and a third-party chart goes in `<ChartContainer series={['revenue']}>` so it names no colour at all, and a React Flow canvas goes in `<FlowContainer>` with `colorMode={flowColorMode(theme)}`
|
|
2681
2751
|
15. **All sizing props use divider 4** — `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight` are NOT direct pixels. `height={10}` = 2.5rem = 40px
|
|
2682
2752
|
16. **A state your own code sets is a nested prop too**: `dataAttr={{ 'state=open': { … } }}` → `[data-state="open"]`, `ariaAttr={{ selected: { … } }}` → `[aria-selected="true"]`, `has={{ ':checked': { … } }}`, `not={{ hover: { … } }}`, `nth={{ odd: { … } }}`. The attribute itself still goes in `props`
|
|
2683
2753
|
17. **Somebody else's state is `group`/`peer`**: `group={{ 'card/hover': { … } }}` → `.card:hover .x`, `peer={{ 'agree/checked': { … } }}` → `.agree:checked ~ .x`. The ancestor or sibling carries the class via `className`; `hoverGroup` and its four siblings are the older spelling
|
|
@@ -3786,11 +3856,11 @@ with `language` and `code`, `rule`, `image`, `tableWrap`, `table`, `row`, `cell`
|
|
|
3786
3856
|
|
|
3787
3857
|
Where content will be, while it is being fetched.
|
|
3788
3858
|
|
|
3789
|
-
| Prop | Type | What it does
|
|
3790
|
-
| -------- | ----------- |
|
|
3791
|
-
| `lines` | `number` | How many bars. Default 1, which fills the skeleton; past that the last one is short.
|
|
3792
|
-
| `circle` | `boolean` | One round bar — an avatar. `width` sets its size
|
|
3793
|
-
| `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`.
|
|
3859
|
+
| Prop | Type | What it does |
|
|
3860
|
+
| -------- | ----------- | ---------------------------------------------------------------------------------------- |
|
|
3861
|
+
| `lines` | `number` | How many bars. Default 1, which fills the skeleton; past that the last one is short. |
|
|
3862
|
+
| `circle` | `boolean` | One round bar — an avatar. `width` sets its size (40px without one), the height follows. |
|
|
3863
|
+
| `label` | `ReactNode` | Makes it a `role="status"` naming what is on its way. Absent: `aria-hidden`. |
|
|
3794
3864
|
|
|
3795
3865
|
```tsx
|
|
3796
3866
|
<Skeleton lines={3} label="Loading orders" />
|
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.
|
|
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.g(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._(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.v(n.i(e,r?.svg===!0),e.className,r?.base)}exports.default=c,exports.useBoxElement=s,exports.useClassNames=l,exports.useVisibility=n.a;
|
package/box.d.ts
CHANGED
|
@@ -66,9 +66,14 @@ export type BoxTagProps<TTag extends keyof React.JSX.IntrinsicElements = 'div',
|
|
|
66
66
|
* `styles` is defined in element mode only, where the CSS travels as `<style href precedence>` elements;
|
|
67
67
|
* elsewhere it is undefined and rendering it costs nothing, so that line is what to write either way.
|
|
68
68
|
* `{ svg: true }` picks the SVG reset — `Icon` is this hook plus that flag.
|
|
69
|
+
*
|
|
70
|
+
* The class carries the base reset: `display: block`, or `inline-block` on an `<a>`, `<span>`, `<label>`… so a
|
|
71
|
+
* router link styled this way stays in its sentence. `{ base: false }` leaves it out, for a library's own root
|
|
72
|
+
* (`<ReactFlow>`, `<MiniMap>`) whose margin, display and transitions are its own (bug #231).
|
|
69
73
|
*/
|
|
70
74
|
export declare function useClassNames<TKey extends keyof ComponentsAndVariants = never>(props: BoxClassNameProps<TKey>, options?: {
|
|
71
75
|
svg?: boolean;
|
|
76
|
+
base?: boolean;
|
|
72
77
|
}): BoxClassNames;
|
|
73
78
|
export { useVisibility };
|
|
74
79
|
export type { BoxClassNames, BoxClassNameProps };
|
package/box.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as e, g as t, v as n } from "./react-shared.mjs";
|
|
2
2
|
import { a as r, i, n as a, o, r as s } from "./client.mjs";
|
|
3
3
|
import { i as c, n as l, r as u } from "./interactions.mjs";
|
|
4
4
|
import d, { useState as f } from "react";
|
|
@@ -12,8 +12,8 @@ function _({ tag: e, tagProps: t, render: n }) {
|
|
|
12
12
|
onMouseLeave: () => i(!1)
|
|
13
13
|
}, n({ isHover: r }));
|
|
14
14
|
}
|
|
15
|
-
function v(
|
|
16
|
-
let { tag: r = "div", children: a } =
|
|
15
|
+
function v(e, n) {
|
|
16
|
+
let { tag: r = "div", children: a } = e, { classNames: o, styleElements: s } = i(e, r === "svg"), c = t(e, o);
|
|
17
17
|
n && (c.ref = n);
|
|
18
18
|
let l = typeof a == "function" ? d.createElement(_, {
|
|
19
19
|
tag: r,
|
|
@@ -22,10 +22,10 @@ function v(t, n) {
|
|
|
22
22
|
}) : d.createElement(r, c, a);
|
|
23
23
|
return s ? d.createElement(d.Fragment, null, s, l) : l;
|
|
24
24
|
}
|
|
25
|
-
var y =
|
|
25
|
+
var y = e(v, "Box");
|
|
26
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
|
-
return n(i(e, t?.svg === !0), e.className);
|
|
28
|
+
return n(i(e, t?.svg === !0), e.className, t?.base);
|
|
29
29
|
}
|
|
30
30
|
//#endregion
|
|
31
31
|
export { y as default, v as useBoxElement, b as useClassNames, r as useVisibility };
|