@handamade/psi-react 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/Menu.md CHANGED
@@ -8,9 +8,10 @@ Action menu on the native Popover API top layer: the top layer and light dismiss
8
8
  | Prop | Type | Default | Required | Description |
9
9
  |---|---|---|---|---|
10
10
  | `open` | `boolean` | — | yes | Controlled open state; syncs to showPopover()/hidePopover(). |
11
- | `onClose` | `(reason: "item-select" \| "esc" \| "outside") => void` | — | yes | Called when a dismissal is *requested*; the consumer flips `open`. Menu never closes itself on `"esc"` or `"item-select"` — the popover stays open until `open` becomes false (D50). `"outside"` is the one exception: light dismiss is performed by the browser before it tells us, so by the time that reason is reported the popover is already closed. Flipping `open` to false is still required, to keep React's state in step. Invariant (D58): a dismissal is only ever reported for a menu that is currently open according to its own `open` prop. The platform can close an auto popover before the consumer's state catches up — clicking another menu's trigger light-dismisses this one before the consumer's click handler runs — and a report for an already-closed menu would clear a selection that has since moved on. |
11
+ | `onClose` | `(reason: MenuCloseReason) => void` | — | yes | Called when a dismissal is *requested*; the consumer flips `open`. Menu never closes itself on `"esc"` or `"item-select"` — the popover stays open until `open` becomes false (D50). `"outside"` is the one exception: light dismiss is performed by the browser before it tells us, so by the time that reason is reported the popover is already closed. Flipping `open` to false is still required, to keep React's state in step. Invariant (D58): a dismissal is only ever reported for a menu that is currently open according to its own `open` prop. The platform can close an auto popover before the consumer's state catches up — clicking another menu's trigger light-dismisses this one before the consumer's click handler runs — and a report for an already-closed menu would clear a selection that has since moved on. `"tab"` (D102): Tab or Shift Tab inside the open menu. Focus moves on to the next control as the browser moves it; the consumer flips `open`. |
12
12
  | `trigger` | `ReactElement<Record<string, unknown>, string \| JSXElementConstructor<any>>` | — | yes | The trigger element. Must be a single React element that spreads unknown props onto a focusable node (a Psi `Button`, say): Menu clones it to add `aria-haspopup="menu"` and `aria-expanded`, so assistive tech associates the menu with the control users actually focus. |
13
13
  | `placement` | `"bottom-start" \| "bottom-end" \| "top-start" \| "top-end"` | bottom-start | no | Placement relative to the trigger. |
14
+ | `id` | `string` | — | no | The menu element's id, which the trigger's `aria-controls` names (D102). Generated when absent. |
14
15
  | `aria-label` | `string` | — | no | Accessible name for the menu when there is no visible label. |
15
16
  | `aria-describedby` | `string` | — | no | The id of a `MenuText` (or any element) that describes the menu (D99). Unset by default: a description may be spoken every time the menu opens. |
16
17
  | `children` | `ReactNode` | — | yes | MenuItem, MenuSeparator and MenuText children. |
@@ -26,6 +27,8 @@ Action menu on the native Popover API top layer: the top layer and light dismiss
26
27
  | Any single printable key | Typeahead — focuses the first enabled item whose label starts with the typed prefix; the prefix resets after 500ms. Keystrokes with Meta/Ctrl/Alt held are ignored. |
27
28
  | Esc | Suppresses the platform's own dismissal and reports onClose("esc"); the menu stays open until the consumer flips `open` (D50). |
28
29
  | Enter / Space | Activates the focused item (native button behavior). |
30
+ | Arrow Down / Arrow Up on the closed trigger | Opens the menu on its first / last enabled item by clicking the trigger, since Menu cannot open itself; the consumer's onClick flips `open` (D102). |
31
+ | Tab / Shift Tab | Reports onClose("tab") and lets focus move on to the next control; the consumer flips `open`, and focus is not pulled back to the trigger (D102). |
29
32
 
30
33
  Opens on the native top layer via popover="auto", which supplies light dismiss. Controlled-only: every dismissal path (esc, item-select, outside) only reports onClose(reason) — the consumer must flip `open` to actually close it; outside is the one path the platform has already acted on by the time it is reported. Opening moves focus to the first enabled item — Menu takes focus off the trigger as soon as `open` becomes true. Focus returns to the trigger when the menu actually closes, and only if focus is still inside the menu, so a light dismiss onto another control does not steal focus back. Requires an accessible name — pass aria-label. Placement uses CSS anchor positioning where supported and a JS fallback below that floor; below it there is no collision flip.
