@handamade/psi-react 0.21.0 → 0.23.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.
Files changed (72) hide show
  1. package/README.md +17 -0
  2. package/dist/Field/Field.d.ts +5 -1
  3. package/dist/Field/Field.d.ts.map +1 -1
  4. package/dist/NavTree/NavItem.d.ts +12 -8
  5. package/dist/NavTree/NavItem.d.ts.map +1 -1
  6. package/dist/Tabs/Tab.d.ts.map +1 -1
  7. package/dist/Tabs/Tabs.d.ts +20 -1
  8. package/dist/Tabs/Tabs.d.ts.map +1 -1
  9. package/dist/Toast/Announcement.d.ts +12 -4
  10. package/dist/Toast/Announcement.d.ts.map +1 -1
  11. package/dist/Toast/ToastProvider.d.ts +27 -4
  12. package/dist/Toast/ToastProvider.d.ts.map +1 -1
  13. package/dist/Toast/ToastRegion.d.ts +11 -2
  14. package/dist/Toast/ToastRegion.d.ts.map +1 -1
  15. package/dist/Toast/useToast.d.ts +11 -2
  16. package/dist/Toast/useToast.d.ts.map +1 -1
  17. package/dist/Toolbar/Toolbar.d.ts +16 -6
  18. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1050 -1015
  22. package/dist/manifest.json +32 -11
  23. package/dist/patterns.json +4 -4
  24. package/dist/styles.css +1 -1
  25. package/docs/Announcement.md +3 -3
  26. package/docs/AppShell.md +1 -1
  27. package/docs/AspectRatio.md +1 -1
  28. package/docs/Banner.md +1 -1
  29. package/docs/Button.md +1 -1
  30. package/docs/Card.md +1 -1
  31. package/docs/Checkbox.md +1 -1
  32. package/docs/CopyButton.md +1 -1
  33. package/docs/CursorPagination.md +1 -1
  34. package/docs/DescriptionItem.md +1 -1
  35. package/docs/DescriptionList.md +1 -1
  36. package/docs/Dialog.md +1 -1
  37. package/docs/Field.md +2 -2
  38. package/docs/IconButton.md +1 -1
  39. package/docs/InlineAlert.md +1 -1
  40. package/docs/Input.md +1 -1
  41. package/docs/Menu.md +1 -1
  42. package/docs/MenuItem.md +1 -1
  43. package/docs/MenuSeparator.md +1 -1
  44. package/docs/NavBar.md +1 -1
  45. package/docs/NavGroup.md +1 -1
  46. package/docs/NavItem.md +5 -5
  47. package/docs/NavTree.md +1 -1
  48. package/docs/Pagination.md +1 -1
  49. package/docs/Panel.md +1 -1
  50. package/docs/Select.md +1 -1
  51. package/docs/Skeleton.md +1 -1
  52. package/docs/SkipLink.md +1 -1
  53. package/docs/Switch.md +1 -1
  54. package/docs/Tab.md +1 -1
  55. package/docs/TabList.md +1 -1
  56. package/docs/TabPanel.md +1 -1
  57. package/docs/Table.md +1 -1
  58. package/docs/TableBody.md +1 -1
  59. package/docs/TableCaption.md +1 -1
  60. package/docs/TableCell.md +1 -1
  61. package/docs/TableHead.md +1 -1
  62. package/docs/TableHeaderCell.md +1 -1
  63. package/docs/TableRow.md +1 -1
  64. package/docs/Tabs.md +3 -3
  65. package/docs/Tag.md +1 -1
  66. package/docs/Toast.md +1 -1
  67. package/docs/ToastProvider.md +6 -3
  68. package/docs/ToastRegion.md +4 -4
  69. package/docs/Toolbar.md +2 -2
  70. package/docs/Tooltip.md +1 -1
  71. package/llms.txt +10 -7
  72. package/package.json +3 -2
package/docs/NavBar.md CHANGED
@@ -30,5 +30,5 @@ Override `--psi-navbar-*` custom properties at any scope; interactive states der
30
30
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
31
31
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
32
32
  - 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.
33
- - 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="end" of Fields (D87).
33
+ - 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).
34
34
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/NavGroup.md CHANGED
@@ -36,5 +36,5 @@ Override `--psi-nav-tree-*` custom properties at any scope; interactive states d
36
36
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
37
37
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
38
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="end" of Fields (D87).
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
40
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/NavItem.md CHANGED
@@ -1,15 +1,15 @@
1
1
  <!-- Generated by emit-docs.ts — do not edit. -->
2
2
  # NavItem
3
3
 
4
- One entry of a `NavTree` (D88): an `<li>` that styles the single anchor inside it. Router apps pass their own link as the child, so it keeps its `href`, `aria-current` and handlers; `href` is for plain links and for presets, where the compose tree can hold only manifest components and a raw `<a>` is not one. The current page is never marked by colour alone: medium weight, a raised surface and a bar at the inline start.
4
+ One entry of a `NavTree` (D88): an `<li>` that styles the single anchor inside it. Router apps pass their own link as the child, so it keeps its `href` and handlers; with `current`, `NavItem` clones it to add `aria-current="page"` (D93), as `Menu` clones its trigger. `href` is for plain links and for presets, where the compose tree can hold only manifest components and a raw `<a>` is not one. The current page is never marked by colour alone: medium weight, a raised surface and a bar at the inline start.
5
5
 
6
6
  ## Props
7
7
 
8
8
  | Prop | Type | Default | Required | Description |
9
9
  |---|---|---|---|---|
10
- | `children` | `ReactNode` | — | yes | Without `href`: one anchor, your router's link — put `aria-current="page"` on the current one. With `href`: the link's text. |
10
+ | `children` | `ReactNode` | — | yes | Without `href`: one anchor, your router's link. With `href`: the link's text. |
11
11
  | `href` | `string` | — | no | Render the anchor yourself (D88), for a plain link or a preset: the `<li>` holds `<a href>` with `children` as its text. Router apps leave it out and pass their own link as the child, so the link keeps its own handlers. |
12
- | `current` | `boolean` | — | no | Mark the link as the current page (`aria-current="page"`). Only with `href`; a link you pass as a child carries its own `aria-current`. |
12
+ | `current` | `boolean` | — | no | Mark the link as the current page (`aria-current="page"`): on the anchor `href` renders, or on the single element you pass as a child (D93). Without `current`, a child's own `aria-current` is left alone, so a router that marks its active link keeps working. |
13
13
  | `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |
14
14
  | `ref` | `Ref<HTMLLIElement>` | — | no | Forwarded ref to the `<li>`. |
15
15
 
@@ -19,7 +19,7 @@ One entry of a `NavTree` (D88): an `<li>` that styles the single anchor inside i
19
19
  |---|---|
20
20
  | Enter | Follows the link inside it (native anchor behavior); the link is the consumer's router link. |
21
21
 
22
- An <li> that styles the one anchor inside it. Router apps pass their own link as the child and set aria-current="page" on the current one; with href the NavItem renders the anchor itself (for plain links and presets) and current sets aria-current="page" on it. The current page is marked by weight, a raised surface and an inline-start bar, never by colour alone, and the focus ring is drawn inset on the anchor.
22
+ An <li> that styles the one anchor inside it. Router apps pass their own link as the child; current clones it with aria-current="page" (D93), and without current the child keeps its own aria-current; with href the NavItem renders the anchor itself (for plain links and presets) and current sets aria-current="page" on it. The current page is marked by weight, a raised surface and an inline-start bar, never by colour alone, and the focus ring is drawn inset on the anchor.
23
23
 
24
24
  ## Theming
25
25
 
@@ -36,5 +36,5 @@ Override `--psi-nav-tree-*` custom properties at any scope; interactive states d
36
36
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
37
37
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
38
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="end" of Fields (D87).
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
40
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/NavTree.md CHANGED
@@ -35,5 +35,5 @@ Override `--psi-nav-tree-*` custom properties at any scope; interactive states d
35
35
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
36
36
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
37
37
  - 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.
38
- - 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="end" of Fields (D87).
38
+ - 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).
39
39
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
@@ -29,5 +29,5 @@ This component has no `--psi-pagination-*` tokens — its styling binds scale to
29
29
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
30
30
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
31
31
  - 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.
32
- - 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="end" of Fields (D87).
32
+ - 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).
33
33
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Panel.md CHANGED
@@ -35,5 +35,5 @@ Override `--psi-panel-*` custom properties at any scope.
35
35
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
36
36
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
37
37
  - 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.
38
- - 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="end" of Fields (D87).
38
+ - 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).
39
39
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Select.md CHANGED
@@ -37,5 +37,5 @@ Override `--psi-select-*` custom properties at any scope; interactive states der
37
37
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
38
38
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
39
39
  - 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.
40
- - 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="end" of Fields (D87).
40
+ - 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).
41
41
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Skeleton.md CHANGED
@@ -46,5 +46,5 @@ Override `--psi-skeleton-*` custom properties at any scope.
46
46
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
47
47
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
48
48
  - 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.
49
- - 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="end" of Fields (D87).
49
+ - 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).
50
50
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/SkipLink.md CHANGED
@@ -36,5 +36,5 @@ Override `--psi-skip-link-*` custom properties at any scope.
36
36
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
37
37
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
38
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="end" of Fields (D87).
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
40
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Switch.md CHANGED
@@ -33,5 +33,5 @@ Override `--psi-switch-*` custom properties at any scope.
33
33
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
34
34
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
35
35
  - 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.
36
- - 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="end" of Fields (D87).
36
+ - 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).
37
37
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Tab.md CHANGED
@@ -36,5 +36,5 @@ This component has no `--psi-tab-*` tokens — its styling binds scale tokens on
36
36
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
37
37
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
38
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="end" of Fields (D87).
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
40
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/TabList.md CHANGED
@@ -37,5 +37,5 @@ This component has no `--psi-tab-list-*` tokens — its styling binds scale toke
37
37
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
38
38
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
39
39
  - 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.
40
- - 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="end" of Fields (D87).
40
+ - 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).
41
41
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/TabPanel.md CHANGED
@@ -35,5 +35,5 @@ This component has no `--psi-tab-panel-*` tokens — its styling binds scale tok
35
35
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
36
36
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
37
37
  - 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.
38
- - 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="end" of Fields (D87).
38
+ - 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).
39
39
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Table.md CHANGED
@@ -34,5 +34,5 @@ Override `--psi-table-*` custom properties at any scope; interactive states deri
34
34
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
35
35
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
36
36
  - 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.
37
- - 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="end" of Fields (D87).
37
+ - 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).
38
38
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/TableBody.md CHANGED
@@ -26,5 +26,5 @@ This component has no `--psi-table-body-*` tokens — its styling binds scale to
26
26
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
27
27
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
28
28
  - 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.
29
- - 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="end" of Fields (D87).
29
+ - 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).
30
30
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
@@ -35,5 +35,5 @@ This component has no `--psi-table-caption-*` tokens — its styling binds scale
35
35
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
36
36
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
37
37
  - 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.
38
- - 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="end" of Fields (D87).
38
+ - 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).
39
39
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/TableCell.md CHANGED
@@ -29,5 +29,5 @@ This component has no `--psi-table-cell-*` tokens — its styling binds scale to
29
29
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
30
30
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
31
31
  - 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.
32
- - 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="end" of Fields (D87).
32
+ - 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).
33
33
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/TableHead.md CHANGED
@@ -26,5 +26,5 @@ This component has no `--psi-table-head-*` tokens — its styling binds scale to
26
26
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
27
27
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
28
28
  - 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.
29
- - 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="end" of Fields (D87).
29
+ - 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).
30
30
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
@@ -28,5 +28,5 @@ This component has no `--psi-table-header-cell-*` tokens — its styling binds s
28
28
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
29
29
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
30
30
  - 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.
31
- - 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="end" of Fields (D87).
31
+ - 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).
32
32
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/TableRow.md CHANGED
@@ -28,5 +28,5 @@ This component has no `--psi-table-row-*` tokens — its styling binds scale tok
28
28
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
29
29
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
30
30
  - 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.
31
- - 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="end" of Fields (D87).
31
+ - 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).
32
32
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Tabs.md CHANGED
@@ -7,7 +7,7 @@ Tab set root (D67) — holds no selection state of its own, following D50, D53 a
7
7
 
8
8
  | Prop | Type | Default | Required | Description |
9
9
  |---|---|---|---|---|
10
- | `value` | `string` | — | yes | Controlled selected tab, matched against each `Tab`/`TabPanel` value. |
10
+ | `value` | `string` | — | yes | Controlled selected tab, matched against each `Tab`/`TabPanel` value. A value that matches no tab selects nothing, and the first enabled tab holds the list's tab stop (D93); development builds warn. |
11
11
  | `onValueChange` | `(value: string) => void` | — | yes | Fires with the newly selected value; the consumer flips `value`. |
12
12
  | `orientation` | `"horizontal" \| "vertical"` | horizontal | no | Axis of the tab list, which also picks the arrow keys. |
13
13
  | `children` | `ReactNode` | — | yes | A `TabList` and one `TabPanel` per tab. |
@@ -18,7 +18,7 @@ Tab set root (D67) — holds no selection state of its own, following D50, D53 a
18
18
 
19
19
  | Keys | Behavior |
20
20
  |---|---|
21
- | Tab | Enters the tab list at its selected tab (one stop for the whole list), then moves on to the active panel. |
21
+ | Tab | Enters the tab list at its selected tab (one stop for the whole list), then moves on to the active panel. When value matches no tab, the first enabled tab holds the stop (the first tab if all are disabled) and none is selected; a selected disabled tab keeps the stop (D93). |
22
22
 
23
23
  Controlled-only (D67): value and onValueChange are required and Tabs never selects itself. Tab and TabPanel pair by string value, not index, so their source order need not match. Every panel renders and unselected ones carry `hidden`, so aria-controls always resolves and panel DOM state survives a switch.
24
24
 
@@ -37,5 +37,5 @@ Override `--psi-tabs-*` custom properties at any scope; interactive states deriv
37
37
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
38
38
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
39
39
  - 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.
40
- - 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="end" of Fields (D87).
40
+ - 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).
41
41
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Tag.md CHANGED
@@ -47,5 +47,5 @@ Override `--psi-tag-*` custom properties at any scope.
47
47
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
48
48
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
49
49
  - 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.
50
- - 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="end" of Fields (D87).
50
+ - 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).
51
51
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Toast.md CHANGED
@@ -50,5 +50,5 @@ Override `--psi-toast-*` custom properties at any scope.
50
50
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
51
51
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
52
52
  - 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.
53
- - 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="end" of Fields (D87).
53
+ - 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).
54
54
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
@@ -1,7 +1,7 @@
1
1
  <!-- Generated by emit-docs.ts — do not edit. -->
2
2
  # ToastProvider
3
3
 
4
- Owns the toast queue, its auto-dismiss timers, and the single ToastRegion (D65). This is the library's one stateful container, and the exception is deliberately narrow. D50 and D53 rejected internal state for Dialog and Menu because the consumer already owned the state that decided visibility — a menu is open because a user clicked a trigger the consumer rendered. A toast has no such owner: it is created by an outcome, not by a UI state, and it disappears on a timer nobody is watching. The rule that survives is the useful half — presentational components stay controlled (`Toast` still holds nothing), a stateful container may exist when the state has no natural owner, and it must be opt-in. Timers pause while the pointer or focus is inside the region (WCAG 2.2.1), and resume with the time *remaining* rather than a fresh full duration — restarting would let a user hold a toast open indefinitely by jiggling the mouse.
4
+ Owns the toast queue, its auto-dismiss timers, and the single ToastRegion (D65). This is the library's one stateful container, and the exception is deliberately narrow. D50 and D53 rejected internal state for Dialog and Menu because the consumer already owned the state that decided visibility — a menu is open because a user clicked a trigger the consumer rendered. A toast has no such owner: it is created by an outcome, not by a UI state, and it disappears on a timer nobody is watching. The rule that survives is the useful half — presentational components stay controlled (`Toast` still holds nothing), a stateful container may exist when the state has no natural owner, and it must be opt-in. Timers pause while the pointer or focus is inside the region (WCAG 2.2.1), and resume with the time *remaining* rather than a fresh full duration — restarting would let a user hold a toast open indefinitely by jiggling the mouse. **Announcements share the queue, not the limit (D91).** `announce()` adds a speech-only Announcement to the same region — never a live region of its own — and it leaves after a one-second dwell. A later announcement does not remove an earlier one early: removing a node just after it was added can cut its speech, and with non-atomic wrappers (D90) each added node is spoken on its own. Announcements live in their own list, so they never evict a visible toast; their timers live in the same map, so `dismiss`, `clear()` and unmount dispose them as they do a toast's, and they pause with the region. Remaining props go to the ToastRegion (D91): its `aria-label`, a `ref` to the region element, a `className`, and `data-*` attributes such as `data-react-aria-top-layer`.
5
5
 
6
6
  ## Props
7
7
 
@@ -12,6 +12,9 @@ Owns the toast queue, its auto-dismiss timers, and the single ToastRegion (D65).
12
12
  | `actionDuration` | `number` | 10000 | no | Auto-dismiss for toasts carrying an action, in ms. An affordance that vanishes before it can be reached is not an affordance. |
13
13
  | `placement` | `"top-start" \| "top-end" \| "bottom-start" \| "bottom-end"` | bottom-end | no | Corner the stack occupies. |
14
14
  | `children` | `ReactNode` | — | yes | The subtree that may call `useToast()`. The region is rendered alongside it. |
15
+ | `aria-label` | `string` | "Notifications" | no | Accessible name for the region landmark, forwarded to ToastRegion. |
16
+ | `className` | `string` | — | no | Forwarded to the region element. |
17
+ | `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the region element. |
15
18
 