31
34
 
package/docs/Radio.md ADDED
@@ -0,0 +1,40 @@
1
+ <!-- Generated by emit-docs.ts — do not edit. -->
2
+ # Radio
3
+
4
+ One choice in a RadioGroup (D104): a native radio with a built-in label, drawn like Checkbox on a circle. Takes its name, checked state, required and aria-invalid from the group.
5
+
6
+ ## Props
7
+
8
+ | Prop | Type | Default | Required | Description |
9
+ |---|---|---|---|---|
10
+ | `value` | `string` | — | yes | The value this radio selects. |
11
+ | `children` | `ReactNode` | — | no | Label text. |
12
+ | `disabled` | `boolean` | — | no | Disables this radio; a disabled RadioGroup disables all of them. |
13
+ | `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |
14
+ | `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type="radio">` element. |
15
+
16
+ ## Keyboard & assistive tech
17
+
18
+ | Keys | Behavior |
19
+ |---|---|
20
+ | Space | Selects the focused radio when none is selected. |
21
+
22
+ Native <input type=radio> underneath (visually hidden), drawn like Checkbox on a circle. Takes its name, checked state, required and aria-invalid from its RadioGroup.
23
+
24
+ ## Theming
25
+
26
+ This component has no `--psi-radio-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.
27
+
28
+
29
+
30
+ ## Rules
31
+
32
+ - One accent per visual group; everything else neutral or ghost.
33
+ - danger only for actions with real consequences.
34
+ - Sizes are px numbers (24|32|40|48), never S/M/L.
35
+ - Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.
36
+ - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
37
+ - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
38
+ - Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.
39
+ - Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination ("Rows per page" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as="form" align="start" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).
40
+ - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
@@ -0,0 +1,46 @@
1
+ <!-- Generated by emit-docs.ts — do not edit. -->
2
+ # RadioGroup
3
+
4
+ A set of native radios sharing one name (D104). The browser supplies the keyboard: Tab enters on the checked radio, and the arrows move and select. Inside `Field group` the fieldset and legend form and name the group, so this adds no role; standalone it is `role="radiogroup"` and needs `aria-label` or `aria-labelledby`. A required or invalid Field makes every radio `required` and `aria-invalid`.
5
+
6
+ ## Props
7
+
8
+ | Prop | Type | Default | Required | Description |
9
+ |---|---|---|---|---|
10
+ | `children` | `ReactNode` | — | no | `Radio` children. |
11
+ | `name` | `string` | — | no | The radios' shared `name`. Generated when absent. |
12
+ | `value` | `string` | — | no | The selected value (controlled). |
13
+ | `defaultValue` | `string` | — | no | The initially selected value (uncontrolled). |
14
+ | `onValueChange` | `((value: string) => void)` | — | no | Called with the value of the radio the user selects. |
15
+ | `disabled` | `boolean` | false | no | Disables every radio in the group. |
16
+ | `orientation` | `"vertical" \| "horizontal"` | vertical | no | Stack the radios, or lay them in a row. |
17
+ | `aria-label` | `string` | — | no | Accessible name when the group stands alone. Inside `Field group` the legend names the group instead. |
18
+ | `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root element. |
19
+ | `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |
20
+
21
+ ## Keyboard & assistive tech
22
+
23
+ | Keys | Behavior |
24
+ |---|---|
25
+ | Tab | Enters the group on the checked radio, or the first one; one tab stop for the group. |
26
+ | Arrow keys | Move to the next or previous radio and select it (native radios sharing one name). |
27
+
28
+ Native radios sharing one name (D104). Inside Field group the fieldset and legend form and name the group, so RadioGroup adds no role; standalone it is role="radiogroup" and needs aria-label or aria-labelledby. A required or invalid Field makes every radio required and aria-invalid.
29
+
30
+ ## Theming
31
+
32
+ Override `--psi-radio-group-*` custom properties at any scope.
33
+
34
+
35
+
36
+ ## Rules
37
+
38
+ - One accent per visual group; everything else neutral or ghost.
39
+ - danger only for actions with real consequences.
40
+ - Sizes are px numbers (24|32|40|48), never S/M/L.
41
+ - Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.
42
+ - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
43
+ - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
44
+ - Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.
45
+ - Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination ("Rows per page" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as="form" align="start" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).
46
+ - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
@@ -0,0 +1,43 @@
1
+ <!-- Generated by emit-docs.ts — do not edit. -->
2
+ # Textarea
3
+
4
+ Multi-line text input (D104): the Input recipe on a native `<textarea>`. Inside a Field, id/aria-describedby/aria-invalid/required are wired as for Input (D49). `minLength` and `maxLength` are the native attributes. With `maxLength`, a counter under the control shows the length; it is not a live region (a count spoken on every keystroke is noise) and joins aria-describedby, so it is read with the field on focus.
5
+
6
+ ## Props
7
+
8
+ | Prop | Type | Default | Required | Description |
9
+ |---|---|---|---|---|
10
+ | `rows` | `number` | 3 | no | Visible rows. A textarea's height is its rows; there is no `size`. |
11
+ | `minLength` | `number` | — | no | Native minimum length, reported by constraint validation. |
12
+ | `maxLength` | `number` | — | no | Native maximum length, enforced while typing. With it, the counter shows `{length} / {maxLength}` unless `showCounter={false}`. |
13
+ | `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |
14
+ | `showCounter` | `boolean` | true | no | With `maxLength`, show the `{length} / {maxLength}` counter under the control. It is not a live region; it describes the textarea. |
15
+ | `ref` | `Ref<HTMLTextAreaElement>` | — | no | Forwarded ref to the underlying `<textarea>` element. |
16
+ | `placeholder` | `string` | — | no | Native placeholder text shown while the field is empty (only on components whose host element supports it). |
17
+ | `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |
18
+
19
+ ## Keyboard & assistive tech
20
+
21
+ | Keys | Behavior |
22
+ |---|---|
23
+ | Tab | A native <textarea>; Enter adds a line, it does not submit the form. |
24
+
25
+ Inside a Field, id/aria-describedby/aria-invalid/required are wired as for Input (D104). minLength and maxLength are native. With maxLength, a counter under the control shows {length} / {maxLength}; it is not a live region, and it joins aria-describedby so it is read with the field on focus, with a visually hidden "characters".
26
+
27
+ ## Theming
28
+
29
+ Override `--psi-textarea-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).
30
+
31
+
32
+
33
+ ## Rules
34
+
35
+ - One accent per visual group; everything else neutral or ghost.
36
+ - danger only for actions with real consequences.
37
+ - Sizes are px numbers (24|32|40|48), never S/M/L.
38
+ - Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.
39
+ - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
40
+ - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
41
+ - Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.
42
+ - Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination ("Rows per page" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as="form" align="start" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).
43
+ - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @handamade/psi-react
2
2
 
3
- 47 React 19 components consuming @handamade/psi-tokens. Zero runtime deps, CSS Modules, ref-as-prop.
3
+ 50 React 19 components consuming @handamade/psi-tokens. Zero runtime deps, CSS Modules, ref-as-prop.
4
4
 
5
5
  ## Machine-readable artifacts
6
6
  - dist/manifest.json: full component/prop inventory with types and defaults
7
- - dist/patterns.json: 23 composition patterns (D47) — gap-annotated recipes with clarifying parameters; presets are generated JSX, never hand-written
7
+ - dist/patterns.json: 24 composition patterns (D47) — gap-annotated recipes with clarifying parameters; presets are generated JSX, never hand-written
8
8
  - docs/{Component}.md: per-component docs (props, theming, variant guidance)
9
9
 
10
10
  ## Rules for generated code
@@ -58,7 +58,7 @@
58
58
  - `<Table>` renders `<table>` and is **controlled-only** — it holds no state (D62). Sorting is `sort={{key, direction}}` + `onSortChange`, which is called with the **next** state, already toggled — store it as given (a fresh column arrives `asc`; an active `asc` emits `desc`); selection is `selected` (a `ReadonlySet<string>` keyed by each `<TableRow rowId>`) + `onSelectionChange`. `sortable` and `selectable` only enable the affordances. `size` is `32 | 40 | 48` px (default 40) and `stickyHeader` pins the header. It does no filtering, sorting or slicing of rows — the consumer does that.
59
59
  - `<TableHead>`, `<TableBody>`, `<TableRow>` render `<thead>`, `<tbody>`, `<tr>`. `<TableHeaderCell>` renders `<th scope="col">`, taking `sortKey` to become sortable and putting `aria-sort` on the `<th>`. `<TableCell>` renders `<td>`. Both cells take `numeric`, which right-aligns **and** renders tabular figures.
60
60
  - `<TableCaption detail="Rows 51–100 of more than 100,000">Applications</TableCaption>` renders `<caption>` as the table's first child — its visible name, plus a `detail` line for the range (D85). For a token-paged list give it `tabIndex={-1}` and move focus to it (through its `ref`) after a page change, so the new range is read. `<TableCell colSpan={n}>` spans an empty-state row; `<TableCell rowHeader>` renders `<th scope="row">` for the cell that names its row. Every family member spreads its remaining attributes onto its element (`aria-busy` on `TableBody` while a page loads, `aria-label`, `id`, `data-*`) and takes a `ref`; its own attributes (`data-size`, `scope`, `aria-sort`) win.
61
- - Selecting rows: give each `<TableRow>` a `rowId` and a `selectLabel` — the row checkbox has no visible label, so `selectLabel` is its accessible name. `<TableHead>` grows the select-all checkbox automatically; do not add one. Consult dist/patterns.json for the `data-table` pattern.
61
+ - Selecting rows: give each `<TableRow>` a `rowId` and a `selectLabel` — the row checkbox has no visible label, so `selectLabel` is its accessible name. `<TableHead>` grows the select-all checkbox automatically; do not add one. Outside a Table, `<Checkbox indeterminate>` shows the mixed state (D103): it sets the DOM property (exposed as aria-checked="mixed") on every render while passed, and draws a dash. Consult dist/patterns.json for the `data-table` pattern.
62
62
 
63
63
  ## Pagination and CursorPagination
64
64
  - `<Pagination page={n} pageCount={total} onPageChange={...}>`: a numbered pager with ellipsis truncation (`‹ 1 … 4 5 6 … 13 ›`), built from Button/IconButton (D63). `siblingCount` (default 1) sets how many pages flank the current one before truncating; `aria-current="page"` marks the active page and the ellipsis is inert. `page` outside `[1, pageCount]` (including non-finite values) is clamped for rendering and warns in development (D78).
@@ -67,6 +67,7 @@
67
67
 
68
68
  ## Dialog, and drawers (there is no Drawer component)
69
69
  - `<Dialog open onClose={(reason) => …} title footer width={400|560|720} dismissible>`: modal on the native `<dialog>` top layer. **Controlled-only** (D50) — every dismissal path (`"esc"`, `"backdrop"`, `"close-button"`) only *reports* via `onClose(reason)`; the consumer flips `open`. `dismissible={false}` swallows Esc and backdrop, leaving the footer as the only exit.
70
+ - **A consequential confirmation** (D101): `<Dialog dismissible={false} closeOnEscape initialFocus={cancelRef} footer={<><Button ref={cancelRef}>Cancel</Button><Button variant="danger">Delete</Button></>}>`. Focus starts on Cancel, Escape reports `onClose("esc")`, and there is no close button or backdrop dismissal. `initialFocus` takes a ref and is focused on every open, after the platform's own focusing; React's `autoFocus` does not work inside a closed `<dialog>`. `closeOnEscape` defaults to `dismissible`. `closeLabel="Close"` gives the close button visible text instead of the icon.
70
71
  - **A drawer / side sheet is `<Dialog placement="inline-end">`, not a separate component (D66).** `placement` is `center | inline-start | inline-end`; the `inline-*` values pin the panel full-height to that edge. Do not import `Drawer` — it does not exist and never will. `width` is then the drawer's width; the height is always the viewport.
71
72
  - Placement changes position and nothing else: modality, the focus trap, `aria-modal`, focus restore and the dismissal reasons are identical. Logical (`inline-*`, not left/right), so RTL flips for free. Consult dist/patterns.json for the `detail-drawer` pattern.
72
73
 
@@ -106,12 +107,14 @@
106
107
  - `TabList` requires `aria-label`. The active panel is itself a tab stop, so a panel with no focusable content is still reachable.
107
108
 
108
109
  ## Menu family (4)
109
- - `<Menu open onClose={(reason) => …} trigger={<IconButton …/>} placement="bottom-start|bottom-end|top-start|top-end" aria-label="…">`: action menu on the native Popover API — top layer and light dismiss come from the platform, roving keyboard and dismissal reasons from Psi (D53). **Controlled-only** like Dialog: `onClose(reason)` reports `"esc" | "outside" | "item-select"` and the consumer flips `open`. Compose `<MenuItem onSelect>` and `<MenuSeparator>`; the trigger needs an accessible name, so an icon-only trigger is `<IconButton aria-label>`.
110
+ - `<Menu open onClose={(reason) => …} trigger={<IconButton …/>} placement="bottom-start|bottom-end|top-start|top-end" aria-label="…">`: action menu on the native Popover API — top layer and light dismiss come from the platform, roving keyboard and dismissal reasons from Psi (D53). **Controlled-only** like Dialog: `onClose(reason)` reports `"esc" | "outside" | "item-select" | "tab"` and the consumer flips `open`. The menu-button keyboard is complete (D102): the trigger carries `aria-controls`; Arrow Down / Arrow Up on the closed trigger open it on the first / last item (Menu clicks the trigger, so the trigger's own `onClick` must toggle `open`); Tab reports `"tab"` and lets focus move on. Compose `<MenuItem onSelect>` and `<MenuSeparator>`; the trigger needs an accessible name, so an icon-only trigger is `<IconButton aria-label>`.
110
111
  - `<MenuText>` is a line of text that is not an action (D99), such as a build identifier: inside the menu with no role, in the secondary foreground, never focused, and skipped by the arrows, Home, End and typeahead. It is never `aria-hidden`. To have it spoken when the menu opens, give it an `id` and pass that id as Menu's `aria-describedby` (off by default). Consult dist/patterns.json for `account-menu`.
111
112
  - Many menus, one state: for a row-actions column, hold a single `openMenuId` and render each row's Menu with `open={openMenuId === row.id}`, closing with `onClose={() => setOpenMenuId(null)}`. Do not give each row its own boolean. This is the shape D58 fixed a real dismissal bug in — switching directly between two menus left both closed, because the platform light-dismisses the first before the consumer's click handler runs.
112
113
 
113
114
  ## Compositions (recipes for what has no component yet)
114
115
 
116
+ - `<Textarea maxLength={280}>` is a multi-line text field (D104): the Input recipe on a native `<textarea>`, wired by a Field like Input. `minLength`/`maxLength` are native; with `maxLength` a counter under it shows `{length} / {maxLength}` — not a live region, read with the field through aria-describedby (`showCounter={false}` hides it). No `size`: its height is `rows` (default 3).
117
+ - `<RadioGroup value onValueChange><Radio value="a">A</Radio>…</RadioGroup>` (D104): native radios sharing one name, so the browser supplies Tab and the arrow keys. Put it in `<Field group label required error>`: the fieldset and legend name the group and RadioGroup adds no role; standalone it is `role="radiogroup"` and needs `aria-label`. A required or invalid Field makes every radio `required` and `aria-invalid`. `orientation="horizontal"` lays them in a row. Consult dist/patterns.json for `feedback-form`.
115
118
  - Form field: wrap the label, control, and message with `<Field label description error required>{control}</Field>` — label association (htmlFor/id), the description/error message line (swaps to error, aria-live="polite"; `announce={false}` drops the live region for an app that routes every announcement through one announcer of its own — the message still describes the control, D81), and aria-describedby/aria-invalid wiring onto a nested Input/Select are automatic (D49). `required` marks the label with an asterisk; `requiredText="(Required)"` states it in a word instead, the label's colour, aria-hidden because the control's `required` is the signal (D84). A control Psi did not write joins a Field with `const field = useFieldControl()` spread onto its focusable element — it returns `id`, `aria-labelledby` (a `<label for>` names only a labelable element, and a custom control is usually a div), `aria-describedby`, `aria-invalid`, `aria-required` and `required` as the Field sets them, and `{}` outside a Field (D84); the element needs a role that admits aria-required, such as combobox. `group` mode renders fieldset/legend for self-labeled controls (Checkbox/Switch). Standalone Input/Select used outside Field still need `aria-invalid` and `aria-describedby` paired manually. Spacing: `psi-gap-24` between fields, `psi-gap-12` for toggle groups, `psi-gap-8` for button rows and label+Tag rows.
116
119
  - Stacks/rows: `.psi-gap-*` utilities set only the gap — they do not create a flex/grid container. Pair them with `display: flex` (column for stacks, row for button groups).
117
120
  - Page-level spacing: the form scale above (24/12/8) governs controls inside a form, not page layout. For layout use `psi-gap-32` between sections of a page, `psi-gap-24` between cards or panels in a grid, `psi-py-32` for a section's vertical padding, and `.psi-container` for the page's max-width and gutters (it is in utilities.css, not components.css). Marketing-scale rhythm is app-level and deliberately not tokenized — the system stops at the container.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handamade/psi-react",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,7 +47,7 @@
47
47
  "tsx": "^4.22.4",
48
48
  "vite": "^6.0.0",
49
49
  "vitest": "^3.2.6",
50
- "@handamade/psi-tokens": "0.24.0"
50
+ "@handamade/psi-tokens": "0.26.0"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "vite build && tsc -p tsconfig.build.json && tsx scripts/emit-styles-dts.ts && tsx scripts/emit-manifest.ts && tsx scripts/emit-patterns.ts && tsx scripts/emit-docs.ts",