16
19
  ## Keyboard & assistive tech
17
20
 
@@ -19,7 +22,7 @@ Owns the toast queue, its auto-dismiss timers, and the single ToastRegion (D65).
19
22
  |---|---|
20
23
  | Tab | Focus entering the region pauses every auto-dismiss timer; leaving resumes them. |
21
24
 
22
- Owns the queue, the auto-dismiss timers and the single ToastRegion (D65). Timers pause while the pointer or focus is inside the region and resume with the time remaining, satisfying WCAG 2.2.1 for content that disappears on a timer. Toasts carrying an action get a longer default lifetime, so the affordance cannot vanish before it is reached. useToast() throws outside a provider rather than silently no-opping.
25
+ Owns the queue, the auto-dismiss timers and the single ToastRegion (D65). Timers pause while the pointer or focus is inside the region and resume with the time remaining, satisfying WCAG 2.2.1 for content that disappears on a timer. Toasts carrying an action get a longer default lifetime, so the affordance cannot vanish before it is reached. useToast() throws outside a provider rather than silently no-opping. announce(message, { politeness }) speaks through the same region without showing anything (D91): it renders an Announcement into the wrapper its politeness names (default polite), never a live region of its own, so the application keeps one announcer. An announcement leaves after a one-second dwell, is not removed early by a later one, and never counts against limit, so it cannot evict a visible toast. Its timer pauses with the region and is disposed by dismiss, clear() and unmount like a toast's. Remaining props (aria-label, ref, className, data-* such as data-react-aria-top-layer) are forwarded to the region element.
23
26
 
24
27
  ## Theming
25
28
 
@@ -36,5 +39,5 @@ This component has no `--psi-toast-provider-*` tokens — its styling binds scal
36
39
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
37
40
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
38
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.
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="end" of Fields (D87).
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).
40
43
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
@@ -1,14 +1,14 @@
1
1
  <!-- Generated by emit-docs.ts — do not edit. -->
2
2
  # ToastRegion
3
3
 
4
- The positioned live region that holds the toast stack (D64). Two things here are load-bearing and both look like details: 1. **The two live wrappers are always rendered**, empty queue included. A live region announces mutations to a subtree that already existed; a wrapper that mounts together with its first toast reads as a new subtree, and that first toast is never announced. 2. **`popover="manual"`, not `"auto"`.** Manual puts the region in the native top layer without light dismiss. The top layer is required because Dialog uses showModal() — also top layer — so a fixed region at --psi-z-overlay would paint *under* the modal backdrop, hiding the confirmation for the action a user just took inside a dialog. And `auto` would be dismissed by the very click that raised the toast. Routing reads `politeness`, then `variant`, off each child — the same Children.map technique Table uses for select-all injection. `politeness` wins (D83): the owner of an announcement knows what kind of event it is, and tone is only the default. Consequence of the split, visible in the `InRegion` VR baseline: the stack is grouped by politeness, not strictly chronological — every assertive toast sorts below every polite one regardless of arrival order. Chronological order is preserved *within* each group. Keeping it exact across both would mean one live region with a politeness that changes per message, which is the thing the two wrappers exist to avoid. Accepted: at `limit` 3 the grouping reads as severity ordering, and the newest toast still lands nearest the screen edge within its group. Remaining HTML attributes, `data-*` included, are spread onto the root element (D83). That is how an application marks the region `data-react-aria-top-layer`, so that a React Aria overlay — which hides everything outside itself with `aria-hidden` — leaves the two live regions exposed. The region's own attributes always win.
4
+ The positioned live region that holds the toast stack (D64). Two things here are load-bearing and both look like details: 1. **The two live wrappers are always rendered**, empty queue included. A live region announces mutations to a subtree that already existed; a wrapper that mounts together with its first toast reads as a new subtree, and that first toast is never announced. 2. **`popover="manual"`, not `"auto"`.** Manual puts the region in the native top layer without light dismiss. The top layer is required because Dialog uses showModal() — also top layer — so a fixed region at --psi-z-overlay would paint *under* the modal backdrop, hiding the confirmation for the action a user just took inside a dialog. And `auto` would be dismissed by the very click that raised the toast. Routing reads `politeness`, then `variant`, off each child — the same Children.map technique Table uses for select-all injection. `politeness` wins (D83): the owner of an announcement knows what kind of event it is, and tone is only the default. Consequence of the split, visible in the `InRegion` VR baseline: the stack is grouped by politeness, not strictly chronological — every assertive toast sorts below every polite one regardless of arrival order. Chronological order is preserved *within* each group. Keeping it exact across both would mean one live region with a politeness that changes per message, which is the thing the two wrappers exist to avoid. Accepted: at `limit` 3 the grouping reads as severity ordering, and the newest toast still lands nearest the screen edge within its group. Remaining HTML attributes, `data-*` included, are spread onto the root element (D83). That is how an application marks the region `data-react-aria-top-layer`, so that a React Aria overlay — which hides everything outside itself with `aria-hidden` — leaves the two live regions exposed. The region's own attributes always win — `role` included. **The region is a landmark, and the wrappers speak only what is new (D90).** `role="region"` makes the `aria-label` legal (a name on a role-less `div` is prohibited) and gives the stack a place in the landmark list. Both wrappers set `aria-atomic="false"` explicitly: `role="status"` and `role="alert"` carry an implicit `true`, which lets a screen reader re-speak everything already in the wrapper whenever one message is added beside it.
5
5
 
6
6
  ## Props
7
7
 
8
8
  | Prop | Type | Default | Required | Description |
9
9
  |---|---|---|---|---|
10
10
  | `placement` | `"top-start" \| "top-end" \| "bottom-start" \| "bottom-end"` | bottom-end | no | Corner the stack occupies. |
11
- | `aria-label` | `string` | Notifications | no | Accessible name for the region. |
11
+ | `aria-label` | `string` | Notifications | no | Accessible name for the region landmark. |
12
12
  | `children` | `ReactNode` | — | yes | The stack — `Toast` and `Announcement` elements, each routed to a live wrapper by its `politeness`, or by a Toast's variant when it names none. |
13
13
  | `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |
14
14
  | `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the region element. |
@@ -19,7 +19,7 @@ The positioned live region that holds the toast stack (D64). Two things here are
19
19
  |---|---|
20
20
  | Tab | Moves into the stacked toasts' controls in DOM order; the region itself is not focusable. |
21
21
 
22
- Renders two always-present live wrappers — role="status"/aria-live="polite" and role="alert"/aria-live="assertive" — and routes each child into one. A Toast or an Announcement that names its politeness goes where it says (D83); a Toast that names none is routed by variant (neutral/success polite, warning/danger assertive). Whatever it holds, the region contains exactly two live regions. Remaining HTML attributes, data-* included, land on the root element, so it can be marked data-react-aria-top-layer and stay exposed while a React Aria overlay is open. Both stay in the DOM when the queue is empty: a live region announces mutations to a subtree that already existed, so a wrapper mounting with its first toast would leave that toast unannounced. Sits on the native top layer via popover="manual", so a toast raised from inside a modal Dialog is still painted above the backdrop and still announced — though showModal() makes everything outside the dialog inert, so it cannot be clicked until the dialog closes; manual (not auto) means no light dismiss, so the click that raised the toast cannot close it. The region is click-through (pointer-events: none) and each toast takes its own clicks back.
22
+ Is itself a role="region" landmark named by aria-label (default "Notifications"; a caller's role never replaces it, D90), and renders two always-present live wrappers — role="status"/aria-live="polite" and role="alert"/aria-live="assertive", both aria-atomic="false" so a screen reader speaks only the message that was added, not what is already there (D90) — and routes each child into one. A Toast or an Announcement that names its politeness goes where it says (D83); a Toast that names none is routed by variant (neutral/success polite, warning/danger assertive). Whatever it holds, the region contains exactly two live regions. Remaining HTML attributes, data-* included, land on the root element, so it can be marked data-react-aria-top-layer and stay exposed while a React Aria overlay is open. Both stay in the DOM when the queue is empty: a live region announces mutations to a subtree that already existed, so a wrapper mounting with its first toast would leave that toast unannounced. Sits on the native top layer via popover="manual", so a toast raised from inside a modal Dialog is still painted above the backdrop and still announced — though showModal() makes everything outside the dialog inert, so it cannot be clicked until the dialog closes; manual (not auto) means no light dismiss, so the click that raised the toast cannot close it. The region is click-through (pointer-events: none) and each toast takes its own clicks back.
23
23
 
24
24
  ## Theming
25
25
 
@@ -36,5 +36,5 @@ This component has no `--psi-toast-region-*` tokens — its styling binds scale
36
36
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
37
37
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
38
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="end" of Fields (D87).
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
40
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Toolbar.md CHANGED
@@ -9,7 +9,7 @@ Horizontal grouping row for filter/search controls (D52). Wraps on overflow; zer
9
9
  |---|---|---|---|---|
10
10
  | `children` | `ReactNode` | — | no | The controls. |
11
11
  | `gap` | `8 \| 12 \| 16` | 8 | no | Gap between controls in px. |
12
- | `align` | `"center" \| "end"` | center | no | Cross-axis alignment. `end` lines items up on their bottom edge, so controls under visible Field labels and a button without one share a control line (D87). It lines up each item's *last* line: a Field in an end-aligned row must end on its control — no description or error line under it; put a hint in the label. |
12
+ | `align` | `"center" \| "end" \| "start"` | center | no | Cross-axis alignment of the row's items. `center` (the default) centres them, for a row of controls without visible labels such as filter-toolbar. `end` and `start` are for a row of Fields with visible labels beside an unlabelled button. `end` (D87) lines items up on their bottom edge. It lines up each item's *last* line, so a Field in an end-aligned row must end on its control — no description or error line under it; use `start` for one. `start` (D92) lines up the controls: items share their top edge, and a *direct* `button` or `a` child drops by `--psi-toolbar-action-offset` (one Field label line plus the Field gap) onto the control line. Only a direct child is offset: a wrapped trigger (a Tooltip-wrapped button, a Menu trigger) is not. A Field's description or error line hangs below its control and moves nothing else. Every label in the row must fit on one line; a label that wraps pushes its control below the others. |
13
13
  | `as` | `"div" \| "form"` | div | no | Root element. `form` renders a real `<form>`, so a submit Button submits it and Enter in a field does too; with `aria-label` it is a named form landmark, so it takes no `role="group"` (D87). Wire `onSubmit`. |
14
14
  | `ref` | `Ref<HTMLDivElement \| HTMLFormElement>` | — | no | Forwarded ref to the root element. |
15
15
  | `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |
@@ -37,5 +37,5 @@ Override `--psi-toolbar-*` custom properties at any scope.
37
37
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
38
38
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
39
39
  - 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.
40
- - 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="end" of Fields (D87).
40
+ - 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).
41
41
  - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Tooltip.md CHANGED
@@ -35,5 +35,5 @@ Override `--psi-tooltip-*` custom properties at any scope.
35
35
  - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
36
36
  - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
37
37
  - 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.
38
- - 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="end" of Fields (D87).
38
+ - 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).
39
39
  - 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
@@ -35,7 +35,8 @@
35
35
 
36
36
  ## Toolbar
37
37
  - <Toolbar gap={8|12|16} aria-label="Filters">: horizontal wrapping row for filter/search controls (Input, Select, Tag, Button). Wraps on overflow (D52); with aria-label it announces as role=group. Deliberately not ARIA role=toolbar (no roving tabindex). Unblocks the filter-toolbar pattern — consult dist/patterns.json for the preset.
38
- - `align="end"` lines items up on their bottom edge, so Field-wrapped controls (label above) and a button without a label share one control line (D87). It aligns each item's last line: a Field in that row must end on its control — no description or error under it.
38
+ - `align="start"` lines up the controls of Field-wrapped filters (label above) and a button without a label (D92): items share their top edge, and a direct `button`/`a` child drops by `--psi-toolbar-action-offset` (= `--psi-field-label-height` + `--psi-field-gap`, 26px) onto the control line. A Field's description or error line hangs below its control and moves nothing else — set `error` on the Field. Every label in the row must be one line; a wrapped label pushes its control off the line. filter-form uses it.
39
+ - `align="end"` lines items up on their bottom edge (D87). It aligns each item's last line, so a Field in that row must end on its control — no description or error under it; use `start` when a field can show an error.
39
40
  - `as="form"` renders a real `<form>`: a `Button type="submit"` and Enter in a field submit it; wire `onSubmit`. With `aria-label` it is a named form landmark (no role=group). Filters that apply as they change, named by aria-label → filter-toolbar; labelled filters submitted together with *Find* → filter-form. Never name a control by its placeholder alone.
40
41
 
41
42
  ## NavBar
@@ -75,14 +76,14 @@
75
76
 
76
77
  ## Toast family (4) + useToast
77
78
  - `<Toast variant="neutral|success|warning|danger" action={...} onDismiss={...}>message</Toast>` is presentational and **controlled** (D64): it holds no state, runs no timer and never removes itself — `onDismiss` reports and the owner disposes. Variants are Tag's **status** axis, not the flat action variants; there is deliberately no `accent`. The variant's meaning is announced by a visually hidden status word, so never rely on the icon colour alone; `statusLabel="Fehler:"` replaces that word with a translated one and `statusLabel={null}` drops it (D83).
78
- - `<ToastRegion placement="bottom-end">` is the positioned container and must wrap every toast. It renders **two always-present live wrappers** (`role="status"` polite, `role="alert"` assertive) and routes each child into one: by its `politeness` prop when it names one, otherwise by variant — neutral/success polite, warning/danger assertive. Set `politeness="assertive"` on a success toast when the outcome of the user's own action should interrupt (D83). Whatever it holds, the region is exactly two live regions. Other HTML attributes, `data-*` included, land on its root element: mark it `data-react-aria-top-layer` if the app uses React Aria overlays, or they hide it. It sits on the native top layer via `popover="manual"`, so a toast raised from inside a modal `Dialog` is still painted above the backdrop and still announced — though not clickable until the dialog closes, since `showModal()` makes everything outside the dialog inert. Raise toasts *after* the dialog closes if they carry an `action`.
79
- - `<ToastProvider limit={3} duration={5000} actionDuration={10000}>` owns the queue, the timers and the single region — the library's **one stateful container** (D65), and opt-in. Inside it, `const toast = useToast()` gives `show({variant, message, action, politeness, statusLabel}) => id`, `dismiss(id)` and `clear()`. Timers pause while the pointer or focus is in the region and resume with the time remaining (WCAG 2.2.1); toasts carrying an action get the longer lifetime.
80
- - `<Announcement politeness="polite|assertive">text</Announcement>` is **not a live region** (D83). It is visually hidden text that `ToastRegion` places in its polite (default) or assertive wrapper — for an event that must be spoken and has nothing to show, such as a state change of the object on screen, or field errors the form already displays. It renders no `role` and no `aria-live`, so outside a `ToastRegion` it announces nothing: never use it as a second announcer. It never removes itself; remove it once spoken, and to announce the same text again render it again with a new `key`. Consult dist/patterns.json for the `announcer` pattern.
79
+ - `<ToastRegion placement="bottom-end">` is the positioned container and must wrap every toast. It is itself a `role="region"` landmark named by `aria-label` (default "Notifications"; a `role` you pass never replaces it) and renders **two always-present live wrappers** (`role="status"` polite, `role="alert"` assertive, both `aria-atomic="false"` so only the message just added is spoken) (D90), and routes each child into one: by its `politeness` prop when it names one, otherwise by variant — neutral/success polite, warning/danger assertive. Set `politeness="assertive"` on a success toast when the outcome of the user's own action should interrupt (D83). Whatever it holds, the region is exactly two live regions. Other HTML attributes, `data-*` included, land on its root element: mark it `data-react-aria-top-layer` if the app uses React Aria overlays, or they hide it. It sits on the native top layer via `popover="manual"`, so a toast raised from inside a modal `Dialog` is still painted above the backdrop and still announced — though not clickable until the dialog closes, since `showModal()` makes everything outside the dialog inert. Raise toasts *after* the dialog closes if they carry an `action`.
80
+ - `<ToastProvider limit={3} duration={5000} actionDuration={10000}>` owns the queue, the timers and the single region — the library's **one stateful container** (D65), and opt-in. Inside it, `const toast = useToast()` gives `show({variant, message, action, politeness, statusLabel}) => id`, `announce(message, {politeness}) => id`, `dismiss(id)` and `clear()`. `announce()` speaks without showing anything (D91): it renders an `Announcement` into the provider's one region — never a second live region — leaves after a one-second dwell, is not removed early by a later one, and never counts against `limit`. Region props (`aria-label`, `ref`, `className`, `data-*` such as `data-react-aria-top-layer`) given to `ToastProvider` are forwarded to the region element. Timers pause while the pointer or focus is in the region and resume with the time remaining (WCAG 2.2.1); toasts carrying an action get the longer lifetime.
81
+ - `<Announcement politeness="polite|assertive">text</Announcement>` is **not a live region** (D83). It is visually hidden text that `ToastRegion` places in its polite (default) or assertive wrapper — for an event that must be spoken and has nothing to show, such as a state change of the object on screen, or field errors the form already displays. It renders no `role` and no `aria-live`, so outside a `ToastRegion` it announces nothing: never use it as a second announcer. Under a `ToastProvider`, raise it with `useToast().announce()` instead of rendering it, and the provider removes it after its dwell (D91). Rendered by hand it never removes itself; remove it after the same one-second dwell, not before, and to announce the same text again render it again with a new `key`. The `announcer` pattern in dist/patterns.json is for an application that renders its own `ToastRegion` and no `ToastProvider`; under a provider, never render a second region — call `announce()`.
81
82
  - **Do not hand-roll a queue or a `setTimeout`** — that is what `ToastProvider` is for. Do not render a bare `<Toast>` outside a `ToastRegion`: it would announce nothing. `useToast()` throws outside a provider rather than silently doing nothing.
82
83
 
83
84
  ## NavTree family (3)
84
85
  - `<NavTree aria-label="Main">` is a `<nav>` around a `<ul>` of `<NavItem>` and `<NavGroup>` (D88). `aria-label` is **required**: an app has more than one navigation landmark. It is native and router-agnostic — **Tab** moves through group buttons and the links of open groups; there is no roving tabindex, no arrow keys and no `role="menu"` or `role="tree"`.
85
- - `<NavItem>` is an `<li>` that styles the one anchor inside it: pass your router's link, with `aria-current="page"` on the current one. The current page shows by weight, a raised surface and an inline-start bar, never by colour alone. `<NavItem href="#x" current>Text</NavItem>` renders the anchor itself (`aria-current="page"` when `current`): for plain links and presets, where a compose tree can hold only manifest components; router apps keep passing their own link as the child.
86
+ - `<NavItem>` is an `<li>` that styles the one anchor inside it: pass your router's link. `current` clones that single child with `aria-current="page"` (D93); without `current` the child's own `aria-current` is left alone, so a router that marks its active link keeps working. The current page shows by weight, a raised surface and an inline-start bar, never by colour alone. `<NavItem href="#x" current>Text</NavItem>` renders the anchor itself (`aria-current="page"` when `current`): for plain links and presets, where a compose tree can hold only manifest components; router apps keep passing their own link as the child.
86
87
  - `<NavGroup label open onOpenChange>` is **controlled-only**, like Tabs: `open` and `onOpenChange` are required and it never opens itself. It renders a `<button aria-expanded aria-controls>` over a `<ul>` that is `hidden` when closed, so `aria-controls` always resolves. The chevron is `aria-hidden` and turns on a duration token.
87
88
  - Tokens `--psi-nav-tree-*` alias the semantic layer, so under a `data-psi-theme="dark"` sidebar in a light page they resolve dark. The theme paints no background: the sidebar paints `--psi-bg-primary` itself.
88
89
 
@@ -99,6 +100,7 @@
99
100
  - **Values are strings, not indices**, and `Tab`/`TabPanel` pair by value — their source order need not match, though keeping it matching reads better.
100
101
  - **Activation is automatic**: arrow keys move focus and selection together. There is no manual-activation mode. Arrows follow the orientation's axis (Left/Right horizontal, Up/Down vertical) with wrap; Home/End jump to the first/last enabled tab; disabled tabs are skipped and use `aria-disabled`, not the `disabled` attribute.
101
102
  - **Every panel renders; unselected ones get `hidden`.** So `aria-controls` always resolves and panel DOM state survives a switch. If a panel is expensive, render less *inside* it (`{selected ? <Heavy/> : null}`) rather than omitting the `TabPanel` — omitting it leaves the tab's `aria-controls` pointing at nothing.
103
+ - **A `value` that matches no tab** (an address naming a tab the user may not see) still leaves the list one tab stop: the first enabled tab takes it, or the first tab when all are disabled (D93). Nothing is selected, and development builds warn with the value. The selection stays yours; `Tabs` never picks one. A selected tab that is disabled keeps the stop, as before.
102
104
  - `TabList` requires `aria-label`. The active panel is itself a tab stop, so a panel with no focusable content is still reachable.
103
105
 
104
106
  ## Menu family (3)
@@ -110,12 +112,13 @@
110
112
  - 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.
111
113
  - 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).
112
114
  - 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.
113
- - Utility classes: families are `psi-gap-*`, `psi-p-*`/`psi-px-*`/`psi-py-*`, `psi-m-*`/`psi-mx-*`/`psi-my-*` (all on the spacing scale), `psi-text-*` and `psi-display-*` (typography), plus `.psi-tabular`, `.psi-container`, `.psi-media-tint` and `.psi-sr-only` (hides content visually while keeping it in the accessibility tree). Scaled utility families set one property each and create no layout context — pair `psi-gap-*` with `display: flex`; the bare/fixed utilities (`.psi-container`, `.psi-media-tint`, `.psi-sr-only`) set several. Use `psi-m-0` to kill a UA margin rather than an inline style. The exact roster is `@handamade/psi-tokens/guidance.json` → `utilities.classes`.
115
+ - Utility classes: families are `psi-gap-*`, `psi-p-*`/`psi-px-*`/`psi-py-*`, `psi-m-*`/`psi-mx-*`/`psi-my-*` (all on the spacing scale), `psi-text-*` and `psi-display-*` (typography), `psi-fg-*` (foreground colour, one per fg token: `psi-fg-primary`, `psi-fg-secondary`, `psi-fg-on-accent`, … — `color: var(--psi-fg-<role>)`) (D94), plus `.psi-tabular`, `.psi-container`, `.psi-media-tint` and `.psi-sr-only` (hides content visually while keeping it in the accessibility tree). Scaled utility families set one property each and create no layout context — pair `psi-gap-*` with `display: flex`; the bare/fixed utilities (`.psi-container`, `.psi-media-tint`, `.psi-sr-only`) set several. Use `psi-m-0` to kill a UA margin rather than an inline style. A utility on a component root beats the component's own value (D94): `<Tabs className="psi-gap-24">` gets a 24px gap. The exact roster is `@handamade/psi-tokens/guidance.json` → `utilities.classes`.
114
116
  - Native pass-through: components spread unlisted props onto their underlying element (Input → `<input>`, Select → `<select>`, Button → `<button>`/`<a>`), so `value`, `onChange`, `type`, `id`, `autoComplete`, `checked` etc. all work even though manifest.json lists only the component-specific props. Exception: Tooltip takes no host props — put handlers/attributes on its trigger child.
115
117
  - Theming, programmatic: the standard setup is `data-psi-theme` on `<html>`. From JS, set it once at app init: `document.documentElement.dataset.psiTheme = "dark"` (React: in a root-level effect). Theme CSS does NOT paint the page background — paint `background: var(--psi-bg-primary)` at your app root yourself. For a themed subtree, set `data-psi-theme` on a wrapper and paint its background the same way; brand font combos do not reach nested subtrees (see guidance.fonts.scope).
116
118
  - Size selection: 32 is the default (compact UI); 40 for comfortable forms and marketing pages; 24 for inline/table-row actions; 48 for hero CTAs.
117
119
  - Tags & the accent rule (D40): Tags are passive labels — they do NOT count against "one accent per visual group" (that rule governs buttons/CTAs). Badge mapping: highlight (e.g. a plan badge) → `accent-subtle`; meta → `neutral-subtle`; status → `success`/`warning`/`danger`. On Tag, `subtle` is a boolean prop — `accent-subtle` is spelled `variant="accent" subtle` (Tag's variant union has no `-subtle` entries).
118
- - CSS import order: all DS stylesheets are order-independent — base.css opens with an explicit `@layer psi.base, psi.theme, psi.components, psi.utilities;` statement. Recommended listing for readability: base → theme → components → utilities → @handamade/psi-react/styles.
120
+ - CSS import order: all five DS stylesheets are order-independent — base.css and @handamade/psi-react/styles both open with an explicit `@layer psi.base, psi.theme, psi.components, psi.utilities;` statement. Recommended listing for readability: base → theme → components → utilities → @handamade/psi-react/styles.
121
+ - Cascade (D94): component rules live in `@layer psi.components`, so `psi.utilities` beats them, and so does any of your CSS in a later layer. **Unlayered CSS beats every Psi component rule whatever its specificity**, so an unlayered reset (`button { padding: 0 }`, `* { margin: 0 }`, `a { color: inherit }`) strips Psi components. Put resets and element rules in `@layer psi.base` (or in your own layer declared before Psi's: `@layer reset, psi.base, psi.theme, psi.components, psi.utilities;`); keep deliberate component overrides unlayered or in a layer after Psi's.
119
122
  - Widths: content widths are app-level; the system tokenizes the page container only (`--psi-container-max`). Dialog is the first component with its own width union — `width` is `400 | 560 | 720` (default 560).
120
123
  - Full semantic token roster: `@handamade/psi-tokens/resolved/<theme>.json` has every token with resolved values — this file lists only the patterns.
121
124
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handamade/psi-react",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,13 +40,14 @@
40
40
  "@types/react-dom": "^19.2.3",
41
41
  "@vitejs/plugin-react": "^4.3.0",
42
42
  "jsdom": "^29.1.1",
43
+ "postcss": "^8.5.15",
43
44
  "react": "^19.2.0",
44
45
  "react-docgen-typescript": "^2.4.0",
45
46
  "react-dom": "^19.2.0",
46
47
  "tsx": "^4.22.4",
47
48
  "vite": "^6.0.0",
48
49
  "vitest": "^3.2.6",
49
- "@handamade/psi-tokens": "0.21.0"
50
+ "@handamade/psi-tokens": "0.23.0"
50
51
  },
51
52
  "scripts": {
52
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",