@handamade/psi-mcp 0.20.0 → 0.22.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/README.md +1 -1
- package/dist/psi-index.json +1795 -291
- package/llms.txt +1 -1
- package/package.json +3 -3
package/dist/psi-index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.22.0",
|
|
3
3
|
"themes": [
|
|
4
4
|
"light",
|
|
5
5
|
"dark",
|
|
@@ -32,6 +32,13 @@
|
|
|
32
32
|
"default": 32,
|
|
33
33
|
"description": "Height in px (24 | 32 | 40 | 48)."
|
|
34
34
|
},
|
|
35
|
+
{
|
|
36
|
+
"name": "type",
|
|
37
|
+
"type": "\"button\" | \"submit\" | \"reset\"",
|
|
38
|
+
"required": false,
|
|
39
|
+
"default": null,
|
|
40
|
+
"description": "Button type. Declared so the manifest lists it and a pattern can say\n`submit` (D87). No default is set: the platform's applies — `submit`\ninside a form, a plain button elsewhere."
|
|
41
|
+
},
|
|
35
42
|
{
|
|
36
43
|
"name": "href",
|
|
37
44
|
"type": "string",
|
|
@@ -69,7 +76,7 @@
|
|
|
69
76
|
}
|
|
70
77
|
],
|
|
71
78
|
"slots": [],
|
|
72
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Button\n\nAction trigger. With `href` renders an anchor with identical styling (D33); `disabled` then maps to `aria-disabled`.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label content. |\n| `variant` | `\"accent\" \\| \"accent-subtle\" \\| \"neutral\" \\| \"neutral-subtle\" \\| \"ghost\" \\| \"danger\" \\| \"danger-subtle\" \\| \"outline\"` | neutral | no | Visual variant. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height in px (24 \\| 32 \\| 40 \\| 48). |\n| `href` | `string` | — | no | Render as an anchor with this href (D33). disabled → aria-disabled, no href attribute, pointer-events: none. |\n| `target` | `string` | — | no | Anchor target; only used with href. |\n| `rel` | `string` | — | no | Anchor rel; only used with href. |\n| `ref` | `Ref<HTMLButtonElement \\| HTMLAnchorElement>` | — | no | Forwarded ref to the underlying element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Activates the button. |\n| Tab | Focusable; visible focus ring via :focus-visible. |\n\nWith href it renders an <a>; disabled anchors get aria-disabled, lose the href attribute, and suppress activation (D33).\n\n## Theming\n\nOverride `--psi-button-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
79
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Button\n\nAction trigger. With `href` renders an anchor with identical styling (D33); `disabled` then maps to `aria-disabled`.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label content. |\n| `variant` | `\"accent\" \\| \"accent-subtle\" \\| \"neutral\" \\| \"neutral-subtle\" \\| \"ghost\" \\| \"danger\" \\| \"danger-subtle\" \\| \"outline\"` | neutral | no | Visual variant. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height in px (24 \\| 32 \\| 40 \\| 48). |\n| `type` | `\"button\" \\| \"submit\" \\| \"reset\"` | — | no | Button type. Declared so the manifest lists it and a pattern can say `submit` (D87). No default is set: the platform's applies — `submit` inside a form, a plain button elsewhere. |\n| `href` | `string` | — | no | Render as an anchor with this href (D33). disabled → aria-disabled, no href attribute, pointer-events: none. |\n| `target` | `string` | — | no | Anchor target; only used with href. |\n| `rel` | `string` | — | no | Anchor rel; only used with href. |\n| `ref` | `Ref<HTMLButtonElement \\| HTMLAnchorElement>` | — | no | Forwarded ref to the underlying element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Activates the button. |\n| Tab | Focusable; visible focus ring via :focus-visible. |\n\nWith href it renders an <a>; disabled anchors get aria-disabled, lose the href attribute, and suppress activation (D33).\n\n## Theming\n\nOverride `--psi-button-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
73
80
|
},
|
|
74
81
|
{
|
|
75
82
|
"name": "IconButton",
|
|
@@ -140,7 +147,7 @@
|
|
|
140
147
|
}
|
|
141
148
|
],
|
|
142
149
|
"slots": [],
|
|
143
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# IconButton\n\nSquare icon-only button; same variant/size/href contract as Button. Give it an accessible name (`aria-label`).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The icon element — an Icon* component. |\n| `aria-label` | `string` | — | yes | Accessible name for the icon-only control. Required: an IconButton has no text to name it, so omitting this ships an unnameable button (D60). If you also pass `aria-labelledby`, that wins per the accname algorithm. |\n| `variant` | `\"accent\" \\| \"neutral\" \\| \"ghost\" \\| \"danger\"` | neutral | no | Visual variant. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Square size in px (24 \\| 32 \\| 40 \\| 48). |\n| `href` | `string` | — | no | Render as an anchor with this href (D33). disabled → aria-disabled, no href attribute, pointer-events: none. |\n| `target` | `string` | — | no | Anchor target; only used with href. |\n| `rel` | `string` | — | no | Anchor rel; only used with href. |\n| `ref` | `Ref<HTMLButtonElement \\| HTMLAnchorElement>` | — | no | Forwarded ref to the underlying element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Activates. |\n\nRequires an accessible name — pass aria-label. IconButton does not hide its icon for you; mark the icon aria-hidden yourself.\n\n## Theming\n\nOverride `--psi-button-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
150
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# IconButton\n\nSquare icon-only button; same variant/size/href contract as Button. Give it an accessible name (`aria-label`).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The icon element — an Icon* component. |\n| `aria-label` | `string` | — | yes | Accessible name for the icon-only control. Required: an IconButton has no text to name it, so omitting this ships an unnameable button (D60). If you also pass `aria-labelledby`, that wins per the accname algorithm. |\n| `variant` | `\"accent\" \\| \"neutral\" \\| \"ghost\" \\| \"danger\"` | neutral | no | Visual variant. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Square size in px (24 \\| 32 \\| 40 \\| 48). |\n| `href` | `string` | — | no | Render as an anchor with this href (D33). disabled → aria-disabled, no href attribute, pointer-events: none. |\n| `target` | `string` | — | no | Anchor target; only used with href. |\n| `rel` | `string` | — | no | Anchor rel; only used with href. |\n| `ref` | `Ref<HTMLButtonElement \\| HTMLAnchorElement>` | — | no | Forwarded ref to the underlying element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Activates. |\n\nRequires an accessible name — pass aria-label. IconButton does not hide its icon for you; mark the icon aria-hidden yourself.\n\n## Theming\n\nOverride `--psi-button-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
144
151
|
},
|
|
145
152
|
{
|
|
146
153
|
"name": "Card",
|
|
@@ -190,7 +197,7 @@
|
|
|
190
197
|
}
|
|
191
198
|
],
|
|
192
199
|
"slots": [],
|
|
193
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Card\n\nContent container with an optional edge-to-edge media slot; `stacked` (media above body) or `featured` (media beside body) layout.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Body content, beside or below the media slot. |\n| `variant` | `\"stacked\" \\| \"featured\"` | stacked | no | stacked = media above body; featured = media beside body (~1.6fr/1fr), stacks under md. |\n| `media` | `ReactNode` | — | no | Media slot rendered edge-to-edge (img, AspectRatio, …). |\n| `hoverLift` | `boolean` | false | no | Lift on hover: translateY(-6px) over --psi-duration-350. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nOverride `--psi-card-*` custom properties at any scope.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
200
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Card\n\nContent container with an optional edge-to-edge media slot; `stacked` (media above body) or `featured` (media beside body) layout.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Body content, beside or below the media slot. |\n| `variant` | `\"stacked\" \\| \"featured\"` | stacked | no | stacked = media above body; featured = media beside body (~1.6fr/1fr), stacks under md. |\n| `media` | `ReactNode` | — | no | Media slot rendered edge-to-edge (img, AspectRatio, …). |\n| `hoverLift` | `boolean` | false | no | Lift on hover: translateY(-6px) over --psi-duration-350. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nOverride `--psi-card-*` custom properties at any scope.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
194
201
|
},
|
|
195
202
|
{
|
|
196
203
|
"name": "Panel",
|
|
@@ -233,7 +240,7 @@
|
|
|
233
240
|
"order": 1
|
|
234
241
|
}
|
|
235
242
|
],
|
|
236
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Panel\n\nElevated surface panel (D51): secondary background, faint hairline, radius-12 — the shared --psi-surface-* recipe Dialog's panel also binds. Not a Card: no media slot, no hover lift, opaque by design.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Panel contents. |\n| `padding` | `16 \\| 24` | 24 | no | Inner padding in px. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Skipped — Panel itself is not focusable; focus moves through its children. |\n\nPlain <div> container with no implicit role. Pass aria-* host props if the panel should announce as a region.\n\n## Theming\n\nOverride `--psi-panel-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
243
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Panel\n\nElevated surface panel (D51): secondary background, faint hairline, radius-12 — the shared --psi-surface-* recipe Dialog's panel also binds. Not a Card: no media slot, no hover lift, opaque by design.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Panel contents. |\n| `padding` | `16 \\| 24` | 24 | no | Inner padding in px. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Skipped — Panel itself is not focusable; focus moves through its children. |\n\nPlain <div> container with no implicit role. Pass aria-* host props if the panel should announce as a region.\n\n## Theming\n\nOverride `--psi-panel-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
237
244
|
},
|
|
238
245
|
{
|
|
239
246
|
"name": "DescriptionList",
|
|
@@ -287,7 +294,7 @@
|
|
|
287
294
|
"order": 1
|
|
288
295
|
}
|
|
289
296
|
],
|
|
290
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# DescriptionList\n\nKey/value display (D70): a `<dl>` of term/value pairs, the body `detail-drawer` has always described and never had a component for. No `size` prop — this is type, and the type scale is already expressed by --psi-text-*. Consumers restyle via the token family, not a prop.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | One DescriptionItem per field. |\n| `layout` | `\"stacked\" \\| \"inline\"` | stacked | no | `stacked` puts the term above its value; `inline` puts them in a two-column grid, which is what a detail drawer wants. |\n| `gap` | `8 \\| 12 \\| 16` | 12 | no | Gap between pairs, in px. |\n| `ref` | `Ref<HTMLDListElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nOverride `--psi-description-list-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
297
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# DescriptionList\n\nKey/value display (D70): a `<dl>` of term/value pairs, the body `detail-drawer` has always described and never had a component for. No `size` prop — this is type, and the type scale is already expressed by --psi-text-*. Consumers restyle via the token family, not a prop.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | One DescriptionItem per field. |\n| `layout` | `\"stacked\" \\| \"inline\"` | stacked | no | `stacked` puts the term above its value; `inline` puts them in a two-column grid, which is what a detail drawer wants. |\n| `gap` | `8 \\| 12 \\| 16` | 12 | no | Gap between pairs, in px. |\n| `ref` | `Ref<HTMLDListElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nOverride `--psi-description-list-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
291
298
|
},
|
|
292
299
|
{
|
|
293
300
|
"name": "DescriptionItem",
|
|
@@ -323,7 +330,7 @@
|
|
|
323
330
|
}
|
|
324
331
|
],
|
|
325
332
|
"slots": [],
|
|
326
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# DescriptionItem\n\nOne term/value pair. The term is a prop and the value is children, matching Field's `label` idiom rather than inventing a two-slot shape. Renders `<div><dt/><dd/></div>`. The wrapper is the HTML5-sanctioned grouping element for a `<dl>`, so the `<dt>`/`<dd>` association assistive tech relies on is intact — and it keeps both layouts to one CSS rule each instead of fighting a single grid to space pairs differently from the term/value inside a pair.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The value — renders the <dd>. |\n| `term` | `ReactNode` | — | yes | The term — renders the `<dt>`. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the group wrapper. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-description-item-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
333
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# DescriptionItem\n\nOne term/value pair. The term is a prop and the value is children, matching Field's `label` idiom rather than inventing a two-slot shape. Renders `<div><dt/><dd/></div>`. The wrapper is the HTML5-sanctioned grouping element for a `<dl>`, so the `<dt>`/`<dd>` association assistive tech relies on is intact — and it keeps both layouts to one CSS rule each instead of fighting a single grid to space pairs differently from the term/value inside a pair.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The value — renders the <dd>. |\n| `term` | `ReactNode` | — | yes | The term — renders the `<dt>`. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the group wrapper. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-description-item-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
327
334
|
},
|
|
328
335
|
{
|
|
329
336
|
"name": "Input",
|
|
@@ -373,7 +380,7 @@
|
|
|
373
380
|
}
|
|
374
381
|
],
|
|
375
382
|
"slots": [],
|
|
376
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Input\n\nSingle-line text input with pixel-true heights (24–48) and an error state. Inside a Field, id/aria-describedby/aria-invalid/required are wired automatically (D49).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `type` | `\"number\" \\| \"text\" \\| \"search\" \\| \"email\" \\| \"tel\" \\| \"url\" \\| \"password\" \\| \"date\"` | text | no | Input type. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height in px (24 \\| 32 \\| 40 \\| 48). |\n| `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input>` element. |\n| `placeholder` | `string` | — | no | Native placeholder text shown while the field is empty (only on components whose host element supports it). |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focuses the native <input>; focus ring on the field. |\n\nerror sets a red border only. Inside a Field, aria-invalid and aria-describedby are wired automatically (D49); standalone, pair them yourself.\n\n## Theming\n\nOverride `--psi-input-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
383
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Input\n\nSingle-line text input with pixel-true heights (24–48) and an error state. Inside a Field, id/aria-describedby/aria-invalid/required are wired automatically (D49).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `type` | `\"number\" \\| \"text\" \\| \"search\" \\| \"email\" \\| \"tel\" \\| \"url\" \\| \"password\" \\| \"date\"` | text | no | Input type. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height in px (24 \\| 32 \\| 40 \\| 48). |\n| `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input>` element. |\n| `placeholder` | `string` | — | no | Native placeholder text shown while the field is empty (only on components whose host element supports it). |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focuses the native <input>; focus ring on the field. |\n\nerror sets a red border only. Inside a Field, aria-invalid and aria-describedby are wired automatically (D49); standalone, pair them yourself.\n\n## Theming\n\nOverride `--psi-input-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
377
384
|
},
|
|
378
385
|
{
|
|
379
386
|
"name": "Select",
|
|
@@ -416,11 +423,11 @@
|
|
|
416
423
|
}
|
|
417
424
|
],
|
|
418
425
|
"slots": [],
|
|
419
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Select\n\nStyled native `<select>` with pixel-true heights (24–48) and an error state. Inside a Field, id/aria-describedby/aria-invalid/required are wired automatically (D49).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The <option> elements. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height in px (24 \\| 32 \\| 40 \\| 48). Replaces the native `size` attribute (which sets the visible option-row count on a listbox-style `<select>`) with the design system's control height in pixels. |\n| `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |\n| `ref` | `Ref<HTMLSelectElement>` | — | no | Forwarded ref to the underlying `<select>` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focuses the native <select>. |\n| Arrow keys / typeahead | Native option navigation. |\n\nerror: same contract as Input — sets a red border only. Inside a Field, aria-invalid and aria-describedby are wired automatically (D49); standalone, pair them yourself.\n\n## Theming\n\nOverride `--psi-select-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
426
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Select\n\nStyled native `<select>` with pixel-true heights (24–48) and an error state. Inside a Field, id/aria-describedby/aria-invalid/required are wired automatically (D49).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The <option> elements. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height in px (24 \\| 32 \\| 40 \\| 48). Replaces the native `size` attribute (which sets the visible option-row count on a listbox-style `<select>`) with the design system's control height in pixels. |\n| `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |\n| `ref` | `Ref<HTMLSelectElement>` | — | no | Forwarded ref to the underlying `<select>` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focuses the native <select>. |\n| Arrow keys / typeahead | Native option navigation. |\n\nerror: same contract as Input — sets a red border only. Inside a Field, aria-invalid and aria-describedby are wired automatically (D49); standalone, pair them yourself.\n\n## Theming\n\nOverride `--psi-select-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
420
427
|
},
|
|
421
428
|
{
|
|
422
429
|
"name": "Field",
|
|
423
|
-
"description": "Labeled form-row wrapper: label above, control, one message line below — description normally, error when set (aria-live unless `announce={false}`). Auto-wires id/aria-describedby/aria-invalid into Input and Select (D49).",
|
|
430
|
+
"description": "Labeled form-row wrapper: label above, control, one message line below — description normally, error when set (aria-live unless `announce={false}`). Auto-wires id/aria-describedby/aria-invalid into Input and Select (D49). Overriding `--psi-field-gap` or `--psi-field-label-height` on a narrower scope than the theme root, inside a `Toolbar align=\"start\"` row, also needs `--psi-toolbar-action-offset` set on that same scope, or the row's button keeps the theme-root offset (D92).",
|
|
424
431
|
"props": [
|
|
425
432
|
{
|
|
426
433
|
"name": "children",
|
|
@@ -457,6 +464,13 @@
|
|
|
457
464
|
"default": false,
|
|
458
465
|
"description": "Renders the required marker and flows `required` to the control."
|
|
459
466
|
},
|
|
467
|
+
{
|
|
468
|
+
"name": "requiredText",
|
|
469
|
+
"type": "string",
|
|
470
|
+
"required": false,
|
|
471
|
+
"default": null,
|
|
472
|
+
"description": "With `required`, the label shows this text — \"(Required)\", say — instead\nof the asterisk, so the requirement is stated in words and not by a glyph\nalone (D84). Visible and aria-hidden: the control carries the programmatic\nsignal (`required` / `aria-required`), and a screen reader that heard both\nwould say it twice. Without `required` it renders nothing."
|
|
473
|
+
},
|
|
460
474
|
{
|
|
461
475
|
"name": "group",
|
|
462
476
|
"type": "boolean",
|
|
@@ -513,7 +527,7 @@
|
|
|
513
527
|
"order": 3
|
|
514
528
|
}
|
|
515
529
|
],
|
|
516
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Field\n\nLabeled form-row wrapper: label above, control, one message line below — description normally, error when set (aria-live unless `announce={false}`). Auto-wires id/aria-describedby/aria-invalid into Input and Select (D49).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The control this field labels. |\n| `label` | `ReactNode` | — | no | Label content; renders a <label> (or <legend> in group mode). |\n| `description` | `ReactNode` | — | no | Helper line under the control; replaced by error when error is set. |\n| `error` | `ReactNode` | — | no | Error content; when truthy it replaces the description and switches the field (and a wrapped Input/Select) into error state. |\n| `required` | `boolean` | false | no | Renders the required marker and flows `required` to the control. |\n| `group` | `boolean` | false | no | Group mode: fieldset/legend wrapping several self-labeled controls (Checkbox/Switch); the message describes the whole group. |\n| `htmlFor` | `string` | — | no | Override the generated control id (pair it with the same id on the control). |\n| `announce` | `boolean` | true | no | When false, the message line is not a live region: an application that routes every announcement through one announcer of its own sets this and announces field errors itself. The message still describes the control (aria-describedby). |\n| `ref` | `Ref<HTMLDivElement \\| HTMLFieldSetElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focus moves to the wrapped control; the label is announced with it. |\n\nWires label association, aria-describedby and aria-invalid into a wrapped Input/Select automatically; the message line is aria-live=polite. Group mode renders fieldset/legend.\n\n## Theming\n\nOverride `--psi-field-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
530
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Field\n\nLabeled form-row wrapper: label above, control, one message line below — description normally, error when set (aria-live unless `announce={false}`). Auto-wires id/aria-describedby/aria-invalid into Input and Select (D49). Overriding `--psi-field-gap` or `--psi-field-label-height` on a narrower scope than the theme root, inside a `Toolbar align=\"start\"` row, also needs `--psi-toolbar-action-offset` set on that same scope, or the row's button keeps the theme-root offset (D92).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The control this field labels. |\n| `label` | `ReactNode` | — | no | Label content; renders a <label> (or <legend> in group mode). |\n| `description` | `ReactNode` | — | no | Helper line under the control; replaced by error when error is set. |\n| `error` | `ReactNode` | — | no | Error content; when truthy it replaces the description and switches the field (and a wrapped Input/Select) into error state. |\n| `required` | `boolean` | false | no | Renders the required marker and flows `required` to the control. |\n| `requiredText` | `string` | — | no | With `required`, the label shows this text — \"(Required)\", say — instead of the asterisk, so the requirement is stated in words and not by a glyph alone (D84). Visible and aria-hidden: the control carries the programmatic signal (`required` / `aria-required`), and a screen reader that heard both would say it twice. Without `required` it renders nothing. |\n| `group` | `boolean` | false | no | Group mode: fieldset/legend wrapping several self-labeled controls (Checkbox/Switch); the message describes the whole group. |\n| `htmlFor` | `string` | — | no | Override the generated control id (pair it with the same id on the control). |\n| `announce` | `boolean` | true | no | When false, the message line is not a live region: an application that routes every announcement through one announcer of its own sets this and announces field errors itself. The message still describes the control (aria-describedby). |\n| `ref` | `Ref<HTMLDivElement \\| HTMLFieldSetElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focus moves to the wrapped control; the label is announced with it. |\n\nWires label association, aria-describedby and aria-invalid into a wrapped Input/Select automatically; the message line is aria-live=polite. Group mode renders fieldset/legend. required marks the label with an aria-hidden asterisk and flows required to the control; requiredText replaces the asterisk with a visible word such as \"(Required)\", also aria-hidden, because the control's required/aria-required is what assistive tech reads (D84). A control Psi did not write joins the same wiring through useFieldControl(): spread its result — id, aria-labelledby (a label-for names only a labelable element, and a custom control is usually not one), aria-describedby, aria-invalid, aria-required, required, each present only when the Field sets it — onto the element that takes focus (D84).\n\n## Theming\n\nOverride `--psi-field-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
517
531
|
},
|
|
518
532
|
{
|
|
519
533
|
"name": "Dialog",
|
|
@@ -621,7 +635,7 @@
|
|
|
621
635
|
"order": 3
|
|
622
636
|
}
|
|
623
637
|
],
|
|
624
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Dialog\n\nModal dialog on the native <dialog> top layer: focus trap, aria-modal and focus restore come from the platform; title/footer slots, Esc/backdrop/ close-button dismissal via onClose(reason) (D50). `placement` moves the panel and nothing else (D66): an `inline-start` / `inline-end` Dialog is a drawer, pinned full-height to that edge, with the same modality, focus trap, focus restore and dismissal reasons as a centered one. There is deliberately no separate Drawer component — a sibling would have to duplicate or wrap all of that.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Body content, between the title and footer slots. |\n| `open` | `boolean` | — | yes | Controlled open state; syncs to showModal()/close(). |\n| `onClose` | `(reason: \"esc\" \\| \"backdrop\" \\| \"close-button\") => void` | — | yes | Called on every dismissal attempt with its source; the consumer flips `open`. |\n| `title` | `ReactNode` | — | no | Heading; renders an <h2> wired to aria-labelledby. Without it, pass aria-label. |\n| `footer` | `ReactNode` | — | no | Action row (Buttons — one accent per group, danger for destructive). |\n| `width` | `400 \\| 560 \\| 720` | 560 | no | Panel width in px (400 \\| 560 \\| 720). For an `inline-*` placement this is the drawer's width; the height is always the full viewport. |\n| `placement` | `\"center\" \\| \"inline-start\" \\| \"inline-end\"` | center | no | Where the panel sits. `inline-start` / `inline-end` pin it full-height to that edge, making it a drawer (D66). |\n| `dismissible` | `boolean` | true | no | false = no close button, Esc and backdrop swallowed — footer is the only exit. |\n| `ref` | `Ref<HTMLDialogElement>` | — | no | Forwarded ref to the underlying <dialog> element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Esc | Dismisses via onClose('esc') when dismissible; swallowed otherwise. |\n| Tab | Focus is trapped inside by the native <dialog> top layer; restored on close. |\n\nRendered with showModal(): aria-modal, inert background and focus restore come from the platform. title wires aria-labelledby; without title, pass aria-label. Backdrop click dismisses only when dismissible. placement=\"inline-start\"/\"inline-end\" pins the panel full-height to that edge — that is Psi's drawer, and it changes nothing about modality, the focus trap, focus restore or the dismissal reasons (D66). A drawer's panel scrolls internally so a dismissible={false} footer stays reachable.\n\n## Theming\n\nOverride `--psi-dialog-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
638
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Dialog\n\nModal dialog on the native <dialog> top layer: focus trap, aria-modal and focus restore come from the platform; title/footer slots, Esc/backdrop/ close-button dismissal via onClose(reason) (D50). `placement` moves the panel and nothing else (D66): an `inline-start` / `inline-end` Dialog is a drawer, pinned full-height to that edge, with the same modality, focus trap, focus restore and dismissal reasons as a centered one. There is deliberately no separate Drawer component — a sibling would have to duplicate or wrap all of that.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Body content, between the title and footer slots. |\n| `open` | `boolean` | — | yes | Controlled open state; syncs to showModal()/close(). |\n| `onClose` | `(reason: \"esc\" \\| \"backdrop\" \\| \"close-button\") => void` | — | yes | Called on every dismissal attempt with its source; the consumer flips `open`. |\n| `title` | `ReactNode` | — | no | Heading; renders an <h2> wired to aria-labelledby. Without it, pass aria-label. |\n| `footer` | `ReactNode` | — | no | Action row (Buttons — one accent per group, danger for destructive). |\n| `width` | `400 \\| 560 \\| 720` | 560 | no | Panel width in px (400 \\| 560 \\| 720). For an `inline-*` placement this is the drawer's width; the height is always the full viewport. |\n| `placement` | `\"center\" \\| \"inline-start\" \\| \"inline-end\"` | center | no | Where the panel sits. `inline-start` / `inline-end` pin it full-height to that edge, making it a drawer (D66). |\n| `dismissible` | `boolean` | true | no | false = no close button, Esc and backdrop swallowed — footer is the only exit. |\n| `ref` | `Ref<HTMLDialogElement>` | — | no | Forwarded ref to the underlying <dialog> element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Esc | Dismisses via onClose('esc') when dismissible; swallowed otherwise. |\n| Tab | Focus is trapped inside by the native <dialog> top layer; restored on close. |\n\nRendered with showModal(): aria-modal, inert background and focus restore come from the platform. title wires aria-labelledby; without title, pass aria-label. Backdrop click dismisses only when dismissible. placement=\"inline-start\"/\"inline-end\" pins the panel full-height to that edge — that is Psi's drawer, and it changes nothing about modality, the focus trap, focus restore or the dismissal reasons (D66). A drawer's panel scrolls internally so a dismissible={false} footer stays reachable.\n\n## Theming\n\nOverride `--psi-dialog-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
625
639
|
},
|
|
626
640
|
{
|
|
627
641
|
"name": "Checkbox",
|
|
@@ -657,7 +671,7 @@
|
|
|
657
671
|
}
|
|
658
672
|
],
|
|
659
673
|
"slots": [],
|
|
660
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Checkbox\n\nNative checkbox with a built-in label; for independent binary choices.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label text. |\n| `aria-label` | `string` | — | no | Accessible name when there is no visible label — a table's row-selection checkbox, say. Optional, unlike IconButton's required form (D60): a labelled checkbox is still the common case. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type=\"checkbox\">` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Space | Toggles. Native <input type=checkbox> underneath (visually hidden). |\n\n\n## Theming\n\nOverride `--psi-checkbox-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
674
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Checkbox\n\nNative checkbox with a built-in label; for independent binary choices.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label text. |\n| `aria-label` | `string` | — | no | Accessible name when there is no visible label — a table's row-selection checkbox, say. Optional, unlike IconButton's required form (D60): a labelled checkbox is still the common case. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type=\"checkbox\">` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Space | Toggles. Native <input type=checkbox> underneath (visually hidden). |\n\n\n## Theming\n\nOverride `--psi-checkbox-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
661
675
|
},
|
|
662
676
|
{
|
|
663
677
|
"name": "Switch",
|
|
@@ -686,7 +700,7 @@
|
|
|
686
700
|
}
|
|
687
701
|
],
|
|
688
702
|
"slots": [],
|
|
689
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Switch\n\nOn/off toggle (`role=\"switch\"`) with a built-in label; for settings that take effect immediately.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label text. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type=\"checkbox\" role=\"switch\">` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Space | Toggles. Native checkbox input with role=\"switch\"; announced as a switch, not a checkbox. |\n\n\n## Theming\n\nOverride `--psi-switch-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
703
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Switch\n\nOn/off toggle (`role=\"switch\"`) with a built-in label; for settings that take effect immediately.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label text. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type=\"checkbox\" role=\"switch\">` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Space | Toggles. Native checkbox input with role=\"switch\"; announced as a switch, not a checkbox. |\n\n\n## Theming\n\nOverride `--psi-switch-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
690
704
|
},
|
|
691
705
|
{
|
|
692
706
|
"name": "Tag",
|
|
@@ -736,11 +750,11 @@
|
|
|
736
750
|
}
|
|
737
751
|
],
|
|
738
752
|
"slots": [],
|
|
739
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tag\n\nCompact label for status or category; semantic variants, optional subtle tint and dismiss button.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label content. |\n| `variant` | `\"neutral\" \\| \"accent\" \\| \"success\" \\| \"warning\" \\| \"danger\"` | neutral | no | Semantic color variant. |\n| `subtle` | `boolean` | false | no | Use tinted background with semantic foreground. |\n| `onDismiss` | `(() => void)` | — | no | When provided, renders a dismiss button that calls this handler. |\n| `ref` | `Ref<HTMLSpanElement>` | — | no | Forwarded ref to the underlying `<span>` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space (dismiss button) | onDismiss renders a real <button>; keyboard-dismissible. |\n\nPassive label otherwise; not in the tab order without onDismiss.\n\n## Theming\n\nOverride `--psi-tag-*` custom properties at any scope.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
753
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tag\n\nCompact label for status or category; semantic variants, optional subtle tint and dismiss button.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label content. |\n| `variant` | `\"neutral\" \\| \"accent\" \\| \"success\" \\| \"warning\" \\| \"danger\"` | neutral | no | Semantic color variant. |\n| `subtle` | `boolean` | false | no | Use tinted background with semantic foreground. |\n| `onDismiss` | `(() => void)` | — | no | When provided, renders a dismiss button that calls this handler. |\n| `ref` | `Ref<HTMLSpanElement>` | — | no | Forwarded ref to the underlying `<span>` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space (dismiss button) | onDismiss renders a real <button>; keyboard-dismissible. |\n\nPassive label otherwise; not in the tab order without onDismiss.\n\n## Theming\n\nOverride `--psi-tag-*` custom properties at any scope.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
740
754
|
},
|
|
741
755
|
{
|
|
742
756
|
"name": "Toolbar",
|
|
743
|
-
"description": "Horizontal grouping row for filter/search controls (D52). Wraps on overflow; zero JS. Deliberately NOT ARIA role=\"toolbar\" — that role contracts roving-tabindex arrow-key navigation, wrong for form controls. With aria-label it announces as role=\"group\".",
|
|
757
|
+
"description": "Horizontal grouping row for filter/search controls (D52). Wraps on overflow; zero JS. Deliberately NOT ARIA role=\"toolbar\" — that role contracts roving-tabindex arrow-key navigation, wrong for form controls. With aria-label it announces as role=\"group\"; as a form, as a named form landmark instead (D87).",
|
|
744
758
|
"props": [
|
|
745
759
|
{
|
|
746
760
|
"name": "children",
|
|
@@ -756,9 +770,23 @@
|
|
|
756
770
|
"default": 8,
|
|
757
771
|
"description": "Gap between controls in px."
|
|
758
772
|
},
|
|
773
|
+
{
|
|
774
|
+
"name": "align",
|
|
775
|
+
"type": "\"center\" | \"end\" | \"start\"",
|
|
776
|
+
"required": false,
|
|
777
|
+
"default": "center",
|
|
778
|
+
"description": "Cross-axis alignment of the row's items. `center` (the default) centres\nthem, for a row of controls without visible labels such as\nfilter-toolbar. `end` and `start` are for a row of Fields with visible\nlabels beside an unlabelled button.\n`end` (D87) lines items up on their bottom edge. It lines up each item's\n*last* line, so a Field in an end-aligned row must end on its control —\nno description or error line under it; use `start` for one.\n`start` (D92) lines up the controls: items share their top edge, and a\n*direct* `button` or `a` child drops by `--psi-toolbar-action-offset`\n(one Field label line plus the Field gap) onto the control line. Only a\ndirect child is offset: a wrapped trigger (a Tooltip-wrapped button, a\nMenu trigger) is not. A Field's description or error line hangs below\nits control and moves nothing else. Every label in the row must fit on\none line; a label that wraps pushes its control below the others."
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
"name": "as",
|
|
782
|
+
"type": "\"div\" | \"form\"",
|
|
783
|
+
"required": false,
|
|
784
|
+
"default": "div",
|
|
785
|
+
"description": "Root element. `form` renders a real `<form>`, so a submit Button\nsubmits it and Enter in a field does too; with `aria-label` it is a\nnamed form landmark, so it takes no `role=\"group\"` (D87). Wire\n`onSubmit`."
|
|
786
|
+
},
|
|
759
787
|
{
|
|
760
788
|
"name": "ref",
|
|
761
|
-
"type": "Ref<HTMLDivElement>",
|
|
789
|
+
"type": "Ref<HTMLDivElement | HTMLFormElement>",
|
|
762
790
|
"required": false,
|
|
763
791
|
"default": null,
|
|
764
792
|
"description": "Forwarded ref to the root element."
|
|
@@ -779,7 +807,7 @@
|
|
|
779
807
|
"order": 1
|
|
780
808
|
}
|
|
781
809
|
],
|
|
782
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Toolbar\n\nHorizontal grouping row for filter/search controls (D52). Wraps on overflow; zero JS. Deliberately NOT ARIA role=\"toolbar\" — that role contracts roving-tabindex arrow-key navigation, wrong for form controls. With aria-label it announces as role=\"group\".\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The controls. |\n| `gap` | `8 \\| 12 \\| 16` | 8 | no | Gap between controls in px. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Moves through the controls in DOM order — no roving tabindex (deliberately not role=toolbar, D52). |\n\nWith aria-label it renders role=group so the control cluster announces with a name; unlabeled it is a plain layout div.\n\n## Theming\n\nOverride `--psi-toolbar-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
810
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Toolbar\n\nHorizontal grouping row for filter/search controls (D52). Wraps on overflow; zero JS. Deliberately NOT ARIA role=\"toolbar\" — that role contracts roving-tabindex arrow-key navigation, wrong for form controls. With aria-label it announces as role=\"group\"; as a form, as a named form landmark instead (D87).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The controls. |\n| `gap` | `8 \\| 12 \\| 16` | 8 | no | Gap between controls in px. |\n| `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. |\n| `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`. |\n| `ref` | `Ref<HTMLDivElement \\| HTMLFormElement>` | — | no | Forwarded ref to the root element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Moves through the controls in DOM order — no roving tabindex (deliberately not role=toolbar, D52). |\n\nWith aria-label it renders role=group so the control cluster announces with a name; unlabeled it is a plain layout div.\n\n## Theming\n\nOverride `--psi-toolbar-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
783
811
|
},
|
|
784
812
|
{
|
|
785
813
|
"name": "Tooltip",
|
|
@@ -808,7 +836,7 @@
|
|
|
808
836
|
}
|
|
809
837
|
],
|
|
810
838
|
"slots": [],
|
|
811
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tooltip\n\nShort hover/focus hint attached to a single trigger element.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `content` | `ReactNode` | — | yes | Tooltip content. |\n| `children` | `ReactElement<Record<string, unknown>, string \\| JSXElementConstructor<any>>` | — | yes | The trigger element (must accept ref, onMouseEnter/Leave, onFocus/Blur). |\n| `placement` | `\"top\" \\| \"bottom\" \\| \"left\" \\| \"right\"` | top | no | Preferred placement. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab (focus trigger) | Shows immediately on focus (no delay). |\n| Escape | Dismisses while visible (WCAG 1.4.13). |\n\nHover opens after a short delay; content is linked via aria-describedby while visible. Trigger must accept onMouseEnter/Leave and onFocus/Blur props (cloned in automatically) — no ref forwarding required.\n\n## Theming\n\nOverride `--psi-tooltip-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
839
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tooltip\n\nShort hover/focus hint attached to a single trigger element.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `content` | `ReactNode` | — | yes | Tooltip content. |\n| `children` | `ReactElement<Record<string, unknown>, string \\| JSXElementConstructor<any>>` | — | yes | The trigger element (must accept ref, onMouseEnter/Leave, onFocus/Blur). |\n| `placement` | `\"top\" \\| \"bottom\" \\| \"left\" \\| \"right\"` | top | no | Preferred placement. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab (focus trigger) | Shows immediately on focus (no delay). |\n| Escape | Dismisses while visible (WCAG 1.4.13). |\n\nHover opens after a short delay; content is linked via aria-describedby while visible. Trigger must accept onMouseEnter/Leave and onFocus/Blur props (cloned in automatically) — no ref forwarding required.\n\n## Theming\n\nOverride `--psi-tooltip-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
812
840
|
},
|
|
813
841
|
{
|
|
814
842
|
"name": "NavBar",
|
|
@@ -833,7 +861,21 @@
|
|
|
833
861
|
"type": "ReactNode",
|
|
834
862
|
"required": false,
|
|
835
863
|
"default": null,
|
|
836
|
-
"description": "Nav links."
|
|
864
|
+
"description": "Nav links. The `<nav>` renders only when there are children, so a bar with\na brand and actions and no links has no empty landmark (D88)."
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
"name": "navLabel",
|
|
868
|
+
"type": "string",
|
|
869
|
+
"required": false,
|
|
870
|
+
"default": null,
|
|
871
|
+
"description": "Accessible name of the links' `<nav>` (its `aria-label`). Rest props land\non the `<header>`, so this is the only way to label the `<nav>` (D88)."
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
"name": "fluid",
|
|
875
|
+
"type": "boolean",
|
|
876
|
+
"required": false,
|
|
877
|
+
"default": false,
|
|
878
|
+
"description": "Lay the row out at full width: it drops `psi-container` (the centred\nmax-width column) and keeps the `--psi-gutter` side padding. For an\napplication header over a full-width frame (D88)."
|
|
837
879
|
},
|
|
838
880
|
{
|
|
839
881
|
"name": "ref",
|
|
@@ -850,33 +892,52 @@
|
|
|
850
892
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
851
893
|
}
|
|
852
894
|
],
|
|
853
|
-
"slots": [
|
|
854
|
-
|
|
895
|
+
"slots": [
|
|
896
|
+
{
|
|
897
|
+
"name": "brand",
|
|
898
|
+
"accepts": {},
|
|
899
|
+
"cardinality": "0..*",
|
|
900
|
+
"order": 1
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
"name": "actions",
|
|
904
|
+
"accepts": {},
|
|
905
|
+
"cardinality": "0..*",
|
|
906
|
+
"order": 2
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
"name": "body",
|
|
910
|
+
"accepts": {},
|
|
911
|
+
"cardinality": "0..*",
|
|
912
|
+
"order": 3
|
|
913
|
+
}
|
|
914
|
+
],
|
|
915
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# NavBar\n\nTop navigation bar with brand, nav-link, and trailing-action slots.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `brand` | `ReactNode` | — | no | Brand slot (wordmark / logo link), leading edge. |\n| `actions` | `ReactNode` | — | no | Trailing actions slot (theme switch, CTA). |\n| `children` | `ReactNode` | — | no | Nav links. The `<nav>` renders only when there are children, so a bar with a brand and actions and no links has no empty landmark (D88). |\n| `navLabel` | `string` | — | no | Accessible name of the links' `<nav>` (its `aria-label`). Rest props land on the `<header>`, so this is the only way to label the `<nav>` (D88). |\n| `fluid` | `boolean` | false | no | Lay the row out at full width: it drops `psi-container` (the centred max-width column) and keeps the `--psi-gutter` side padding. For an application header over a full-width frame (D88). |\n| `ref` | `Ref<HTMLElement>` | — | no | Forwarded ref to the header element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nOverride `--psi-navbar-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
855
916
|
},
|
|
856
917
|
{
|
|
857
|
-
"name": "
|
|
858
|
-
"description": "
|
|
918
|
+
"name": "SkipLink",
|
|
919
|
+
"description": "A link to the page's main content, invisible until it takes keyboard focus and then fixed at the inline-start top corner on a surface, drawing the shared focus ring (D88). Render it first in the document. No script: native fragment navigation moves focus to a target that is focusable, so the next *Tab* continues from inside it. `AppShell`'s `main` is one (`tabIndex={-1}`); a bare `<main>` is not, so give it one. A modal `Dialog` makes the rest of the document inert, so the link never needs to beat one. A `ToastRegion` is on the native top layer, which paints above everything whatever its `z-index`: do not place one at `top-start` (its default is `bottom-end`) in an app with a skip link, or it covers the focused link.",
|
|
859
920
|
"props": [
|
|
860
921
|
{
|
|
861
|
-
"name": "
|
|
862
|
-
"type": "
|
|
863
|
-
"required":
|
|
922
|
+
"name": "href",
|
|
923
|
+
"type": "string",
|
|
924
|
+
"required": true,
|
|
864
925
|
"default": null,
|
|
865
|
-
"description": "
|
|
926
|
+
"description": "Fragment of the element to skip to, such as `\"#main\"`. The target must be\nfocusable (`tabIndex={-1}` is enough), or focus does not move."
|
|
866
927
|
},
|
|
867
928
|
{
|
|
868
|
-
"name": "
|
|
869
|
-
"type": "
|
|
929
|
+
"name": "children",
|
|
930
|
+
"type": "ReactNode",
|
|
870
931
|
"required": true,
|
|
871
932
|
"default": null,
|
|
872
|
-
"description": "
|
|
933
|
+
"description": "Visible text, such as \"Skip to content\"."
|
|
873
934
|
},
|
|
874
935
|
{
|
|
875
936
|
"name": "ref",
|
|
876
|
-
"type": "Ref<
|
|
937
|
+
"type": "Ref<HTMLAnchorElement>",
|
|
877
938
|
"required": false,
|
|
878
939
|
"default": null,
|
|
879
|
-
"description": "Forwarded ref to the
|
|
940
|
+
"description": "Forwarded ref to the anchor."
|
|
880
941
|
},
|
|
881
942
|
{
|
|
882
943
|
"name": "className",
|
|
@@ -887,53 +948,25 @@
|
|
|
887
948
|
}
|
|
888
949
|
],
|
|
889
950
|
"slots": [],
|
|
890
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
951
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# SkipLink\n\nA link to the page's main content, invisible until it takes keyboard focus and then fixed at the inline-start top corner on a surface, drawing the shared focus ring (D88). Render it first in the document. No script: native fragment navigation moves focus to a target that is focusable, so the next *Tab* continues from inside it. `AppShell`'s `main` is one (`tabIndex={-1}`); a bare `<main>` is not, so give it one. A modal `Dialog` makes the rest of the document inert, so the link never needs to beat one. A `ToastRegion` is on the native top layer, which paints above everything whatever its `z-index`: do not place one at `top-start` (its default is `bottom-end`) in an app with a skip link, or it covers the focused link.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `href` | `string` | — | yes | Fragment of the element to skip to, such as `\"#main\"`. The target must be focusable (`tabIndex={-1}` is enough), or focus does not move. |\n| `children` | `ReactNode` | — | yes | Visible text, such as \"Skip to content\". |\n| `ref` | `Ref<HTMLAnchorElement>` | — | no | Forwarded ref to the anchor. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Takes focus and becomes visible at the inline-start top corner, drawing the shared focus ring. Put it first in the document so it is the first tab stop. |\n| Enter | Follows the fragment href: focus moves to the target, and the next Tab continues from inside it. |\n\nA native anchor, visually hidden until it is focused (:focus-visible) and with no script (D88). Fragment navigation moves focus only to a focusable target, so the element named by href must be focusable: tabIndex={-1} is enough, and AppShell's main has it. A modal Dialog makes the link inert, so it never needs to beat one. A ToastRegion is on the native top layer and paints over the focused link whatever its z-index: do not place one at top-start in an app with a skip link.\n\n## Theming\n\nOverride `--psi-skip-link-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
891
952
|
},
|
|
892
953
|
{
|
|
893
|
-
"name": "
|
|
894
|
-
"description": "
|
|
954
|
+
"name": "NavTree",
|
|
955
|
+
"description": "A navigation tree (D88): a `<nav aria-label>` around a `<ul>` of `NavItem`s and `NavGroup`s. Native elements only — *Tab* moves through the group buttons and the links of open groups; there is no roving focus and no `role=\"menu\"` or `role=\"tree\"`, because site navigation is a list of links, not a widget. Router-agnostic: pass your router's link inside each `NavItem`, with `aria-current=\"page\"` on the current one.",
|
|
895
956
|
"props": [
|
|
896
|
-
{
|
|
897
|
-
"name": "open",
|
|
898
|
-
"type": "boolean",
|
|
899
|
-
"required": true,
|
|
900
|
-
"default": null,
|
|
901
|
-
"description": "Controlled open state; syncs to showPopover()/hidePopover()."
|
|
902
|
-
},
|
|
903
|
-
{
|
|
904
|
-
"name": "onClose",
|
|
905
|
-
"type": "(reason: \"item-select\" | \"esc\" | \"outside\") => void",
|
|
906
|
-
"required": true,
|
|
907
|
-
"default": null,
|
|
908
|
-
"description": "Called when a dismissal is *requested*; the consumer flips `open`.\n\nMenu never closes itself on `\"esc\"` or `\"item-select\"` — the popover stays\nopen until `open` becomes false (D50). `\"outside\"` is the one exception:\nlight dismiss is performed by the browser before it tells us, so by the\ntime that reason is reported the popover is already closed. Flipping\n`open` to false is still required, to keep React's state in step.\n\nInvariant (D58): a dismissal is only ever reported for a menu that is\ncurrently open according to its own `open` prop. The platform can close\nan auto popover before the consumer's state catches up — clicking another\nmenu's trigger light-dismisses this one before the consumer's click\nhandler runs — and a report for an already-closed menu would clear a\nselection that has since moved on."
|
|
909
|
-
},
|
|
910
|
-
{
|
|
911
|
-
"name": "trigger",
|
|
912
|
-
"type": "ReactElement<Record<string, unknown>, string | JSXElementConstructor<any>>",
|
|
913
|
-
"required": true,
|
|
914
|
-
"default": null,
|
|
915
|
-
"description": "The trigger element. Must be a single React element that spreads unknown\nprops onto a focusable node (a Psi `Button`, say): Menu clones it to add\n`aria-haspopup=\"menu\"` and `aria-expanded`, so assistive tech associates\nthe menu with the control users actually focus."
|
|
916
|
-
},
|
|
917
|
-
{
|
|
918
|
-
"name": "placement",
|
|
919
|
-
"type": "\"bottom-start\" | \"bottom-end\" | \"top-start\" | \"top-end\"",
|
|
920
|
-
"required": false,
|
|
921
|
-
"default": "bottom-start",
|
|
922
|
-
"description": "Placement relative to the trigger."
|
|
923
|
-
},
|
|
924
957
|
{
|
|
925
958
|
"name": "aria-label",
|
|
926
959
|
"type": "string",
|
|
927
|
-
"required":
|
|
960
|
+
"required": true,
|
|
928
961
|
"default": null,
|
|
929
|
-
"description": "Accessible name
|
|
962
|
+
"description": "Accessible name of the navigation landmark, such as \"Main\". Required: an\napp has more than one `<nav>` (a header's, a sidebar's), and an unnamed\none is indistinguishable from the rest. Declared here rather than\ninherited so docgen keeps it in the manifest (D60)."
|
|
930
963
|
},
|
|
931
964
|
{
|
|
932
965
|
"name": "children",
|
|
933
966
|
"type": "ReactNode",
|
|
934
967
|
"required": true,
|
|
935
968
|
"default": null,
|
|
936
|
-
"description": "
|
|
969
|
+
"description": "`NavItem`s and `NavGroup`s — each renders an `<li>`."
|
|
937
970
|
},
|
|
938
971
|
{
|
|
939
972
|
"name": "className",
|
|
@@ -944,146 +977,172 @@
|
|
|
944
977
|
},
|
|
945
978
|
{
|
|
946
979
|
"name": "ref",
|
|
947
|
-
"type": "Ref<
|
|
980
|
+
"type": "Ref<HTMLElement>",
|
|
948
981
|
"required": false,
|
|
949
982
|
"default": null,
|
|
950
|
-
"description": "Forwarded ref to the
|
|
983
|
+
"description": "Forwarded ref to the `<nav>`."
|
|
951
984
|
}
|
|
952
985
|
],
|
|
953
986
|
"slots": [
|
|
954
|
-
{
|
|
955
|
-
"name": "trigger",
|
|
956
|
-
"accepts": {
|
|
957
|
-
"contracts": [
|
|
958
|
-
"interactive"
|
|
959
|
-
]
|
|
960
|
-
},
|
|
961
|
-
"cardinality": "1..1",
|
|
962
|
-
"order": 1
|
|
963
|
-
},
|
|
964
987
|
{
|
|
965
988
|
"name": "body",
|
|
966
989
|
"accepts": {
|
|
967
990
|
"components": [
|
|
968
|
-
"
|
|
969
|
-
"
|
|
991
|
+
"NavGroup",
|
|
992
|
+
"NavItem"
|
|
970
993
|
]
|
|
971
994
|
},
|
|
972
995
|
"cardinality": "1..*",
|
|
973
|
-
"order":
|
|
996
|
+
"order": 1
|
|
974
997
|
}
|
|
975
998
|
],
|
|
976
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
999
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# NavTree\n\nA navigation tree (D88): a `<nav aria-label>` around a `<ul>` of `NavItem`s and `NavGroup`s. Native elements only — *Tab* moves through the group buttons and the links of open groups; there is no roving focus and no `role=\"menu\"` or `role=\"tree\"`, because site navigation is a list of links, not a widget. Router-agnostic: pass your router's link inside each `NavItem`, with `aria-current=\"page\"` on the current one.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `aria-label` | `string` | — | yes | Accessible name of the navigation landmark, such as \"Main\". Required: an app has more than one `<nav>` (a header's, a sidebar's), and an unnamed one is indistinguishable from the rest. Declared here rather than inherited so docgen keeps it in the manifest (D60). |\n| `children` | `ReactNode` | — | yes | `NavItem`s and `NavGroup`s — each renders an `<li>`. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLElement>` | — | no | Forwarded ref to the `<nav>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab / Shift+Tab | Moves through the group buttons and the links of open groups in document order. There is no roving focus and no arrow-key model: it is a list of links, not a menu or a tree widget. |\n\nA native <nav> around a <ul>, named by the required aria-label (an app has more than one navigation landmark). No role=\"menu\" or role=\"tree\" is used (D88). Router-agnostic: the consumer passes its router's link inside each NavItem.\n\n## Theming\n\nOverride `--psi-nav-tree-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
977
1000
|
},
|
|
978
1001
|
{
|
|
979
|
-
"name": "
|
|
980
|
-
"description": "
|
|
1002
|
+
"name": "NavGroup",
|
|
1003
|
+
"description": "A collapsible group of `NavItem`s in a `NavTree` (D88). Controlled-only, like `Tabs` (D67): `open` and `onOpenChange` are required and nothing is remembered. Renders a native `<button aria-expanded aria-controls>` over a `<ul>` that is `hidden` when closed, so the items leave the layout and the accessibility tree while `aria-controls` keeps naming an element. The chevron turns with the state and is `aria-hidden`: the label and `aria-expanded` carry the meaning.",
|
|
981
1004
|
"props": [
|
|
982
1005
|
{
|
|
983
|
-
"name": "
|
|
1006
|
+
"name": "label",
|
|
984
1007
|
"type": "ReactNode",
|
|
985
1008
|
"required": true,
|
|
986
1009
|
"default": null,
|
|
987
|
-
"description": "
|
|
1010
|
+
"description": "The group's visible label, such as \"Reports\"."
|
|
988
1011
|
},
|
|
989
1012
|
{
|
|
990
|
-
"name": "
|
|
991
|
-
"type": "
|
|
1013
|
+
"name": "open",
|
|
1014
|
+
"type": "boolean",
|
|
992
1015
|
"required": true,
|
|
993
1016
|
"default": null,
|
|
994
|
-
"description": "
|
|
1017
|
+
"description": "Whether the group's items are shown. Controlled."
|
|
995
1018
|
},
|
|
996
1019
|
{
|
|
997
|
-
"name": "
|
|
998
|
-
"type": "
|
|
999
|
-
"required":
|
|
1000
|
-
"default":
|
|
1001
|
-
"description": "
|
|
1020
|
+
"name": "onOpenChange",
|
|
1021
|
+
"type": "(open: boolean) => void",
|
|
1022
|
+
"required": true,
|
|
1023
|
+
"default": null,
|
|
1024
|
+
"description": "Called with the next state when the button is pressed. The group never\nchanges `open` itself."
|
|
1002
1025
|
},
|
|
1003
1026
|
{
|
|
1004
|
-
"name": "
|
|
1005
|
-
"type": "
|
|
1027
|
+
"name": "children",
|
|
1028
|
+
"type": "ReactNode",
|
|
1029
|
+
"required": true,
|
|
1030
|
+
"default": null,
|
|
1031
|
+
"description": "`NavItem`s of the group."
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
"name": "className",
|
|
1035
|
+
"type": "string",
|
|
1006
1036
|
"required": false,
|
|
1007
|
-
"default":
|
|
1008
|
-
"description": ""
|
|
1037
|
+
"default": null,
|
|
1038
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1009
1039
|
}
|
|
1010
1040
|
],
|
|
1011
1041
|
"slots": [],
|
|
1012
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
1013
|
-
},
|
|
1014
|
-
{
|
|
1015
|
-
"name": "MenuSeparator",
|
|
1016
|
-
"description": "Hairline rule between Menu item groups. No props by design (D53).",
|
|
1017
|
-
"props": [],
|
|
1018
|
-
"slots": [],
|
|
1019
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# MenuSeparator\n\nHairline rule between Menu item groups. No props by design (D53).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n\n\nNon-interactive rule with role=\"separator\" — exposed to assistive tech as a separator, never focusable, and skipped by roving navigation.\n\n## Theming\n\nThis component has no `--psi-menu-separator-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1042
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# NavGroup\n\nA collapsible group of `NavItem`s in a `NavTree` (D88). Controlled-only, like `Tabs` (D67): `open` and `onOpenChange` are required and nothing is remembered. Renders a native `<button aria-expanded aria-controls>` over a `<ul>` that is `hidden` when closed, so the items leave the layout and the accessibility tree while `aria-controls` keeps naming an element. The chevron turns with the state and is `aria-hidden`: the label and `aria-expanded` carry the meaning.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `label` | `ReactNode` | — | yes | The group's visible label, such as \"Reports\". |\n| `open` | `boolean` | — | yes | Whether the group's items are shown. Controlled. |\n| `onOpenChange` | `(open: boolean) => void` | — | yes | Called with the next state when the button is pressed. The group never changes `open` itself. |\n| `children` | `ReactNode` | — | yes | `NavItem`s of the group. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Activates the group's native button, which asks the parent to open or close the group through onOpenChange. |\n\nControlled-only: open and onOpenChange are required and the group never changes its own state. Renders a <button type=\"button\"> with aria-expanded and aria-controls naming its <ul>, which carries `hidden` when closed, so the links leave the layout and the accessibility tree while aria-controls still resolves. The chevron is aria-hidden and turns on a duration token; the label and aria-expanded carry the meaning.\n\n## Theming\n\nOverride `--psi-nav-tree-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1020
1043
|
},
|
|
1021
1044
|
{
|
|
1022
|
-
"name": "
|
|
1023
|
-
"description": "
|
|
1045
|
+
"name": "NavItem",
|
|
1046
|
+
"description": "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.",
|
|
1024
1047
|
"props": [
|
|
1025
1048
|
{
|
|
1026
|
-
"name": "
|
|
1027
|
-
"type": "
|
|
1028
|
-
"required":
|
|
1029
|
-
"default":
|
|
1030
|
-
"description": "
|
|
1049
|
+
"name": "children",
|
|
1050
|
+
"type": "ReactNode",
|
|
1051
|
+
"required": true,
|
|
1052
|
+
"default": null,
|
|
1053
|
+
"description": "Without `href`: one anchor, your router's link. With `href`: the link's\ntext."
|
|
1031
1054
|
},
|
|
1032
1055
|
{
|
|
1033
|
-
"name": "
|
|
1034
|
-
"type": "
|
|
1056
|
+
"name": "href",
|
|
1057
|
+
"type": "string",
|
|
1035
1058
|
"required": false,
|
|
1036
1059
|
"default": null,
|
|
1037
|
-
"description": "
|
|
1060
|
+
"description": "Render the anchor yourself (D88), for a plain link or a preset: the\n`<li>` holds `<a href>` with `children` as its text. Router apps leave it\nout and pass their own link as the child, so the link keeps its own\nhandlers."
|
|
1038
1061
|
},
|
|
1039
1062
|
{
|
|
1040
|
-
"name": "
|
|
1063
|
+
"name": "current",
|
|
1041
1064
|
"type": "boolean",
|
|
1042
1065
|
"required": false,
|
|
1043
|
-
"default":
|
|
1044
|
-
"description": "
|
|
1066
|
+
"default": null,
|
|
1067
|
+
"description": "Mark the link as the current page (`aria-current=\"page\"`): on the\nanchor `href` renders, or on the single element you pass as a child (D93).\nWithout `current`, a child's own `aria-current` is left alone, so a router\nthat marks its active link keeps working."
|
|
1045
1068
|
},
|
|
1046
1069
|
{
|
|
1047
|
-
"name": "
|
|
1048
|
-
"type": "
|
|
1070
|
+
"name": "className",
|
|
1071
|
+
"type": "string",
|
|
1049
1072
|
"required": false,
|
|
1050
1073
|
"default": null,
|
|
1051
|
-
"description": "
|
|
1074
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1052
1075
|
},
|
|
1053
1076
|
{
|
|
1054
|
-
"name": "
|
|
1055
|
-
"type": "
|
|
1077
|
+
"name": "ref",
|
|
1078
|
+
"type": "Ref<HTMLLIElement>",
|
|
1056
1079
|
"required": false,
|
|
1057
1080
|
"default": null,
|
|
1058
|
-
"description": "
|
|
1059
|
-
}
|
|
1081
|
+
"description": "Forwarded ref to the `<li>`."
|
|
1082
|
+
}
|
|
1083
|
+
],
|
|
1084
|
+
"slots": [],
|
|
1085
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# NavItem\n\nOne 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.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | yes | Without `href`: one anchor, your router's link. With `href`: the link's text. |\n| `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. |\n| `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. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLLIElement>` | — | no | Forwarded ref to the `<li>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter | Follows the link inside it (native anchor behavior); the link is the consumer's router link. |\n\nAn <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.\n\n## Theming\n\nOverride `--psi-nav-tree-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
"name": "AppShell",
|
|
1089
|
+
"description": "The frame of an application (D88): a header row over a sidebar and a `<main>` that each scroll on their own, filling the viewport (`100dvh`). Because the header has a row of its own and `main` is its own scroller, nothing scrolls under the header, so a focused element is never obscured by it (WCAG 2.2 *Focus Not Obscured*) without `scroll-padding`. `main` carries `id={mainId}` and `tabIndex={-1}`: the target of a `SkipLink`, not a tab stop; focused by the skip link it draws the shared focus ring inside its box. A closed sidebar is `hidden`, not narrowed to icons; there is no responsive drawer and no stored open state, so this is a desktop frame. A page whose document must scroll as a whole does not use `AppShell`. Mount it at the root of the page with no margin or padding around it: it is `100dvh` tall, so anything around it makes the document scroll and slides the frame. Do not place a `ToastRegion` at `top-start` in an app with a skip link: the native top layer paints over the focused link.",
|
|
1090
|
+
"props": [
|
|
1060
1091
|
{
|
|
1061
|
-
"name": "
|
|
1062
|
-
"type": "
|
|
1092
|
+
"name": "skipLink",
|
|
1093
|
+
"type": "ReactNode",
|
|
1063
1094
|
"required": false,
|
|
1064
|
-
"default":
|
|
1065
|
-
"description": "
|
|
1095
|
+
"default": null,
|
|
1096
|
+
"description": "A `SkipLink` (D88), rendered first so it is the first tab stop. Point its\n`href` at `#` plus `mainId`."
|
|
1066
1097
|
},
|
|
1067
1098
|
{
|
|
1068
|
-
"name": "
|
|
1069
|
-
"type": "
|
|
1099
|
+
"name": "header",
|
|
1100
|
+
"type": "ReactNode",
|
|
1070
1101
|
"required": false,
|
|
1071
|
-
"default":
|
|
1072
|
-
"description": "
|
|
1102
|
+
"default": null,
|
|
1103
|
+
"description": "The header row, usually a `NavBar` (D88). It sits in a row of its own\nabove the sidebar and `main`, so nothing scrolls under it."
|
|
1073
1104
|
},
|
|
1074
1105
|
{
|
|
1075
|
-
"name": "
|
|
1076
|
-
"type": "
|
|
1106
|
+
"name": "sidebar",
|
|
1107
|
+
"type": "ReactNode",
|
|
1077
1108
|
"required": false,
|
|
1078
1109
|
"default": null,
|
|
1079
|
-
"description": "
|
|
1110
|
+
"description": "The sidebar's content, usually a `NavTree` (D88). The shell wraps it in a\n`<div>`, not an `<aside>`: a `NavTree` is already the landmark."
|
|
1080
1111
|
},
|
|
1081
1112
|
{
|
|
1082
1113
|
"name": "children",
|
|
1083
1114
|
"type": "ReactNode",
|
|
1084
|
-
"required":
|
|
1115
|
+
"required": false,
|
|
1085
1116
|
"default": null,
|
|
1086
|
-
"description": "
|
|
1117
|
+
"description": "The page, rendered inside `<main>` (D88)."
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
"name": "sidebarOpen",
|
|
1121
|
+
"type": "boolean",
|
|
1122
|
+
"required": false,
|
|
1123
|
+
"default": true,
|
|
1124
|
+
"description": "Whether the sidebar is shown. Controlled: wire your Menu toggle's\n`aria-expanded` and this to one piece of state. Closed, the sidebar is\n`hidden`, so it leaves the layout and the accessibility tree, and stays in\nthe DOM so the toggle's `aria-controls` keeps naming an element (D88)."
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
"name": "sidebarTheme",
|
|
1128
|
+
"type": "string",
|
|
1129
|
+
"required": false,
|
|
1130
|
+
"default": null,
|
|
1131
|
+
"description": "A theme name for the sidebar alone, such as `\"dark\"`: sets\n`data-psi-theme` on the sidebar element, so every token inside re-resolves\nunder that theme while the rest of the page keeps its own (D88). The\nsidebar paints `--psi-bg-primary` itself; a theme paints no background."
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
"name": "sidebarId",
|
|
1135
|
+
"type": "string",
|
|
1136
|
+
"required": false,
|
|
1137
|
+
"default": "sidebar",
|
|
1138
|
+
"description": "`id` of the sidebar element, which a toggle names with `aria-controls`\n(D88)."
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
"name": "mainId",
|
|
1142
|
+
"type": "string",
|
|
1143
|
+
"required": false,
|
|
1144
|
+
"default": "main",
|
|
1145
|
+
"description": "`id` of `<main>`, which a `SkipLink`'s `href` names (D88)."
|
|
1087
1146
|
},
|
|
1088
1147
|
{
|
|
1089
1148
|
"name": "className",
|
|
@@ -1094,26 +1153,314 @@
|
|
|
1094
1153
|
},
|
|
1095
1154
|
{
|
|
1096
1155
|
"name": "ref",
|
|
1097
|
-
"type": "Ref<
|
|
1156
|
+
"type": "Ref<HTMLDivElement>",
|
|
1098
1157
|
"required": false,
|
|
1099
1158
|
"default": null,
|
|
1100
|
-
"description": "Forwarded ref to the
|
|
1159
|
+
"description": "Forwarded ref to the root `<div>`."
|
|
1101
1160
|
}
|
|
1102
1161
|
],
|
|
1103
1162
|
"slots": [
|
|
1104
1163
|
{
|
|
1105
|
-
"name": "
|
|
1164
|
+
"name": "skipLink",
|
|
1106
1165
|
"accepts": {
|
|
1107
1166
|
"components": [
|
|
1108
|
-
"
|
|
1109
|
-
"TableBody"
|
|
1167
|
+
"SkipLink"
|
|
1110
1168
|
]
|
|
1111
1169
|
},
|
|
1112
|
-
"cardinality": "1
|
|
1170
|
+
"cardinality": "0..1",
|
|
1113
1171
|
"order": 1
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
"name": "header",
|
|
1175
|
+
"accepts": {
|
|
1176
|
+
"components": [
|
|
1177
|
+
"NavBar"
|
|
1178
|
+
]
|
|
1179
|
+
},
|
|
1180
|
+
"cardinality": "0..1",
|
|
1181
|
+
"order": 2
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
"name": "sidebar",
|
|
1185
|
+
"accepts": {
|
|
1186
|
+
"components": [
|
|
1187
|
+
"NavTree"
|
|
1188
|
+
]
|
|
1189
|
+
},
|
|
1190
|
+
"cardinality": "0..1",
|
|
1191
|
+
"order": 3
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
"name": "body",
|
|
1195
|
+
"accepts": {},
|
|
1196
|
+
"cardinality": "0..*",
|
|
1197
|
+
"order": 4
|
|
1114
1198
|
}
|
|
1115
1199
|
],
|
|
1116
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
1200
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# AppShell\n\nThe frame of an application (D88): a header row over a sidebar and a `<main>` that each scroll on their own, filling the viewport (`100dvh`). Because the header has a row of its own and `main` is its own scroller, nothing scrolls under the header, so a focused element is never obscured by it (WCAG 2.2 *Focus Not Obscured*) without `scroll-padding`. `main` carries `id={mainId}` and `tabIndex={-1}`: the target of a `SkipLink`, not a tab stop; focused by the skip link it draws the shared focus ring inside its box. A closed sidebar is `hidden`, not narrowed to icons; there is no responsive drawer and no stored open state, so this is a desktop frame. A page whose document must scroll as a whole does not use `AppShell`. Mount it at the root of the page with no margin or padding around it: it is `100dvh` tall, so anything around it makes the document scroll and slides the frame. Do not place a `ToastRegion` at `top-start` in an app with a skip link: the native top layer paints over the focused link.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `skipLink` | `ReactNode` | — | no | A `SkipLink` (D88), rendered first so it is the first tab stop. Point its `href` at `#` plus `mainId`. |\n| `header` | `ReactNode` | — | no | The header row, usually a `NavBar` (D88). It sits in a row of its own above the sidebar and `main`, so nothing scrolls under it. |\n| `sidebar` | `ReactNode` | — | no | The sidebar's content, usually a `NavTree` (D88). The shell wraps it in a `<div>`, not an `<aside>`: a `NavTree` is already the landmark. |\n| `children` | `ReactNode` | — | no | The page, rendered inside `<main>` (D88). |\n| `sidebarOpen` | `boolean` | true | no | Whether the sidebar is shown. Controlled: wire your Menu toggle's `aria-expanded` and this to one piece of state. Closed, the sidebar is `hidden`, so it leaves the layout and the accessibility tree, and stays in the DOM so the toggle's `aria-controls` keeps naming an element (D88). |\n| `sidebarTheme` | `string` | — | no | A theme name for the sidebar alone, such as `\"dark\"`: sets `data-psi-theme` on the sidebar element, so every token inside re-resolves under that theme while the rest of the page keeps its own (D88). The sidebar paints `--psi-bg-primary` itself; a theme paints no background. |\n| `sidebarId` | `string` | sidebar | no | `id` of the sidebar element, which a toggle names with `aria-controls` (D88). |\n| `mainId` | `string` | main | no | `id` of `<main>`, which a `SkipLink`'s `href` names (D88). |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | The first stop is the skip link (when given), then the header, the sidebar's links and buttons, and the links of main, in document order. The shell adds no stop of its own: main has tabIndex={-1}. |\n| Enter (on the skip link) | Follows the fragment href: focus moves to main, drawing the shared focus ring inside its box, and the next Tab continues from main's first link. |\n\nA grid filling the viewport: the header sits in a row of its own and main is its own scroller, so nothing scrolls under the header and a focused element is never obscured by it (WCAG 2.2 Focus Not Obscured) without scroll-padding, which does nothing on a main that is not the document's scroller (D88). main carries id={mainId} and tabIndex={-1}, the skip link's target. A closed sidebar is `hidden`, so it leaves the layout and the accessibility tree while its id stays in the DOM for a toggle's aria-controls; the toggle's aria-expanded and sidebarOpen are wired by the consumer, since the shell is controlled and stores nothing. The sidebar is a div, not an aside: the NavTree inside is already the navigation landmark. sidebarTheme sets data-psi-theme on the sidebar alone, so its tokens re-resolve under that theme in a page that keeps its own; the sidebar paints --psi-bg-primary itself. Desktop frame only: no responsive drawer. Do not place a ToastRegion at top-start in an app with a skip link.\n\n## Theming\n\nOverride `--psi-app-shell-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
"name": "AspectRatio",
|
|
1204
|
+
"description": "Fixed-aspect-ratio box that sizes its child media.",
|
|
1205
|
+
"props": [
|
|
1206
|
+
{
|
|
1207
|
+
"name": "children",
|
|
1208
|
+
"type": "ReactNode",
|
|
1209
|
+
"required": false,
|
|
1210
|
+
"default": null,
|
|
1211
|
+
"description": "Content constrained to the ratio."
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
"name": "ratio",
|
|
1215
|
+
"type": "number",
|
|
1216
|
+
"required": true,
|
|
1217
|
+
"default": null,
|
|
1218
|
+
"description": "Width/height ratio, e.g. 16/10 or 4/5."
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
"name": "ref",
|
|
1222
|
+
"type": "Ref<HTMLDivElement>",
|
|
1223
|
+
"required": false,
|
|
1224
|
+
"default": null,
|
|
1225
|
+
"description": "Forwarded ref to the frame element."
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
"name": "className",
|
|
1229
|
+
"type": "string",
|
|
1230
|
+
"required": false,
|
|
1231
|
+
"default": null,
|
|
1232
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1233
|
+
}
|
|
1234
|
+
],
|
|
1235
|
+
"slots": [],
|
|
1236
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# AspectRatio\n\nFixed-aspect-ratio box that sizes its child media.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Content constrained to the ratio. |\n| `ratio` | `number` | — | yes | Width/height ratio, e.g. 16/10 or 4/5. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the frame element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-aspect-ratio-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
"name": "Menu",
|
|
1240
|
+
"description": "Action menu on the native Popover API top layer: the top layer and light dismiss come from the platform; Esc, roving keyboard, placement and dismissal reasons are Psi's (D53). Controlled-only, like Dialog (D50). Controlled-only means Menu does not change its own visibility. Esc and item-select *report* a dismissal via onClose(reason) and leave the popover open; only the consumer flipping `open` to false actually closes it. Esc calls preventDefault() to suppress the platform's own popover dismissal, which is what makes that possible. Light dismiss (outside click) is the one asymmetry, and it is forced by the platform: the browser hides the popover itself and the hide-side `beforetoggle` is not cancelable, so the popover is already closed by the time the resulting `toggle` lets us report onClose(\"outside\"). The consumer must still flip `open` to false so React's state matches the DOM. `toggle` therefore reports only that one reason. A close driven by the consumer (`open` true -> false) runs through the sync effect's own hidePopover(), which raises a `toggle` too — that one is suppressed, because a programmatic close is not a dismissal and must not call onClose.",
|
|
1241
|
+
"props": [
|
|
1242
|
+
{
|
|
1243
|
+
"name": "open",
|
|
1244
|
+
"type": "boolean",
|
|
1245
|
+
"required": true,
|
|
1246
|
+
"default": null,
|
|
1247
|
+
"description": "Controlled open state; syncs to showPopover()/hidePopover()."
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
"name": "onClose",
|
|
1251
|
+
"type": "(reason: \"item-select\" | \"esc\" | \"outside\") => void",
|
|
1252
|
+
"required": true,
|
|
1253
|
+
"default": null,
|
|
1254
|
+
"description": "Called when a dismissal is *requested*; the consumer flips `open`.\n\nMenu never closes itself on `\"esc\"` or `\"item-select\"` — the popover stays\nopen until `open` becomes false (D50). `\"outside\"` is the one exception:\nlight dismiss is performed by the browser before it tells us, so by the\ntime that reason is reported the popover is already closed. Flipping\n`open` to false is still required, to keep React's state in step.\n\nInvariant (D58): a dismissal is only ever reported for a menu that is\ncurrently open according to its own `open` prop. The platform can close\nan auto popover before the consumer's state catches up — clicking another\nmenu's trigger light-dismisses this one before the consumer's click\nhandler runs — and a report for an already-closed menu would clear a\nselection that has since moved on."
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
"name": "trigger",
|
|
1258
|
+
"type": "ReactElement<Record<string, unknown>, string | JSXElementConstructor<any>>",
|
|
1259
|
+
"required": true,
|
|
1260
|
+
"default": null,
|
|
1261
|
+
"description": "The trigger element. Must be a single React element that spreads unknown\nprops onto a focusable node (a Psi `Button`, say): Menu clones it to add\n`aria-haspopup=\"menu\"` and `aria-expanded`, so assistive tech associates\nthe menu with the control users actually focus."
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
"name": "placement",
|
|
1265
|
+
"type": "\"bottom-start\" | \"bottom-end\" | \"top-start\" | \"top-end\"",
|
|
1266
|
+
"required": false,
|
|
1267
|
+
"default": "bottom-start",
|
|
1268
|
+
"description": "Placement relative to the trigger."
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
"name": "aria-label",
|
|
1272
|
+
"type": "string",
|
|
1273
|
+
"required": false,
|
|
1274
|
+
"default": null,
|
|
1275
|
+
"description": "Accessible name for the menu when there is no visible label."
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
"name": "children",
|
|
1279
|
+
"type": "ReactNode",
|
|
1280
|
+
"required": true,
|
|
1281
|
+
"default": null,
|
|
1282
|
+
"description": "MenuItem and MenuSeparator children."
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
"name": "className",
|
|
1286
|
+
"type": "string",
|
|
1287
|
+
"required": false,
|
|
1288
|
+
"default": null,
|
|
1289
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
"name": "ref",
|
|
1293
|
+
"type": "Ref<HTMLDivElement>",
|
|
1294
|
+
"required": false,
|
|
1295
|
+
"default": null,
|
|
1296
|
+
"description": "Forwarded ref to the popover element."
|
|
1297
|
+
}
|
|
1298
|
+
],
|
|
1299
|
+
"slots": [
|
|
1300
|
+
{
|
|
1301
|
+
"name": "trigger",
|
|
1302
|
+
"accepts": {
|
|
1303
|
+
"contracts": [
|
|
1304
|
+
"interactive"
|
|
1305
|
+
]
|
|
1306
|
+
},
|
|
1307
|
+
"cardinality": "1..1",
|
|
1308
|
+
"order": 1
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
"name": "body",
|
|
1312
|
+
"accepts": {
|
|
1313
|
+
"components": [
|
|
1314
|
+
"MenuItem",
|
|
1315
|
+
"MenuSeparator"
|
|
1316
|
+
]
|
|
1317
|
+
},
|
|
1318
|
+
"cardinality": "1..*",
|
|
1319
|
+
"order": 2
|
|
1320
|
+
}
|
|
1321
|
+
],
|
|
1322
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Menu\n\nAction menu on the native Popover API top layer: the top layer and light dismiss come from the platform; Esc, roving keyboard, placement and dismissal reasons are Psi's (D53). Controlled-only, like Dialog (D50). Controlled-only means Menu does not change its own visibility. Esc and item-select *report* a dismissal via onClose(reason) and leave the popover open; only the consumer flipping `open` to false actually closes it. Esc calls preventDefault() to suppress the platform's own popover dismissal, which is what makes that possible. Light dismiss (outside click) is the one asymmetry, and it is forced by the platform: the browser hides the popover itself and the hide-side `beforetoggle` is not cancelable, so the popover is already closed by the time the resulting `toggle` lets us report onClose(\"outside\"). The consumer must still flip `open` to false so React's state matches the DOM. `toggle` therefore reports only that one reason. A close driven by the consumer (`open` true -> false) runs through the sync effect's own hidePopover(), which raises a `toggle` too — that one is suppressed, because a programmatic close is not a dismissal and must not call onClose.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `open` | `boolean` | — | yes | Controlled open state; syncs to showPopover()/hidePopover(). |\n| `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. |\n| `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. |\n| `placement` | `\"bottom-start\" \\| \"bottom-end\" \\| \"top-start\" \\| \"top-end\"` | bottom-start | no | Placement relative to the trigger. |\n| `aria-label` | `string` | — | no | Accessible name for the menu when there is no visible label. |\n| `children` | `ReactNode` | — | yes | MenuItem and MenuSeparator children. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the popover element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Arrow Down / Arrow Up | Moves between enabled items, wrapping at both ends. Disabled items are skipped. |\n| Home / End | Jumps to the first or last enabled item. |\n| 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. |\n| Esc | Suppresses the platform's own dismissal and reports onClose(\"esc\"); the menu stays open until the consumer flips `open` (D50). |\n| Enter / Space | Activates the focused item (native button behavior). |\n\nOpens 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.\n\n## Theming\n\nOverride `--psi-menu-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
"name": "MenuItem",
|
|
1326
|
+
"description": "One action in a Menu. Renders a real <button> so activation, Enter and Space come from the platform; the roving tabindex is applied by Menu's keyboard hook, which finds items via [data-psi-menu-item] (D53).",
|
|
1327
|
+
"props": [
|
|
1328
|
+
{
|
|
1329
|
+
"name": "children",
|
|
1330
|
+
"type": "ReactNode",
|
|
1331
|
+
"required": true,
|
|
1332
|
+
"default": null,
|
|
1333
|
+
"description": "Item label."
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
"name": "onSelect",
|
|
1337
|
+
"type": "() => void",
|
|
1338
|
+
"required": true,
|
|
1339
|
+
"default": null,
|
|
1340
|
+
"description": "Fires on activation; Menu then reports the dismissal via onClose(\"item-select\") (D50)."
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
"name": "variant",
|
|
1344
|
+
"type": "\"neutral\" | \"danger\"",
|
|
1345
|
+
"required": false,
|
|
1346
|
+
"default": "neutral",
|
|
1347
|
+
"description": "danger is for destructive actions only (house rule)."
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
"name": "disabled",
|
|
1351
|
+
"type": "boolean",
|
|
1352
|
+
"required": false,
|
|
1353
|
+
"default": false,
|
|
1354
|
+
"description": ""
|
|
1355
|
+
}
|
|
1356
|
+
],
|
|
1357
|
+
"slots": [],
|
|
1358
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# MenuItem\n\nOne action in a Menu. Renders a real <button> so activation, Enter and Space come from the platform; the roving tabindex is applied by Menu's keyboard hook, which finds items via [data-psi-menu-item] (D53).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | yes | Item label. |\n| `onSelect` | `() => void` | — | yes | Fires on activation; Menu then reports the dismissal via onClose(\"item-select\") (D50). |\n| `variant` | `\"neutral\" \\| \"danger\"` | neutral | no | danger is for destructive actions only (house rule). |\n| `disabled` | `boolean` | false | no | |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Activates; Menu reports onClose(\"item-select\") but does not close itself. |\n\nRenders a real <button>. disabled sets aria-disabled (not the disabled attribute) so the item stays discoverable to assistive tech while being skipped by roving navigation. variant=\"danger\" is for destructive actions only.\n\n## Theming\n\nThis component has no `--psi-menu-item-*` 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.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
"name": "MenuSeparator",
|
|
1362
|
+
"description": "Hairline rule between Menu item groups. No props by design (D53).",
|
|
1363
|
+
"props": [],
|
|
1364
|
+
"slots": [],
|
|
1365
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# MenuSeparator\n\nHairline rule between Menu item groups. No props by design (D53).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n\n\nNon-interactive rule with role=\"separator\" — exposed to assistive tech as a separator, never focusable, and skipped by roving navigation.\n\n## Theming\n\nThis component has no `--psi-menu-separator-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
"name": "Table",
|
|
1369
|
+
"description": "Data table on native table semantics. Holds no state: sorting, selection and pagination are the consumer's (D62, extending D50/D53).",
|
|
1370
|
+
"props": [
|
|
1371
|
+
{
|
|
1372
|
+
"name": "size",
|
|
1373
|
+
"type": "32 | 40 | 48",
|
|
1374
|
+
"required": false,
|
|
1375
|
+
"default": 40,
|
|
1376
|
+
"description": "Row height in px."
|
|
1377
|
+
},
|
|
1378
|
+
{
|
|
1379
|
+
"name": "stickyHeader",
|
|
1380
|
+
"type": "boolean",
|
|
1381
|
+
"required": false,
|
|
1382
|
+
"default": null,
|
|
1383
|
+
"description": "Pins the header while the body scrolls."
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
"name": "sortable",
|
|
1387
|
+
"type": "boolean",
|
|
1388
|
+
"required": false,
|
|
1389
|
+
"default": false,
|
|
1390
|
+
"description": "Enables the sort affordance on header cells that declare a `sortKey`."
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
"name": "sort",
|
|
1394
|
+
"type": "TableSortState | null",
|
|
1395
|
+
"required": false,
|
|
1396
|
+
"default": null,
|
|
1397
|
+
"description": "Controlled sort state; `null` when nothing is sorted."
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
"name": "onSortChange",
|
|
1401
|
+
"type": "((sort: TableSortState) => void)",
|
|
1402
|
+
"required": false,
|
|
1403
|
+
"default": null,
|
|
1404
|
+
"description": "Called with the **next** sort state, already toggled — store it as given\nand do not toggle again. A fresh column arrives `\"asc\"`; an active `\"asc\"`\ncolumn emits `\"desc\"`; an active `\"desc\"` column emits `\"asc\"`.\n\nOptional in the type because `sortable` may be false; a discriminated union\nexpressing the real contract does not survive docgen's flat prop\nextraction, which would strip these props from the manifest entirely (D62)."
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
"name": "selectable",
|
|
1408
|
+
"type": "boolean",
|
|
1409
|
+
"required": false,
|
|
1410
|
+
"default": false,
|
|
1411
|
+
"description": "Renders the row-selection checkbox column."
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
"name": "selected",
|
|
1415
|
+
"type": "ReadonlySet<string>",
|
|
1416
|
+
"required": false,
|
|
1417
|
+
"default": "new Set<string>()",
|
|
1418
|
+
"description": "Controlled selection, keyed by each `TableRow`'s `rowId`."
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
"name": "onSelectionChange",
|
|
1422
|
+
"type": "((selected: ReadonlySet<string>) => void)",
|
|
1423
|
+
"required": false,
|
|
1424
|
+
"default": null,
|
|
1425
|
+
"description": "Called with the next selection. See `onSortChange` on why it is optional."
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
"name": "children",
|
|
1429
|
+
"type": "ReactNode",
|
|
1430
|
+
"required": true,
|
|
1431
|
+
"default": null,
|
|
1432
|
+
"description": "TableHead and TableBody."
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
"name": "className",
|
|
1436
|
+
"type": "string",
|
|
1437
|
+
"required": false,
|
|
1438
|
+
"default": null,
|
|
1439
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
"name": "ref",
|
|
1443
|
+
"type": "Ref<HTMLTableElement>",
|
|
1444
|
+
"required": false,
|
|
1445
|
+
"default": null,
|
|
1446
|
+
"description": "Forwarded ref to the underlying `<table>` element."
|
|
1447
|
+
}
|
|
1448
|
+
],
|
|
1449
|
+
"slots": [
|
|
1450
|
+
{
|
|
1451
|
+
"name": "body",
|
|
1452
|
+
"accepts": {
|
|
1453
|
+
"components": [
|
|
1454
|
+
"TableCaption",
|
|
1455
|
+
"TableHead",
|
|
1456
|
+
"TableBody"
|
|
1457
|
+
]
|
|
1458
|
+
},
|
|
1459
|
+
"cardinality": "1..*",
|
|
1460
|
+
"order": 1
|
|
1461
|
+
}
|
|
1462
|
+
],
|
|
1463
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Table\n\nData table on native table semantics. Holds no state: sorting, selection and pagination are the consumer's (D62, extending D50/D53).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `size` | `32 \\| 40 \\| 48` | 40 | no | Row height in px. |\n| `stickyHeader` | `boolean` | — | no | Pins the header while the body scrolls. |\n| `sortable` | `boolean` | false | no | Enables the sort affordance on header cells that declare a `sortKey`. |\n| `sort` | `TableSortState \\| null` | — | no | Controlled sort state; `null` when nothing is sorted. |\n| `onSortChange` | `((sort: TableSortState) => void)` | — | no | Called with the **next** sort state, already toggled — store it as given and do not toggle again. A fresh column arrives `\"asc\"`; an active `\"asc\"` column emits `\"desc\"`; an active `\"desc\"` column emits `\"asc\"`. Optional in the type because `sortable` may be false; a discriminated union expressing the real contract does not survive docgen's flat prop extraction, which would strip these props from the manifest entirely (D62). |\n| `selectable` | `boolean` | false | no | Renders the row-selection checkbox column. |\n| `selected` | `ReadonlySet<string>` | new Set<string>() | no | Controlled selection, keyed by each `TableRow`'s `rowId`. |\n| `onSelectionChange` | `((selected: ReadonlySet<string>) => void)` | — | no | Called with the next selection. See `onSortChange` on why it is optional. |\n| `children` | `ReactNode` | — | yes | TableHead and TableBody. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLTableElement>` | — | no | Forwarded ref to the underlying `<table>` element. |\n\n## Theming\n\nOverride `--psi-table-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1117
1464
|
},
|
|
1118
1465
|
{
|
|
1119
1466
|
"name": "TableHead",
|
|
@@ -1124,7 +1471,336 @@
|
|
|
1124
1471
|
"type": "ReactNode",
|
|
1125
1472
|
"required": true,
|
|
1126
1473
|
"default": null,
|
|
1127
|
-
"description": "The header TableRow."
|
|
1474
|
+
"description": "The header TableRow."
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
"name": "className",
|
|
1478
|
+
"type": "string",
|
|
1479
|
+
"required": false,
|
|
1480
|
+
"default": null,
|
|
1481
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1482
|
+
},
|
|
1483
|
+
{
|
|
1484
|
+
"name": "ref",
|
|
1485
|
+
"type": "Ref<HTMLTableSectionElement>",
|
|
1486
|
+
"required": false,
|
|
1487
|
+
"default": null,
|
|
1488
|
+
"description": "Forwarded ref to the `<thead>`."
|
|
1489
|
+
}
|
|
1490
|
+
],
|
|
1491
|
+
"slots": [],
|
|
1492
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableHead\n\n`<thead>`. When the table is selectable, prepends the select-all cell to the FIRST header row so the header's column count matches the body's — the checkbox column is Table's, not the consumer's schema. Clones the row rather than rebuilding it, so the consumer's own props on that `TableRow` survive.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | yes | The header TableRow. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLTableSectionElement>` | — | no | Forwarded ref to the `<thead>`. |\n\n## Theming\n\nThis component has no `--psi-table-head-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
"name": "TableBody",
|
|
1496
|
+
"description": "`<tbody>` wrapper. Remaining attributes land on it (D85) — `aria-busy` while a page of rows loads, say.",
|
|
1497
|
+
"props": [
|
|
1498
|
+
{
|
|
1499
|
+
"name": "children",
|
|
1500
|
+
"type": "ReactNode",
|
|
1501
|
+
"required": true,
|
|
1502
|
+
"default": null,
|
|
1503
|
+
"description": "The body TableRows."
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
"name": "className",
|
|
1507
|
+
"type": "string",
|
|
1508
|
+
"required": false,
|
|
1509
|
+
"default": null,
|
|
1510
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1511
|
+
},
|
|
1512
|
+
{
|
|
1513
|
+
"name": "ref",
|
|
1514
|
+
"type": "Ref<HTMLTableSectionElement>",
|
|
1515
|
+
"required": false,
|
|
1516
|
+
"default": null,
|
|
1517
|
+
"description": "Forwarded ref to the `<tbody>`."
|
|
1518
|
+
}
|
|
1519
|
+
],
|
|
1520
|
+
"slots": [],
|
|
1521
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableBody\n\n`<tbody>` wrapper. Remaining attributes land on it (D85) — `aria-busy` while a page of rows loads, say.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | yes | The body TableRows. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLTableSectionElement>` | — | no | Forwarded ref to the `<tbody>`. |\n\n## Theming\n\nThis component has no `--psi-table-body-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
"name": "TableRow",
|
|
1525
|
+
"description": "`<tr>`. `rowId` is the key the `selected` set holds.",
|
|
1526
|
+
"props": [
|
|
1527
|
+
{
|
|
1528
|
+
"name": "rowId",
|
|
1529
|
+
"type": "string",
|
|
1530
|
+
"required": false,
|
|
1531
|
+
"default": null,
|
|
1532
|
+
"description": "Stable identity for selection. Required for selectable tables (D62)."
|
|
1533
|
+
},
|
|
1534
|
+
{
|
|
1535
|
+
"name": "selectLabel",
|
|
1536
|
+
"type": "string",
|
|
1537
|
+
"required": false,
|
|
1538
|
+
"default": null,
|
|
1539
|
+
"description": "Accessible name for this row's selection checkbox."
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
"name": "children",
|
|
1543
|
+
"type": "ReactNode",
|
|
1544
|
+
"required": true,
|
|
1545
|
+
"default": null,
|
|
1546
|
+
"description": "The row's cells."
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
"name": "className",
|
|
1550
|
+
"type": "string",
|
|
1551
|
+
"required": false,
|
|
1552
|
+
"default": null,
|
|
1553
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1554
|
+
},
|
|
1555
|
+
{
|
|
1556
|
+
"name": "ref",
|
|
1557
|
+
"type": "Ref<HTMLTableRowElement>",
|
|
1558
|
+
"required": false,
|
|
1559
|
+
"default": null,
|
|
1560
|
+
"description": "Forwarded ref to the `<tr>`."
|
|
1561
|
+
}
|
|
1562
|
+
],
|
|
1563
|
+
"slots": [],
|
|
1564
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableRow\n\n`<tr>`. `rowId` is the key the `selected` set holds.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `rowId` | `string` | — | no | Stable identity for selection. Required for selectable tables (D62). |\n| `selectLabel` | `string` | — | no | Accessible name for this row's selection checkbox. |\n| `children` | `ReactNode` | — | yes | The row's cells. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLTableRowElement>` | — | no | Forwarded ref to the `<tr>`. |\n\n## Theming\n\nThis component has no `--psi-table-row-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1565
|
+
},
|
|
1566
|
+
{
|
|
1567
|
+
"name": "TableHeaderCell",
|
|
1568
|
+
"description": "`<th scope=\"col\">`. `aria-sort` belongs on the th, never on the inner button — assistive tech reads the sort state from the column header.",
|
|
1569
|
+
"props": [
|
|
1570
|
+
{
|
|
1571
|
+
"name": "sortKey",
|
|
1572
|
+
"type": "string",
|
|
1573
|
+
"required": false,
|
|
1574
|
+
"default": null,
|
|
1575
|
+
"description": "Sort key this column emits. Enables the sort control when the table is `sortable`."
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
"name": "numeric",
|
|
1579
|
+
"type": "boolean",
|
|
1580
|
+
"required": false,
|
|
1581
|
+
"default": null,
|
|
1582
|
+
"description": "Right-aligns and renders tabular figures (D62)."
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
"name": "children",
|
|
1586
|
+
"type": "ReactNode",
|
|
1587
|
+
"required": false,
|
|
1588
|
+
"default": null,
|
|
1589
|
+
"description": "Header label."
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
"name": "className",
|
|
1593
|
+
"type": "string",
|
|
1594
|
+
"required": false,
|
|
1595
|
+
"default": null,
|
|
1596
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
"name": "ref",
|
|
1600
|
+
"type": "Ref<HTMLTableCellElement>",
|
|
1601
|
+
"required": false,
|
|
1602
|
+
"default": null,
|
|
1603
|
+
"description": "Forwarded ref to the `<th>`."
|
|
1604
|
+
}
|
|
1605
|
+
],
|
|
1606
|
+
"slots": [],
|
|
1607
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableHeaderCell\n\n`<th scope=\"col\">`. `aria-sort` belongs on the th, never on the inner button — assistive tech reads the sort state from the column header.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `sortKey` | `string` | — | no | Sort key this column emits. Enables the sort control when the table is `sortable`. |\n| `numeric` | `boolean` | — | no | Right-aligns and renders tabular figures (D62). |\n| `children` | `ReactNode` | — | no | Header label. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLTableCellElement>` | — | no | Forwarded ref to the `<th>`. |\n\n## Theming\n\nThis component has no `--psi-table-header-cell-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
"name": "TableCell",
|
|
1611
|
+
"description": "`<td>`, or `<th scope=\"row\">` with `rowHeader`. `numeric` means right-aligned *and* tabular — a column that aligns but whose digits jitter between rows defeats the purpose. Remaining attributes land on the cell (D85).",
|
|
1612
|
+
"props": [
|
|
1613
|
+
{
|
|
1614
|
+
"name": "numeric",
|
|
1615
|
+
"type": "boolean",
|
|
1616
|
+
"required": false,
|
|
1617
|
+
"default": null,
|
|
1618
|
+
"description": "Right-aligns and renders tabular figures (D62)."
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
"name": "colSpan",
|
|
1622
|
+
"type": "number",
|
|
1623
|
+
"required": false,
|
|
1624
|
+
"default": null,
|
|
1625
|
+
"description": "Columns the cell spans — an empty-state row spanning the table (D85)."
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
"name": "rowHeader",
|
|
1629
|
+
"type": "boolean",
|
|
1630
|
+
"required": false,
|
|
1631
|
+
"default": null,
|
|
1632
|
+
"description": "Renders `<th scope=\"row\">` instead of `<td>`: the cell that names its\nrow, an identifier say, so a screen reader reads it with every cell in\nthe row (D85). Styled as a cell, at medium weight."
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
"name": "children",
|
|
1636
|
+
"type": "ReactNode",
|
|
1637
|
+
"required": false,
|
|
1638
|
+
"default": null,
|
|
1639
|
+
"description": "Cell content."
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
"name": "className",
|
|
1643
|
+
"type": "string",
|
|
1644
|
+
"required": false,
|
|
1645
|
+
"default": null,
|
|
1646
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
"name": "ref",
|
|
1650
|
+
"type": "Ref<HTMLTableCellElement>",
|
|
1651
|
+
"required": false,
|
|
1652
|
+
"default": null,
|
|
1653
|
+
"description": "Forwarded ref to the cell element."
|
|
1654
|
+
}
|
|
1655
|
+
],
|
|
1656
|
+
"slots": [],
|
|
1657
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableCell\n\n`<td>`, or `<th scope=\"row\">` with `rowHeader`. `numeric` means right-aligned *and* tabular — a column that aligns but whose digits jitter between rows defeats the purpose. Remaining attributes land on the cell (D85).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `numeric` | `boolean` | — | no | Right-aligns and renders tabular figures (D62). |\n| `colSpan` | `number` | — | no | Columns the cell spans — an empty-state row spanning the table (D85). |\n| `rowHeader` | `boolean` | — | no | Renders `<th scope=\"row\">` instead of `<td>`: the cell that names its row, an identifier say, so a screen reader reads it with every cell in the row (D85). Styled as a cell, at medium weight. |\n| `children` | `ReactNode` | — | no | Cell content. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLTableCellElement>` | — | no | Forwarded ref to the cell element. |\n\n## Theming\n\nThis component has no `--psi-table-cell-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
"name": "TableCaption",
|
|
1661
|
+
"description": "`<caption>` — the table's name, above it, and an optional detail line (D85). It names the table for assistive tech, which an `aria-label` on the `Table` would also do, but a caption is visible. A token-paged list has no page numbers to land on, so after a page change the consumer moves focus here: give it `tabIndex={-1}`, put the range in `detail`, and call `focus()` on the ref. The ring is the shared one (D82), drawn inside the caption's box. Remaining attributes land on the element.",
|
|
1662
|
+
"props": [
|
|
1663
|
+
{
|
|
1664
|
+
"name": "children",
|
|
1665
|
+
"type": "ReactNode",
|
|
1666
|
+
"required": true,
|
|
1667
|
+
"default": null,
|
|
1668
|
+
"description": "The name line — what the table lists. Also the table's accessible name."
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
"name": "detail",
|
|
1672
|
+
"type": "ReactNode",
|
|
1673
|
+
"required": false,
|
|
1674
|
+
"default": null,
|
|
1675
|
+
"description": "An optional second line: the range shown, say — \"Rows 51–100 of more\nthan 100,000\"."
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
"name": "className",
|
|
1679
|
+
"type": "string",
|
|
1680
|
+
"required": false,
|
|
1681
|
+
"default": null,
|
|
1682
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
"name": "ref",
|
|
1686
|
+
"type": "Ref<HTMLTableCaptionElement>",
|
|
1687
|
+
"required": false,
|
|
1688
|
+
"default": null,
|
|
1689
|
+
"description": "Forwarded ref to the `<caption>`, so focus can be moved to it."
|
|
1690
|
+
}
|
|
1691
|
+
],
|
|
1692
|
+
"slots": [],
|
|
1693
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableCaption\n\n`<caption>` — the table's name, above it, and an optional detail line (D85). It names the table for assistive tech, which an `aria-label` on the `Table` would also do, but a caption is visible. A token-paged list has no page numbers to land on, so after a page change the consumer moves focus here: give it `tabIndex={-1}`, put the range in `detail`, and call `focus()` on the ref. The ring is the shared one (D82), drawn inside the caption's box. Remaining attributes land on the element.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | yes | The name line — what the table lists. Also the table's accessible name. |\n| `detail` | `ReactNode` | — | no | An optional second line: the range shown, say — \"Rows 51–100 of more than 100,000\". |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLTableCaptionElement>` | — | no | Forwarded ref to the `<caption>`, so focus can be moved to it. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Not a tab stop. With tabIndex={-1} the consumer moves focus to it after a page change, and the shared ring is drawn inside the caption. |\n\nRenders <caption>, which names the table for assistive tech and is visible, unlike an aria-label on Table. Put the range in `detail` (\"Rows 51–100 of more than 100,000\") so that moving focus to the caption after a token-paged page change reads the new range (D85). Remaining attributes land on the element.\n\n## Theming\n\nThis component has no `--psi-table-caption-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
"name": "Pagination",
|
|
1697
|
+
"description": "Numbered pager with ellipsis truncation (D63). Standalone rather than a Table family member: `table-pagination` composes it as a Toolbar sibling of the page-size Select.",
|
|
1698
|
+
"props": [
|
|
1699
|
+
{
|
|
1700
|
+
"name": "page",
|
|
1701
|
+
"type": "number",
|
|
1702
|
+
"required": true,
|
|
1703
|
+
"default": null,
|
|
1704
|
+
"description": "Current page, 1-based. A value outside `[1, pageCount]` (including\nnon-finite values like `NaN`) is clamped for rendering and logs a\n`console.warn` in development. `pageCount < 1` renders no page buttons."
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
"name": "pageCount",
|
|
1708
|
+
"type": "number",
|
|
1709
|
+
"required": true,
|
|
1710
|
+
"default": null,
|
|
1711
|
+
"description": "Total number of pages."
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
"name": "onPageChange",
|
|
1715
|
+
"type": "(page: number) => void",
|
|
1716
|
+
"required": true,
|
|
1717
|
+
"default": null,
|
|
1718
|
+
"description": "Called with the requested page. Required — unlike Table's `onSortChange`/\n`onSelectionChange`, which are optional because `sortable`/`selectable`\ngate whether they're meaningful, `Pagination` has no such gating boolean:\nthe prop is unconditionally meaningful, so a `Pagination` without it is a\ndead control (final review finding, D62)."
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
"name": "siblingCount",
|
|
1722
|
+
"type": "number",
|
|
1723
|
+
"required": false,
|
|
1724
|
+
"default": 1,
|
|
1725
|
+
"description": "Pages shown either side of the current one before truncating."
|
|
1726
|
+
},
|
|
1727
|
+
{
|
|
1728
|
+
"name": "aria-label",
|
|
1729
|
+
"type": "string",
|
|
1730
|
+
"required": false,
|
|
1731
|
+
"default": "Pagination",
|
|
1732
|
+
"description": "Accessible name for the nav landmark."
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
"name": "className",
|
|
1736
|
+
"type": "string",
|
|
1737
|
+
"required": false,
|
|
1738
|
+
"default": null,
|
|
1739
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1740
|
+
}
|
|
1741
|
+
],
|
|
1742
|
+
"slots": [],
|
|
1743
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Pagination\n\nNumbered pager with ellipsis truncation (D63). Standalone rather than a Table family member: `table-pagination` composes it as a Toolbar sibling of the page-size Select.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `page` | `number` | — | yes | Current page, 1-based. A value outside `[1, pageCount]` (including non-finite values like `NaN`) is clamped for rendering and logs a `console.warn` in development. `pageCount < 1` renders no page buttons. |\n| `pageCount` | `number` | — | yes | Total number of pages. |\n| `onPageChange` | `(page: number) => void` | — | yes | Called with the requested page. Required — unlike Table's `onSortChange`/ `onSelectionChange`, which are optional because `sortable`/`selectable` gate whether they're meaningful, `Pagination` has no such gating boolean: the prop is unconditionally meaningful, so a `Pagination` without it is a dead control (final review finding, D62). |\n| `siblingCount` | `number` | 1 | no | Pages shown either side of the current one before truncating. |\n| `aria-label` | `string` | Pagination | no | Accessible name for the nav landmark. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-pagination-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
"name": "CursorPagination",
|
|
1747
|
+
"description": "A pager for a list that pages by cursor (D85): a backend that returns a `nextPageToken` and no page count. Two `ghost` Buttons with visible words in a `<nav>`; the chevrons only reinforce them. A direction that is not available is disabled. It holds no state and knows nothing about pages — the consumer holds the tokens and answers `hasPrevious` / `hasNext`. A sibling of `Pagination`, not a mode of it: `Pagination`'s contract is a page count and numbers, and this one has neither.",
|
|
1748
|
+
"props": [
|
|
1749
|
+
{
|
|
1750
|
+
"name": "hasPrevious",
|
|
1751
|
+
"type": "boolean",
|
|
1752
|
+
"required": true,
|
|
1753
|
+
"default": null,
|
|
1754
|
+
"description": "Whether a previous page exists — the consumer knows, from its tokens."
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
"name": "hasNext",
|
|
1758
|
+
"type": "boolean",
|
|
1759
|
+
"required": true,
|
|
1760
|
+
"default": null,
|
|
1761
|
+
"description": "Whether a next page exists."
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
"name": "onPrevious",
|
|
1765
|
+
"type": "() => void",
|
|
1766
|
+
"required": true,
|
|
1767
|
+
"default": null,
|
|
1768
|
+
"description": "Called when the user asks for the previous page."
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
"name": "onNext",
|
|
1772
|
+
"type": "() => void",
|
|
1773
|
+
"required": true,
|
|
1774
|
+
"default": null,
|
|
1775
|
+
"description": "Called when the user asks for the next page."
|
|
1776
|
+
},
|
|
1777
|
+
{
|
|
1778
|
+
"name": "previousLabel",
|
|
1779
|
+
"type": "string",
|
|
1780
|
+
"required": false,
|
|
1781
|
+
"default": "Previous",
|
|
1782
|
+
"description": "Visible text of the previous button."
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
"name": "nextLabel",
|
|
1786
|
+
"type": "string",
|
|
1787
|
+
"required": false,
|
|
1788
|
+
"default": "Next",
|
|
1789
|
+
"description": "Visible text of the next button."
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
"name": "size",
|
|
1793
|
+
"type": "32 | 40",
|
|
1794
|
+
"required": false,
|
|
1795
|
+
"default": 32,
|
|
1796
|
+
"description": "Button height in px."
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
"name": "aria-label",
|
|
1800
|
+
"type": "string",
|
|
1801
|
+
"required": false,
|
|
1802
|
+
"default": "Pagination",
|
|
1803
|
+
"description": "Accessible name for the nav landmark."
|
|
1128
1804
|
},
|
|
1129
1805
|
{
|
|
1130
1806
|
"name": "className",
|
|
@@ -1135,18 +1811,53 @@
|
|
|
1135
1811
|
}
|
|
1136
1812
|
],
|
|
1137
1813
|
"slots": [],
|
|
1138
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
1814
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# CursorPagination\n\nA pager for a list that pages by cursor (D85): a backend that returns a `nextPageToken` and no page count. Two `ghost` Buttons with visible words in a `<nav>`; the chevrons only reinforce them. A direction that is not available is disabled. It holds no state and knows nothing about pages — the consumer holds the tokens and answers `hasPrevious` / `hasNext`. A sibling of `Pagination`, not a mode of it: `Pagination`'s contract is a page count and numbers, and this one has neither.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `hasPrevious` | `boolean` | — | yes | Whether a previous page exists — the consumer knows, from its tokens. |\n| `hasNext` | `boolean` | — | yes | Whether a next page exists. |\n| `onPrevious` | `() => void` | — | yes | Called when the user asks for the previous page. |\n| `onNext` | `() => void` | — | yes | Called when the user asks for the next page. |\n| `previousLabel` | `string` | Previous | no | Visible text of the previous button. |\n| `nextLabel` | `string` | Next | no | Visible text of the next button. |\n| `size` | `32 \\| 40` | 32 | no | Button height in px. |\n| `aria-label` | `string` | Pagination | no | Accessible name for the nav landmark. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Reaches the Previous and Next buttons in order; a disabled direction is skipped. |\n| Enter / Space | Activates the focused button (native button behavior). |\n\nA <nav> named \"Pagination\" (override with aria-label) holding two ghost Buttons with visible words — Previous and Next, translatable through previousLabel/nextLabel — each with a chevron that only reinforces the word (D85). A direction that is unavailable is disabled, not hidden. It holds no state: the consumer holds the page tokens and answers hasPrevious/hasNext. Pair it with a TableCaption whose detail states the range, and move focus there after a page change.\n\n## Theming\n\nThis component has no `--psi-cursor-pagination-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1139
1815
|
},
|
|
1140
1816
|
{
|
|
1141
|
-
"name": "
|
|
1142
|
-
"description": "
|
|
1817
|
+
"name": "Toast",
|
|
1818
|
+
"description": "A single transient notification card (D64). Controlled and presentational: it holds no state, runs no timer, and never removes itself — `onDismiss` reports and the owner disposes, exactly as Dialog (D50) and Menu (D53) do. Toast does not carry its own `role`/`aria-live`. Politeness belongs to the two persistent wrappers inside ToastRegion, because a live region must pre-exist the content it announces; a role on the toast itself would reintroduce the mount-with-content problem one level down. `politeness` (D83) only tells the region which wrapper to use. Composed by hand with ToastRegion for full control, or driven by ToastProvider's queue.",
|
|
1143
1819
|
"props": [
|
|
1820
|
+
{
|
|
1821
|
+
"name": "variant",
|
|
1822
|
+
"type": "\"neutral\" | \"success\" | \"warning\" | \"danger\"",
|
|
1823
|
+
"required": false,
|
|
1824
|
+
"default": "neutral",
|
|
1825
|
+
"description": "Status variant."
|
|
1826
|
+
},
|
|
1144
1827
|
{
|
|
1145
1828
|
"name": "children",
|
|
1146
1829
|
"type": "ReactNode",
|
|
1147
1830
|
"required": true,
|
|
1148
1831
|
"default": null,
|
|
1149
|
-
"description": "The
|
|
1832
|
+
"description": "The message."
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
"name": "action",
|
|
1836
|
+
"type": "ReactNode",
|
|
1837
|
+
"required": false,
|
|
1838
|
+
"default": null,
|
|
1839
|
+
"description": "Trailing affordance — a ghost Button (Undo, Retry, View)."
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
"name": "onDismiss",
|
|
1843
|
+
"type": "(() => void)",
|
|
1844
|
+
"required": false,
|
|
1845
|
+
"default": null,
|
|
1846
|
+
"description": "When provided, renders the dismiss button and calls this. Toast does not\nremove itself; the owner disposes."
|
|
1847
|
+
},
|
|
1848
|
+
{
|
|
1849
|
+
"name": "politeness",
|
|
1850
|
+
"type": "\"polite\" | \"assertive\"",
|
|
1851
|
+
"required": false,
|
|
1852
|
+
"default": null,
|
|
1853
|
+
"description": "Which live wrapper ToastRegion routes this toast into (D83). Unset, the\nvariant decides: neutral and success are polite, warning and danger\nassertive. Set it when the kind of event, not its tone, decides — the\noutcome of the user's own action may interrupt even when it succeeded."
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
"name": "statusLabel",
|
|
1857
|
+
"type": "string | null",
|
|
1858
|
+
"required": false,
|
|
1859
|
+
"default": null,
|
|
1860
|
+
"description": "The visually hidden status word (D83). A string replaces the default\n(\"Success:\", \"Warning:\", \"Error:\") — pass the translated word; `null`\ndrops it. Unset keeps the default, and `neutral` has none."
|
|
1150
1861
|
},
|
|
1151
1862
|
{
|
|
1152
1863
|
"name": "className",
|
|
@@ -1154,35 +1865,62 @@
|
|
|
1154
1865
|
"required": false,
|
|
1155
1866
|
"default": null,
|
|
1156
1867
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1868
|
+
},
|
|
1869
|
+
{
|
|
1870
|
+
"name": "ref",
|
|
1871
|
+
"type": "Ref<HTMLDivElement>",
|
|
1872
|
+
"required": false,
|
|
1873
|
+
"default": null,
|
|
1874
|
+
"description": "Forwarded ref to the underlying `<div>`."
|
|
1157
1875
|
}
|
|
1158
1876
|
],
|
|
1159
|
-
"slots": [
|
|
1160
|
-
|
|
1877
|
+
"slots": [
|
|
1878
|
+
{
|
|
1879
|
+
"name": "body",
|
|
1880
|
+
"accepts": {},
|
|
1881
|
+
"cardinality": "1..*",
|
|
1882
|
+
"order": 1
|
|
1883
|
+
},
|
|
1884
|
+
{
|
|
1885
|
+
"name": "action",
|
|
1886
|
+
"accepts": {
|
|
1887
|
+
"components": [
|
|
1888
|
+
"Button"
|
|
1889
|
+
],
|
|
1890
|
+
"contracts": [
|
|
1891
|
+
"inline-content"
|
|
1892
|
+
]
|
|
1893
|
+
},
|
|
1894
|
+
"cardinality": "0..1",
|
|
1895
|
+
"order": 2
|
|
1896
|
+
}
|
|
1897
|
+
],
|
|
1898
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Toast\n\nA single transient notification card (D64). Controlled and presentational: it holds no state, runs no timer, and never removes itself — `onDismiss` reports and the owner disposes, exactly as Dialog (D50) and Menu (D53) do. Toast does not carry its own `role`/`aria-live`. Politeness belongs to the two persistent wrappers inside ToastRegion, because a live region must pre-exist the content it announces; a role on the toast itself would reintroduce the mount-with-content problem one level down. `politeness` (D83) only tells the region which wrapper to use. Composed by hand with ToastRegion for full control, or driven by ToastProvider's queue.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"neutral\" \\| \"success\" \\| \"warning\" \\| \"danger\"` | neutral | no | Status variant. |\n| `children` | `ReactNode` | — | yes | The message. |\n| `action` | `ReactNode` | — | no | Trailing affordance — a ghost Button (Undo, Retry, View). |\n| `onDismiss` | `(() => void)` | — | no | When provided, renders the dismiss button and calls this. Toast does not remove itself; the owner disposes. |\n| `politeness` | `\"polite\" \\| \"assertive\"` | — | no | Which live wrapper ToastRegion routes this toast into (D83). Unset, the variant decides: neutral and success are polite, warning and danger assertive. Set it when the kind of event, not its tone, decides — the outcome of the user's own action may interrupt even when it succeeded. |\n| `statusLabel` | `string \\| null` | — | no | The visually hidden status word (D83). A string replaces the default (\"Success:\", \"Warning:\", \"Error:\") — pass the translated word; `null` drops it. Unset keeps the default, and `neutral` has none. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the underlying `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Reaches the action and the dismiss button in DOM order. Esc is not a dismissal — a toast is not modal and traps nothing. |\n| Enter / Space | Activates the focused action or dismiss button. |\n\nPresentational and controlled (D64): onDismiss reports and the owner disposes; Toast never removes itself. It carries no role/aria-live of its own — politeness belongs to ToastRegion's two persistent wrappers. The variant's meaning is announced by a visually hidden status word (\"Success:\", \"Warning:\", \"Error:\"), never by colour and icon shape alone; the icon is aria-hidden. neutral has no status and gets no prefix. statusLabel replaces that word with a translated one, or drops it with null (D83). politeness tells ToastRegion which wrapper speaks the toast; unset, the variant decides. The dismiss button requires no props — it is labelled \"Dismiss notification\".\n\n## Theming\n\nOverride `--psi-toast-*` custom properties at any scope.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1161
1899
|
},
|
|
1162
1900
|
{
|
|
1163
|
-
"name": "
|
|
1164
|
-
"description": "
|
|
1901
|
+
"name": "ToastRegion",
|
|
1902
|
+
"description": "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.",
|
|
1165
1903
|
"props": [
|
|
1166
1904
|
{
|
|
1167
|
-
"name": "
|
|
1168
|
-
"type": "
|
|
1905
|
+
"name": "placement",
|
|
1906
|
+
"type": "\"top-start\" | \"top-end\" | \"bottom-start\" | \"bottom-end\"",
|
|
1169
1907
|
"required": false,
|
|
1170
|
-
"default":
|
|
1171
|
-
"description": "
|
|
1908
|
+
"default": "bottom-end",
|
|
1909
|
+
"description": "Corner the stack occupies."
|
|
1172
1910
|
},
|
|
1173
1911
|
{
|
|
1174
|
-
"name": "
|
|
1912
|
+
"name": "aria-label",
|
|
1175
1913
|
"type": "string",
|
|
1176
1914
|
"required": false,
|
|
1177
|
-
"default":
|
|
1178
|
-
"description": "Accessible name for
|
|
1915
|
+
"default": "Notifications",
|
|
1916
|
+
"description": "Accessible name for the region landmark."
|
|
1179
1917
|
},
|
|
1180
1918
|
{
|
|
1181
1919
|
"name": "children",
|
|
1182
1920
|
"type": "ReactNode",
|
|
1183
1921
|
"required": true,
|
|
1184
1922
|
"default": null,
|
|
1185
|
-
"description": "The
|
|
1923
|
+
"description": "The stack — `Toast` and `Announcement` elements, each routed to a live\nwrapper by its `politeness`, or by a Toast's variant when it names none."
|
|
1186
1924
|
},
|
|
1187
1925
|
{
|
|
1188
1926
|
"name": "className",
|
|
@@ -1190,114 +1928,149 @@
|
|
|
1190
1928
|
"required": false,
|
|
1191
1929
|
"default": null,
|
|
1192
1930
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
"name": "ref",
|
|
1934
|
+
"type": "Ref<HTMLDivElement>",
|
|
1935
|
+
"required": false,
|
|
1936
|
+
"default": null,
|
|
1937
|
+
"description": "Forwarded ref to the region element."
|
|
1193
1938
|
}
|
|
1194
1939
|
],
|
|
1195
1940
|
"slots": [],
|
|
1196
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
1941
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# ToastRegion\n\nThe 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.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `placement` | `\"top-start\" \\| \"top-end\" \\| \"bottom-start\" \\| \"bottom-end\"` | bottom-end | no | Corner the stack occupies. |\n| `aria-label` | `string` | Notifications | no | Accessible name for the region landmark. |\n| `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. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the region element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Moves into the stacked toasts' controls in DOM order; the region itself is not focusable. |\n\nIs 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.\n\n## Theming\n\nThis component has no `--psi-toast-region-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1197
1942
|
},
|
|
1198
1943
|
{
|
|
1199
|
-
"name": "
|
|
1200
|
-
"description": "
|
|
1944
|
+
"name": "ToastProvider",
|
|
1945
|
+
"description": "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`.",
|
|
1201
1946
|
"props": [
|
|
1202
1947
|
{
|
|
1203
|
-
"name": "
|
|
1204
|
-
"type": "
|
|
1948
|
+
"name": "limit",
|
|
1949
|
+
"type": "number",
|
|
1205
1950
|
"required": false,
|
|
1206
|
-
"default":
|
|
1207
|
-
"description": "
|
|
1951
|
+
"default": 3,
|
|
1952
|
+
"description": "Max simultaneous toasts; the oldest is evicted first."
|
|
1208
1953
|
},
|
|
1209
1954
|
{
|
|
1210
|
-
"name": "
|
|
1211
|
-
"type": "
|
|
1955
|
+
"name": "duration",
|
|
1956
|
+
"type": "number",
|
|
1212
1957
|
"required": false,
|
|
1213
|
-
"default":
|
|
1214
|
-
"description": "
|
|
1958
|
+
"default": 5000,
|
|
1959
|
+
"description": "Auto-dismiss for toasts with no action, in ms."
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
"name": "actionDuration",
|
|
1963
|
+
"type": "number",
|
|
1964
|
+
"required": false,
|
|
1965
|
+
"default": 10000,
|
|
1966
|
+
"description": "Auto-dismiss for toasts carrying an action, in ms. An affordance that\nvanishes before it can be reached is not an affordance."
|
|
1967
|
+
},
|
|
1968
|
+
{
|
|
1969
|
+
"name": "placement",
|
|
1970
|
+
"type": "\"top-start\" | \"top-end\" | \"bottom-start\" | \"bottom-end\"",
|
|
1971
|
+
"required": false,
|
|
1972
|
+
"default": "bottom-end",
|
|
1973
|
+
"description": "Corner the stack occupies."
|
|
1215
1974
|
},
|
|
1216
1975
|
{
|
|
1217
1976
|
"name": "children",
|
|
1218
1977
|
"type": "ReactNode",
|
|
1219
|
-
"required":
|
|
1978
|
+
"required": true,
|
|
1220
1979
|
"default": null,
|
|
1221
|
-
"description": "
|
|
1980
|
+
"description": "The subtree that may call `useToast()`. The region is rendered alongside it."
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
"name": "aria-label",
|
|
1984
|
+
"type": "string",
|
|
1985
|
+
"required": false,
|
|
1986
|
+
"default": "\"Notifications\"",
|
|
1987
|
+
"description": "Accessible name for the region landmark, forwarded to ToastRegion."
|
|
1222
1988
|
},
|
|
1223
1989
|
{
|
|
1224
1990
|
"name": "className",
|
|
1225
1991
|
"type": "string",
|
|
1226
1992
|
"required": false,
|
|
1227
1993
|
"default": null,
|
|
1228
|
-
"description": "
|
|
1994
|
+
"description": "Forwarded to the region element."
|
|
1995
|
+
},
|
|
1996
|
+
{
|
|
1997
|
+
"name": "ref",
|
|
1998
|
+
"type": "Ref<HTMLDivElement>",
|
|
1999
|
+
"required": false,
|
|
2000
|
+
"default": null,
|
|
2001
|
+
"description": "Forwarded ref to the region element."
|
|
1229
2002
|
}
|
|
1230
2003
|
],
|
|
1231
2004
|
"slots": [],
|
|
1232
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
2005
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# ToastProvider\n\nOwns 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`.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `limit` | `number` | 3 | no | Max simultaneous toasts; the oldest is evicted first. |\n| `duration` | `number` | 5000 | no | Auto-dismiss for toasts with no action, in ms. |\n| `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. |\n| `placement` | `\"top-start\" \\| \"top-end\" \\| \"bottom-start\" \\| \"bottom-end\"` | bottom-end | no | Corner the stack occupies. |\n| `children` | `ReactNode` | — | yes | The subtree that may call `useToast()`. The region is rendered alongside it. |\n| `aria-label` | `string` | \"Notifications\" | no | Accessible name for the region landmark, forwarded to ToastRegion. |\n| `className` | `string` | — | no | Forwarded to the region element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the region element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focus entering the region pauses every auto-dismiss timer; leaving resumes them. |\n\nOwns 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.\n\n## Theming\n\nThis component has no `--psi-toast-provider-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1233
2006
|
},
|
|
1234
2007
|
{
|
|
1235
|
-
"name": "
|
|
1236
|
-
"description": "
|
|
2008
|
+
"name": "Announcement",
|
|
2009
|
+
"description": "Text for a screen reader only, spoken through ToastRegion (D83). **It is not a live region.** It renders no `role` and no `aria-live`, and outside a ToastRegion it announces nothing — the region's two persistent wrappers are the only live regions, and an Announcement is content for one of them. Do not use it as a second announcer. Use it where an event must be spoken and there is nothing to show: a state change of the object on screen, or field errors the form already displays. `politeness` is not read here. ToastRegion reads it off the element to pick a wrapper, the same way it reads a Toast's. The usual way to render one is `useToast().announce(message)` (D91), which puts it in ToastProvider's region and gives it a lifetime: it leaves one second (the dwell) after it was added, and a later announcement does not remove it early — removing a node just after it was added can cut its speech, and the region's non-atomic wrappers (D90) speak each added node on its own. It never counts against the provider's toast `limit`. Rendered by hand, it is controlled, like Toast: it holds no state and never removes itself, so follow the same rule — remove it after a one-second dwell, not before. To announce the same text again, remove it and render it again with a new `key` — a live region speaks changes, and an unchanged node is not one. It takes no `className`: the one thing a class could do to it is make it visible, and then it is a Toast.",
|
|
1237
2010
|
"props": [
|
|
1238
2011
|
{
|
|
1239
|
-
"name": "
|
|
1240
|
-
"type": "
|
|
2012
|
+
"name": "politeness",
|
|
2013
|
+
"type": "\"polite\" | \"assertive\"",
|
|
1241
2014
|
"required": false,
|
|
1242
|
-
"default":
|
|
1243
|
-
"description": "
|
|
2015
|
+
"default": "\"polite\"",
|
|
2016
|
+
"description": "Which of ToastRegion's two live wrappers speaks it."
|
|
1244
2017
|
},
|
|
1245
2018
|
{
|
|
1246
2019
|
"name": "children",
|
|
1247
2020
|
"type": "ReactNode",
|
|
1248
|
-
"required":
|
|
1249
|
-
"default": null,
|
|
1250
|
-
"description": "Cell content."
|
|
1251
|
-
},
|
|
1252
|
-
{
|
|
1253
|
-
"name": "className",
|
|
1254
|
-
"type": "string",
|
|
1255
|
-
"required": false,
|
|
2021
|
+
"required": true,
|
|
1256
2022
|
"default": null,
|
|
1257
|
-
"description": "
|
|
2023
|
+
"description": "The text to announce."
|
|
1258
2024
|
}
|
|
1259
2025
|
],
|
|
1260
2026
|
"slots": [],
|
|
1261
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
2027
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Announcement\n\nText for a screen reader only, spoken through ToastRegion (D83). **It is not a live region.** It renders no `role` and no `aria-live`, and outside a ToastRegion it announces nothing — the region's two persistent wrappers are the only live regions, and an Announcement is content for one of them. Do not use it as a second announcer. Use it where an event must be spoken and there is nothing to show: a state change of the object on screen, or field errors the form already displays. `politeness` is not read here. ToastRegion reads it off the element to pick a wrapper, the same way it reads a Toast's. The usual way to render one is `useToast().announce(message)` (D91), which puts it in ToastProvider's region and gives it a lifetime: it leaves one second (the dwell) after it was added, and a later announcement does not remove it early — removing a node just after it was added can cut its speech, and the region's non-atomic wrappers (D90) speak each added node on its own. It never counts against the provider's toast `limit`. Rendered by hand, it is controlled, like Toast: it holds no state and never removes itself, so follow the same rule — remove it after a one-second dwell, not before. To announce the same text again, remove it and render it again with a new `key` — a live region speaks changes, and an unchanged node is not one. It takes no `className`: the one thing a class could do to it is make it visible, and then it is a Toast.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `politeness` | `\"polite\" \\| \"assertive\"` | \"polite\" | no | Which of ToastRegion's two live wrappers speaks it. |\n| `children` | `ReactNode` | — | yes | The text to announce. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n\n\nNot a live region: it renders no role and no aria-live, and outside a ToastRegion it announces nothing. It is visually hidden text that ToastRegion places in its polite or assertive wrapper, by the politeness prop (default polite) — for an event that must be spoken and has nothing to show. Keep one ToastRegion as the application's only announcer and put every Announcement inside it; never use an Announcement as a second one. Rendered by useToast().announce() (D91), it gets a lifetime: it leaves after a one-second dwell, a later announcement does not remove it early, and it never counts against the toast limit. Rendered by hand it is controlled like Toast and never removes itself — remove it after the same one-second dwell, not before. To announce the same text again, remove it and render it again with a new key — a live region speaks changes, and an unchanged node is not one. Not focusable, and it takes no space in the toast stack.\n\n## Theming\n\nThis component has no `--psi-announcement-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1262
2028
|
},
|
|
1263
2029
|
{
|
|
1264
|
-
"name": "
|
|
1265
|
-
"description": "
|
|
2030
|
+
"name": "Banner",
|
|
2031
|
+
"description": "A full-width, page-level status message (D86). It is not a live region: it renders no `role` and no `aria-live`, so mounting one announces nothing, and announcing stays the application's — route the same event through a `ToastRegion` or an `Announcement` if it must be spoken. Controlled and presentational: it holds no state and never removes itself; `onDismiss` reports and the owner disposes. A tinted strip with no radius and no border, it is meant to span the page edge to edge, above the content. For the same message inside content, use `InlineAlert`.",
|
|
1266
2032
|
"props": [
|
|
1267
2033
|
{
|
|
1268
|
-
"name": "
|
|
1269
|
-
"type": "
|
|
1270
|
-
"required":
|
|
2034
|
+
"name": "variant",
|
|
2035
|
+
"type": "\"neutral\" | \"success\" | \"warning\" | \"danger\"",
|
|
2036
|
+
"required": false,
|
|
2037
|
+
"default": "neutral",
|
|
2038
|
+
"description": "Status variant."
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
"name": "title",
|
|
2042
|
+
"type": "ReactNode",
|
|
2043
|
+
"required": false,
|
|
1271
2044
|
"default": null,
|
|
1272
|
-
"description": "
|
|
2045
|
+
"description": "A short heading, rendered before the body in the medium weight."
|
|
1273
2046
|
},
|
|
1274
2047
|
{
|
|
1275
|
-
"name": "
|
|
1276
|
-
"type": "
|
|
2048
|
+
"name": "children",
|
|
2049
|
+
"type": "ReactNode",
|
|
1277
2050
|
"required": true,
|
|
1278
2051
|
"default": null,
|
|
1279
|
-
"description": "
|
|
2052
|
+
"description": "The message."
|
|
1280
2053
|
},
|
|
1281
2054
|
{
|
|
1282
|
-
"name": "
|
|
1283
|
-
"type": "
|
|
1284
|
-
"required":
|
|
2055
|
+
"name": "action",
|
|
2056
|
+
"type": "ReactNode",
|
|
2057
|
+
"required": false,
|
|
1285
2058
|
"default": null,
|
|
1286
|
-
"description": "
|
|
2059
|
+
"description": "Trailing affordance — a ghost Button (Review, Retry, Learn more)."
|
|
1287
2060
|
},
|
|
1288
2061
|
{
|
|
1289
|
-
"name": "
|
|
1290
|
-
"type": "
|
|
2062
|
+
"name": "onDismiss",
|
|
2063
|
+
"type": "(() => void)",
|
|
1291
2064
|
"required": false,
|
|
1292
|
-
"default":
|
|
1293
|
-
"description": "
|
|
2065
|
+
"default": null,
|
|
2066
|
+
"description": "When provided, renders the dismiss button and calls this. Banner does not\nremove itself; the owner disposes."
|
|
1294
2067
|
},
|
|
1295
2068
|
{
|
|
1296
|
-
"name": "
|
|
1297
|
-
"type": "string",
|
|
2069
|
+
"name": "statusLabel",
|
|
2070
|
+
"type": "string | null",
|
|
1298
2071
|
"required": false,
|
|
1299
|
-
"default":
|
|
1300
|
-
"description": "
|
|
2072
|
+
"default": null,
|
|
2073
|
+
"description": "The visually hidden status word (D83). A string replaces the default\n(\"Success:\", \"Warning:\", \"Error:\") — pass the translated word; `null`\ndrops it. Unset keeps the default, and `neutral` has none."
|
|
1301
2074
|
},
|
|
1302
2075
|
{
|
|
1303
2076
|
"name": "className",
|
|
@@ -1305,14 +2078,47 @@
|
|
|
1305
2078
|
"required": false,
|
|
1306
2079
|
"default": null,
|
|
1307
2080
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
"name": "ref",
|
|
2084
|
+
"type": "Ref<HTMLDivElement>",
|
|
2085
|
+
"required": false,
|
|
2086
|
+
"default": null,
|
|
2087
|
+
"description": "Forwarded ref to the underlying `<div>`."
|
|
1308
2088
|
}
|
|
1309
2089
|
],
|
|
1310
|
-
"slots": [
|
|
1311
|
-
|
|
2090
|
+
"slots": [
|
|
2091
|
+
{
|
|
2092
|
+
"name": "title",
|
|
2093
|
+
"accepts": {},
|
|
2094
|
+
"cardinality": "0..1",
|
|
2095
|
+
"order": 1
|
|
2096
|
+
},
|
|
2097
|
+
{
|
|
2098
|
+
"name": "body",
|
|
2099
|
+
"accepts": {},
|
|
2100
|
+
"cardinality": "1..*",
|
|
2101
|
+
"order": 2
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
"name": "action",
|
|
2105
|
+
"accepts": {
|
|
2106
|
+
"components": [
|
|
2107
|
+
"Button"
|
|
2108
|
+
],
|
|
2109
|
+
"contracts": [
|
|
2110
|
+
"inline-content"
|
|
2111
|
+
]
|
|
2112
|
+
},
|
|
2113
|
+
"cardinality": "0..1",
|
|
2114
|
+
"order": 3
|
|
2115
|
+
}
|
|
2116
|
+
],
|
|
2117
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Banner\n\nA full-width, page-level status message (D86). It is not a live region: it renders no `role` and no `aria-live`, so mounting one announces nothing, and announcing stays the application's — route the same event through a `ToastRegion` or an `Announcement` if it must be spoken. Controlled and presentational: it holds no state and never removes itself; `onDismiss` reports and the owner disposes. A tinted strip with no radius and no border, it is meant to span the page edge to edge, above the content. For the same message inside content, use `InlineAlert`.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"neutral\" \\| \"success\" \\| \"warning\" \\| \"danger\"` | neutral | no | Status variant. |\n| `title` | `ReactNode` | — | no | A short heading, rendered before the body in the medium weight. |\n| `children` | `ReactNode` | — | yes | The message. |\n| `action` | `ReactNode` | — | no | Trailing affordance — a ghost Button (Review, Retry, Learn more). |\n| `onDismiss` | `(() => void)` | — | no | When provided, renders the dismiss button and calls this. Banner does not remove itself; the owner disposes. |\n| `statusLabel` | `string \\| null` | — | no | The visually hidden status word (D83). A string replaces the default (\"Success:\", \"Warning:\", \"Error:\") — pass the translated word; `null` drops it. Unset keeps the default, and `neutral` has none. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the underlying `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Reaches the action and the dismiss button, in that order, when present. The banner itself is not focusable. |\n| Enter / Space | Activates the focused action or the dismiss button. |\n\nNot a live region: it renders no role and no aria-live, so mounting a Banner announces nothing; announcing stays the application's (route the same event through a ToastRegion or an Announcement if it must be spoken). Presentational and controlled (D86): onDismiss reports and the owner disposes; Banner never removes itself. The variant's meaning is carried by a visually hidden status word (\"Success:\", \"Warning:\", \"Error:\"), never by colour and icon shape alone; the icon is aria-hidden. neutral has no status and gets no word. statusLabel replaces that word with a translated one, or drops it with null (D83). The dismiss button is labelled \"Dismiss\". Put it first in the page content so it is met before the content it qualifies.\n\n## Theming\n\nOverride `--psi-banner-*` custom properties at any scope.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1312
2118
|
},
|
|
1313
2119
|
{
|
|
1314
|
-
"name": "
|
|
1315
|
-
"description": "
|
|
2120
|
+
"name": "InlineAlert",
|
|
2121
|
+
"description": "Not `role=\"alert\"`, despite its name. It is not a live region: it renders no `role` and no `aria-live`, so mounting one announces nothing, and announcing stays the application's — route the same event through a `ToastRegion` or an `Announcement` if it must be spoken. A status message that sits inside content (D86): above a form, in a panel. A tinted surface with a 1px border in the variant's colour and a radius; it fills the width of its container rather than setting its own. Presentational and stateless, with no dismiss button. For a page-wide strip, use `Banner`.",
|
|
1316
2122
|
"props": [
|
|
1317
2123
|
{
|
|
1318
2124
|
"name": "variant",
|
|
@@ -1321,6 +2127,13 @@
|
|
|
1321
2127
|
"default": "neutral",
|
|
1322
2128
|
"description": "Status variant."
|
|
1323
2129
|
},
|
|
2130
|
+
{
|
|
2131
|
+
"name": "title",
|
|
2132
|
+
"type": "ReactNode",
|
|
2133
|
+
"required": false,
|
|
2134
|
+
"default": null,
|
|
2135
|
+
"description": "A short heading, rendered before the body in the medium weight."
|
|
2136
|
+
},
|
|
1324
2137
|
{
|
|
1325
2138
|
"name": "children",
|
|
1326
2139
|
"type": "ReactNode",
|
|
@@ -1333,14 +2146,14 @@
|
|
|
1333
2146
|
"type": "ReactNode",
|
|
1334
2147
|
"required": false,
|
|
1335
2148
|
"default": null,
|
|
1336
|
-
"description": "Trailing affordance — a ghost Button (
|
|
2149
|
+
"description": "Trailing affordance — a ghost Button (Review, Retry, Learn more)."
|
|
1337
2150
|
},
|
|
1338
2151
|
{
|
|
1339
|
-
"name": "
|
|
1340
|
-
"type": "
|
|
2152
|
+
"name": "statusLabel",
|
|
2153
|
+
"type": "string | null",
|
|
1341
2154
|
"required": false,
|
|
1342
2155
|
"default": null,
|
|
1343
|
-
"description": "
|
|
2156
|
+
"description": "The visually hidden status word (D83). A string replaces the default\n(\"Success:\", \"Warning:\", \"Error:\") — pass the translated word; `null`\ndrops it. Unset keeps the default, and `neutral` has none."
|
|
1344
2157
|
},
|
|
1345
2158
|
{
|
|
1346
2159
|
"name": "className",
|
|
@@ -1358,11 +2171,17 @@
|
|
|
1358
2171
|
}
|
|
1359
2172
|
],
|
|
1360
2173
|
"slots": [
|
|
2174
|
+
{
|
|
2175
|
+
"name": "title",
|
|
2176
|
+
"accepts": {},
|
|
2177
|
+
"cardinality": "0..1",
|
|
2178
|
+
"order": 1
|
|
2179
|
+
},
|
|
1361
2180
|
{
|
|
1362
2181
|
"name": "body",
|
|
1363
2182
|
"accepts": {},
|
|
1364
2183
|
"cardinality": "1..*",
|
|
1365
|
-
"order":
|
|
2184
|
+
"order": 2
|
|
1366
2185
|
},
|
|
1367
2186
|
{
|
|
1368
2187
|
"name": "action",
|
|
@@ -1375,35 +2194,35 @@
|
|
|
1375
2194
|
]
|
|
1376
2195
|
},
|
|
1377
2196
|
"cardinality": "0..1",
|
|
1378
|
-
"order":
|
|
2197
|
+
"order": 3
|
|
1379
2198
|
}
|
|
1380
2199
|
],
|
|
1381
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
2200
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# InlineAlert\n\nNot `role=\"alert\"`, despite its name. It is not a live region: it renders no `role` and no `aria-live`, so mounting one announces nothing, and announcing stays the application's — route the same event through a `ToastRegion` or an `Announcement` if it must be spoken. A status message that sits inside content (D86): above a form, in a panel. A tinted surface with a 1px border in the variant's colour and a radius; it fills the width of its container rather than setting its own. Presentational and stateless, with no dismiss button. For a page-wide strip, use `Banner`.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"neutral\" \\| \"success\" \\| \"warning\" \\| \"danger\"` | neutral | no | Status variant. |\n| `title` | `ReactNode` | — | no | A short heading, rendered before the body in the medium weight. |\n| `children` | `ReactNode` | — | yes | The message. |\n| `action` | `ReactNode` | — | no | Trailing affordance — a ghost Button (Review, Retry, Learn more). |\n| `statusLabel` | `string \\| null` | — | no | The visually hidden status word (D83). A string replaces the default (\"Success:\", \"Warning:\", \"Error:\") — pass the translated word; `null` drops it. Unset keeps the default, and `neutral` has none. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the underlying `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Reaches the action when present. The alert itself is not focusable. |\n| Enter / Space | Activates the focused action. |\n\nNot a live region, and not role=\"alert\" despite its name: it renders no role and no aria-live, so mounting an InlineAlert announces nothing; announcing stays the application's (route the same event through a ToastRegion or an Announcement if it must be spoken). Presentational (D86): it holds no state and has no dismiss button. The variant's meaning is carried by a visually hidden status word (\"Success:\", \"Warning:\", \"Error:\"), never by colour, border and icon shape alone; the icon is aria-hidden. neutral has no status and gets no word. statusLabel replaces that word with a translated one, or drops it with null (D83). Place it in reading order before the content it qualifies, such as above the form it is about.\n\n## Theming\n\nOverride `--psi-inline-alert-*` custom properties at any scope.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1382
2201
|
},
|
|
1383
2202
|
{
|
|
1384
|
-
"name": "
|
|
1385
|
-
"description": "
|
|
2203
|
+
"name": "Skeleton",
|
|
2204
|
+
"description": "A loading placeholder. It is not a live region: it renders no `role` and no `aria-live`, and it is always `aria-hidden=\"true\"`, so assistive tech skips it entirely. Put `aria-busy` on the region the skeleton stands for — that is what tells a screen reader the content is coming. Its width fills the container; size the container, not the skeleton. The pulse is an opacity animation that stops under `prefers-reduced-motion: reduce` (D86).",
|
|
1386
2205
|
"props": [
|
|
1387
2206
|
{
|
|
1388
|
-
"name": "
|
|
1389
|
-
"type": "\"
|
|
2207
|
+
"name": "variant",
|
|
2208
|
+
"type": "\"text\" | \"block\"",
|
|
1390
2209
|
"required": false,
|
|
1391
|
-
"default": "
|
|
1392
|
-
"description": "
|
|
2210
|
+
"default": "text",
|
|
2211
|
+
"description": "`text` renders `lines` stacked bars; `block` one box."
|
|
1393
2212
|
},
|
|
1394
2213
|
{
|
|
1395
|
-
"name": "
|
|
1396
|
-
"type": "
|
|
2214
|
+
"name": "lines",
|
|
2215
|
+
"type": "number",
|
|
1397
2216
|
"required": false,
|
|
1398
|
-
"default":
|
|
1399
|
-
"description": "
|
|
2217
|
+
"default": 1,
|
|
2218
|
+
"description": "Number of text lines; the last of several is shorter. Text only."
|
|
1400
2219
|
},
|
|
1401
2220
|
{
|
|
1402
|
-
"name": "
|
|
1403
|
-
"type": "
|
|
1404
|
-
"required":
|
|
1405
|
-
"default":
|
|
1406
|
-
"description": "
|
|
2221
|
+
"name": "size",
|
|
2222
|
+
"type": "24 | 32 | 40 | 48",
|
|
2223
|
+
"required": false,
|
|
2224
|
+
"default": 40,
|
|
2225
|
+
"description": "Height of a block, from the size scale. Block only."
|
|
1407
2226
|
},
|
|
1408
2227
|
{
|
|
1409
2228
|
"name": "className",
|
|
@@ -1417,54 +2236,61 @@
|
|
|
1417
2236
|
"type": "Ref<HTMLDivElement>",
|
|
1418
2237
|
"required": false,
|
|
1419
2238
|
"default": null,
|
|
1420
|
-
"description": "Forwarded ref to the
|
|
2239
|
+
"description": "Forwarded ref to the underlying `<div>`."
|
|
1421
2240
|
}
|
|
1422
2241
|
],
|
|
1423
2242
|
"slots": [],
|
|
1424
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
2243
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Skeleton\n\nA loading placeholder. It is not a live region: it renders no `role` and no `aria-live`, and it is always `aria-hidden=\"true\"`, so assistive tech skips it entirely. Put `aria-busy` on the region the skeleton stands for — that is what tells a screen reader the content is coming. Its width fills the container; size the container, not the skeleton. The pulse is an opacity animation that stops under `prefers-reduced-motion: reduce` (D86).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"text\" \\| \"block\"` | text | no | `text` renders `lines` stacked bars; `block` one box. |\n| `lines` | `number` | 1 | no | Number of text lines; the last of several is shorter. Text only. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 40 | no | Height of a block, from the size scale. Block only. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the underlying `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n\n\nNot a live region: it renders no role and no aria-live, and it is always aria-hidden=\"true\", so assistive tech skips it entirely. Put aria-busy=\"true\" on the region the skeleton stands for (a table body, a card, a list) and remove it when the content arrives; that, not the skeleton, is what tells a screen reader the content is loading. Not focusable. The pulse is an opacity animation that stops under prefers-reduced-motion: reduce, because zeroing the duration of an infinite alternating animation flickers instead of stopping (D86).\n\n## Theming\n\nOverride `--psi-skeleton-*` custom properties at any scope.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1425
2244
|
},
|
|
1426
2245
|
{
|
|
1427
|
-
"name": "
|
|
1428
|
-
"description": "
|
|
2246
|
+
"name": "CopyButton",
|
|
2247
|
+
"description": "Not a live region: it renders no role and no `aria-live`, never changes its label or icon after a copy, and announces nothing. The application speaks the result — route `onCopy` through its announcer or a Toast (D86). Copies `value` to the clipboard when pressed. A `Button` with a leading `IconCopy` and a visible label; the icon only reinforces the word.",
|
|
1429
2248
|
"props": [
|
|
1430
2249
|
{
|
|
1431
|
-
"name": "
|
|
1432
|
-
"type": "
|
|
2250
|
+
"name": "value",
|
|
2251
|
+
"type": "string",
|
|
2252
|
+
"required": true,
|
|
2253
|
+
"default": null,
|
|
2254
|
+
"description": "The text written to the clipboard."
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
"name": "label",
|
|
2258
|
+
"type": "string",
|
|
1433
2259
|
"required": false,
|
|
1434
|
-
"default":
|
|
1435
|
-
"description": "
|
|
2260
|
+
"default": "Copy",
|
|
2261
|
+
"description": "Visible text of the button."
|
|
1436
2262
|
},
|
|
1437
2263
|
{
|
|
1438
|
-
"name": "
|
|
1439
|
-
"type": "
|
|
2264
|
+
"name": "size",
|
|
2265
|
+
"type": "24 | 32 | 40 | 48",
|
|
1440
2266
|
"required": false,
|
|
1441
|
-
"default":
|
|
1442
|
-
"description": "
|
|
2267
|
+
"default": 32,
|
|
2268
|
+
"description": "Button height in px."
|
|
1443
2269
|
},
|
|
1444
2270
|
{
|
|
1445
|
-
"name": "
|
|
1446
|
-
"type": "
|
|
2271
|
+
"name": "variant",
|
|
2272
|
+
"type": "\"accent\" | \"accent-subtle\" | \"neutral\" | \"neutral-subtle\" | \"ghost\" | \"danger\" | \"danger-subtle\" | \"outline\"",
|
|
1447
2273
|
"required": false,
|
|
1448
|
-
"default":
|
|
1449
|
-
"description": "
|
|
2274
|
+
"default": "ghost",
|
|
2275
|
+
"description": "Button variant."
|
|
1450
2276
|
},
|
|
1451
2277
|
{
|
|
1452
|
-
"name": "
|
|
1453
|
-
"type": "
|
|
2278
|
+
"name": "onCopy",
|
|
2279
|
+
"type": "((result: \"copied\" | \"failed\") => void)",
|
|
1454
2280
|
"required": false,
|
|
1455
|
-
"default":
|
|
1456
|
-
"description": "
|
|
2281
|
+
"default": null,
|
|
2282
|
+
"description": "Called with the outcome once the clipboard write settles. `\"failed\"` covers\na rejected write and an environment with no clipboard API."
|
|
1457
2283
|
},
|
|
1458
2284
|
{
|
|
1459
|
-
"name": "
|
|
1460
|
-
"type": "
|
|
1461
|
-
"required":
|
|
2285
|
+
"name": "className",
|
|
2286
|
+
"type": "string",
|
|
2287
|
+
"required": false,
|
|
1462
2288
|
"default": null,
|
|
1463
|
-
"description": "
|
|
2289
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1464
2290
|
}
|
|
1465
2291
|
],
|
|
1466
2292
|
"slots": [],
|
|
1467
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n#
|
|
2293
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# CopyButton\n\nNot a live region: it renders no role and no `aria-live`, never changes its label or icon after a copy, and announces nothing. The application speaks the result — route `onCopy` through its announcer or a Toast (D86). Copies `value` to the clipboard when pressed. A `Button` with a leading `IconCopy` and a visible label; the icon only reinforces the word.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | The text written to the clipboard. |\n| `label` | `string` | Copy | no | Visible text of the button. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Button height in px. |\n| `variant` | `\"accent\" \\| \"accent-subtle\" \\| \"neutral\" \\| \"neutral-subtle\" \\| \"ghost\" \\| \"danger\" \\| \"danger-subtle\" \\| \"outline\"` | ghost | no | Button variant. |\n| `onCopy` | `((result: \"copied\" \\| \"failed\") => void)` | — | no | Called with the outcome once the clipboard write settles. `\"failed\"` covers a rejected write and an environment with no clipboard API. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Copies the value to the clipboard. |\n\nNot a live region: it renders no role and no aria-live, announces nothing, and does not change its label or icon after a copy, so a screen reader hears the same button before and after. The application speaks the result: onCopy reports \"copied\" or \"failed\" (a rejected write, or no clipboard API), and the application routes that through its announcer or a Toast (D86). The label is visible text; the icon is aria-hidden and only reinforces it. Focus stays on the button.\n\n## Theming\n\nThis component has no `--psi-copy-button-*` 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.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1468
2294
|
},
|
|
1469
2295
|
{
|
|
1470
2296
|
"name": "Tabs",
|
|
@@ -1475,7 +2301,7 @@
|
|
|
1475
2301
|
"type": "string",
|
|
1476
2302
|
"required": true,
|
|
1477
2303
|
"default": null,
|
|
1478
|
-
"description": "Controlled selected tab, matched against each `Tab`/`TabPanel` value."
|
|
2304
|
+
"description": "Controlled selected tab, matched against each `Tab`/`TabPanel` value. A\nvalue that matches no tab selects nothing, and the first enabled\ntab holds the list's tab stop (D93); development builds warn."
|
|
1479
2305
|
},
|
|
1480
2306
|
{
|
|
1481
2307
|
"name": "onValueChange",
|
|
@@ -1526,7 +2352,7 @@
|
|
|
1526
2352
|
"order": 1
|
|
1527
2353
|
}
|
|
1528
2354
|
],
|
|
1529
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tabs\n\nTab set root (D67) — holds no selection state of its own, following D50, D53 and D62: `value` and `onValueChange` are required and there is no `defaultValue`. Values are strings rather than indices because an index breaks the moment a tab is inserted, and real tab sets map to ids. `Tab` and `TabPanel` pair by value, so their source order need not match. Activation is automatic: arrow keys move focus and selection together. That is the APG default where panel content is already available, and manual activation is deliberately not offered as a mode — a consumer for whom activating a panel is expensive already controls what the panel renders.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | Controlled selected tab, matched against each `Tab`/`TabPanel` value. |\n| `onValueChange` | `(value: string) => void` | — | yes | Fires with the newly selected value; the consumer flips `value`. |\n| `orientation` | `\"horizontal\" \\| \"vertical\"` | horizontal | no | Axis of the tab list, which also picks the arrow keys. |\n| `children` | `ReactNode` | — | yes | A `TabList` and one `TabPanel` per tab. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the wrapper `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Enters the tab list at its selected tab (one stop for the whole list), then moves on to the active panel. |\n\nControlled-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.\n\n## Theming\n\nOverride `--psi-tabs-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
2355
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tabs\n\nTab set root (D67) — holds no selection state of its own, following D50, D53 and D62: `value` and `onValueChange` are required and there is no `defaultValue`. Values are strings rather than indices because an index breaks the moment a tab is inserted, and real tab sets map to ids. `Tab` and `TabPanel` pair by value, so their source order need not match. Activation is automatic: arrow keys move focus and selection together. That is the APG default where panel content is already available, and manual activation is deliberately not offered as a mode — a consumer for whom activating a panel is expensive already controls what the panel renders.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `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. |\n| `onValueChange` | `(value: string) => void` | — | yes | Fires with the newly selected value; the consumer flips `value`. |\n| `orientation` | `\"horizontal\" \\| \"vertical\"` | horizontal | no | Axis of the tab list, which also picks the arrow keys. |\n| `children` | `ReactNode` | — | yes | A `TabList` and one `TabPanel` per tab. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the wrapper `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| 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). |\n\nControlled-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.\n\n## Theming\n\nOverride `--psi-tabs-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1530
2356
|
},
|
|
1531
2357
|
{
|
|
1532
2358
|
"name": "TabList",
|
|
@@ -1562,7 +2388,7 @@
|
|
|
1562
2388
|
}
|
|
1563
2389
|
],
|
|
1564
2390
|
"slots": [],
|
|
1565
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TabList\n\nThe `role=\"tablist\"` container (D67). Owns the roving tabindex: the whole list is one Tab stop, and the arrow keys move between tabs inside it.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `aria-label` | `string` | — | no | Accessible name for the tab set. Declared here rather than inherited so docgen keeps it in the manifest (D60) — a tablist without a name is announced as an unlabelled group, and the prop would otherwise be invisible to an agent reading the manifest. |\n| `children` | `ReactNode` | — | yes | One `Tab` per view. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the tablist element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Arrow Left / Arrow Right | Horizontal orientation: moves to the previous/next enabled tab, wrapping at both ends. Selection follows focus (automatic activation). |\n| Arrow Up / Arrow Down | Vertical orientation: the same, on the block axis. The cross-axis arrows are ignored rather than swallowed. |\n| Home / End | Jumps to the first or last enabled tab. |\n\nRenders role=\"tablist\" with aria-orientation. Requires an accessible name — pass aria-label, which is promoted onto its own props interface (D60) so the manifest shows it. Owns the roving tabindex: exactly one tab is in the page tab order at a time.\n\n## Theming\n\nThis component has no `--psi-tab-list-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
2391
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TabList\n\nThe `role=\"tablist\"` container (D67). Owns the roving tabindex: the whole list is one Tab stop, and the arrow keys move between tabs inside it.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `aria-label` | `string` | — | no | Accessible name for the tab set. Declared here rather than inherited so docgen keeps it in the manifest (D60) — a tablist without a name is announced as an unlabelled group, and the prop would otherwise be invisible to an agent reading the manifest. |\n| `children` | `ReactNode` | — | yes | One `Tab` per view. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the tablist element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Arrow Left / Arrow Right | Horizontal orientation: moves to the previous/next enabled tab, wrapping at both ends. Selection follows focus (automatic activation). |\n| Arrow Up / Arrow Down | Vertical orientation: the same, on the block axis. The cross-axis arrows are ignored rather than swallowed. |\n| Home / End | Jumps to the first or last enabled tab. |\n\nRenders role=\"tablist\" with aria-orientation. Requires an accessible name — pass aria-label, which is promoted onto its own props interface (D60) so the manifest shows it. Owns the roving tabindex: exactly one tab is in the page tab order at a time.\n\n## Theming\n\nThis component has no `--psi-tab-list-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1566
2392
|
},
|
|
1567
2393
|
{
|
|
1568
2394
|
"name": "Tab",
|
|
@@ -1605,7 +2431,7 @@
|
|
|
1605
2431
|
}
|
|
1606
2432
|
],
|
|
1607
2433
|
"slots": [],
|
|
1608
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tab\n\nOne `role=\"tab\"` (D67). Renders a real `<button>`. `disabled` sets `aria-disabled` rather than the `disabled` attribute, so the tab stays discoverable to assistive tech while being skipped by roving navigation — the same choice MenuItem made in D53.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | Pairs this tab with the `TabPanel` of the same value. |\n| `children` | `ReactNode` | — | yes | Label — keep it to one or two words. |\n| `disabled` | `boolean` | false | no | Skipped by arrow navigation and not selectable, but still announced. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLButtonElement>` | — | no | Forwarded ref to the underlying `<button>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Activates (native button behavior); arrow keys already select on focus. |\n\nRenders a real <button> with role=\"tab\", aria-selected and aria-controls. disabled sets aria-disabled (not the disabled attribute) so the tab stays discoverable to assistive tech while being skipped by roving navigation and refused for selection — the same choice MenuItem made in D53.\n\n## Theming\n\nThis component has no `--psi-tab-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
2434
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tab\n\nOne `role=\"tab\"` (D67). Renders a real `<button>`. `disabled` sets `aria-disabled` rather than the `disabled` attribute, so the tab stays discoverable to assistive tech while being skipped by roving navigation — the same choice MenuItem made in D53.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | Pairs this tab with the `TabPanel` of the same value. |\n| `children` | `ReactNode` | — | yes | Label — keep it to one or two words. |\n| `disabled` | `boolean` | false | no | Skipped by arrow navigation and not selectable, but still announced. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLButtonElement>` | — | no | Forwarded ref to the underlying `<button>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Activates (native button behavior); arrow keys already select on focus. |\n\nRenders a real <button> with role=\"tab\", aria-selected and aria-controls. disabled sets aria-disabled (not the disabled attribute) so the tab stays discoverable to assistive tech while being skipped by roving navigation and refused for selection — the same choice MenuItem made in D53.\n\n## Theming\n\nThis component has no `--psi-tab-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1609
2435
|
},
|
|
1610
2436
|
{
|
|
1611
2437
|
"name": "TabPanel",
|
|
@@ -1641,7 +2467,7 @@
|
|
|
1641
2467
|
}
|
|
1642
2468
|
],
|
|
1643
2469
|
"slots": [],
|
|
1644
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TabPanel\n\nOne `role=\"tabpanel\"` (D67). Every panel renders; unselected ones carry `hidden`. Returning null instead would leave `aria-controls` on every unselected tab pointing at an element that does not exist, and would throw away DOM state — a half-filled form in a panel would lose its values on each tab switch. The cost is a heavier DOM when panels are large; the answer to that is rendering less inside the panel, not unmounting it behind the consumer's back. `tabIndex={0}` because a panel whose content has no focusable element would otherwise be unreachable by keyboard.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | Pairs this panel with the `Tab` of the same value. |\n| `children` | `ReactNode` | — | yes | The view for this tab. Rendered even when unselected, then `hidden`. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the panel element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | The panel itself is a tab stop (tabIndex=0), so a panel whose content has no focusable element is still reachable. |\n\nRenders role=\"tabpanel\" with aria-labelledby pointing back at its tab. Unselected panels stay in the DOM with `hidden`.\n\n## Theming\n\nThis component has no `--psi-tab-panel-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
2470
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TabPanel\n\nOne `role=\"tabpanel\"` (D67). Every panel renders; unselected ones carry `hidden`. Returning null instead would leave `aria-controls` on every unselected tab pointing at an element that does not exist, and would throw away DOM state — a half-filled form in a panel would lose its values on each tab switch. The cost is a heavier DOM when panels are large; the answer to that is rendering less inside the panel, not unmounting it behind the consumer's back. `tabIndex={0}` because a panel whose content has no focusable element would otherwise be unreachable by keyboard.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | Pairs this panel with the `Tab` of the same value. |\n| `children` | `ReactNode` | — | yes | The view for this tab. Rendered even when unselected, then `hidden`. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the panel element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | The panel itself is a tab stop (tabIndex=0), so a panel whose content has no focusable element is still reachable. |\n\nRenders role=\"tabpanel\" with aria-labelledby pointing back at its tab. Unselected panels stay in the DOM with `hidden`.\n\n## Theming\n\nThis component has no `--psi-tab-panel-*` 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.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- 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.\n- 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).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1645
2471
|
}
|
|
1646
2472
|
],
|
|
1647
2473
|
"tokens": [
|
|
@@ -3194,6 +4020,233 @@
|
|
|
3194
4020
|
"blocked": false,
|
|
3195
4021
|
"preset": "<Toast action={<Button variant=\"ghost\">Undo</Button>} variant=\"success\">[what just happened]</Toast>\n"
|
|
3196
4022
|
},
|
|
4023
|
+
{
|
|
4024
|
+
"id": "announcer",
|
|
4025
|
+
"intent": "Under a ToastProvider, do not render this — call useToast().announce() (D91). For an application with its own ToastRegion and no provider: one region holding visible toasts and speech-only Announcements, each routed by politeness rather than by tone. For a single confirmation toast, use action-feedback.",
|
|
4026
|
+
"match": [
|
|
4027
|
+
"announcer",
|
|
4028
|
+
"live region",
|
|
4029
|
+
"screen reader announcement",
|
|
4030
|
+
"aria-live",
|
|
4031
|
+
"status message",
|
|
4032
|
+
"announce without a toast",
|
|
4033
|
+
"polite and assertive"
|
|
4034
|
+
],
|
|
4035
|
+
"compose": {
|
|
4036
|
+
"component": "ToastRegion",
|
|
4037
|
+
"slots": {
|
|
4038
|
+
"body": [
|
|
4039
|
+
{
|
|
4040
|
+
"component": "Toast",
|
|
4041
|
+
"props": {
|
|
4042
|
+
"variant": "success",
|
|
4043
|
+
"politeness": "{param:outcome-politeness}"
|
|
4044
|
+
},
|
|
4045
|
+
"slots": {
|
|
4046
|
+
"body": [
|
|
4047
|
+
"{content:outcome}"
|
|
4048
|
+
]
|
|
4049
|
+
}
|
|
4050
|
+
},
|
|
4051
|
+
{
|
|
4052
|
+
"component": "Announcement",
|
|
4053
|
+
"props": {
|
|
4054
|
+
"politeness": "polite"
|
|
4055
|
+
},
|
|
4056
|
+
"slots": {
|
|
4057
|
+
"body": [
|
|
4058
|
+
"{content:state-change}"
|
|
4059
|
+
]
|
|
4060
|
+
}
|
|
4061
|
+
},
|
|
4062
|
+
{
|
|
4063
|
+
"component": "Announcement",
|
|
4064
|
+
"props": {
|
|
4065
|
+
"politeness": "assertive"
|
|
4066
|
+
},
|
|
4067
|
+
"slots": {
|
|
4068
|
+
"body": [
|
|
4069
|
+
"{content:field-errors}"
|
|
4070
|
+
]
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
]
|
|
4074
|
+
}
|
|
4075
|
+
},
|
|
4076
|
+
"parameters": [
|
|
4077
|
+
{
|
|
4078
|
+
"key": "outcome-politeness",
|
|
4079
|
+
"ask": "Should the outcome of the user's own action interrupt, success included?",
|
|
4080
|
+
"options": [
|
|
4081
|
+
"assertive",
|
|
4082
|
+
"polite"
|
|
4083
|
+
],
|
|
4084
|
+
"default": "assertive"
|
|
4085
|
+
}
|
|
4086
|
+
],
|
|
4087
|
+
"content": {
|
|
4088
|
+
"outcome": "[the outcome of the action the user just took]",
|
|
4089
|
+
"state-change": "[a change to the object on screen, spoken and not shown]",
|
|
4090
|
+
"field-errors": "[how many fields need attention, spoken after a failed submission]"
|
|
4091
|
+
},
|
|
4092
|
+
"gaps": [],
|
|
4093
|
+
"blocked": false,
|
|
4094
|
+
"preset": "<ToastRegion>\n <Toast politeness=\"assertive\" variant=\"success\">[the outcome of the action the user just took]</Toast>\n <Announcement politeness=\"polite\">[a change to the object on screen, spoken and not shown]</Announcement>\n <Announcement politeness=\"assertive\">[how many fields need attention, spoken after a failed submission]</Announcement>\n</ToastRegion>\n"
|
|
4095
|
+
},
|
|
4096
|
+
{
|
|
4097
|
+
"id": "app-shell",
|
|
4098
|
+
"intent": "An application frame: a skip link, a full-width header with a Menu toggle that names the sidebar (aria-expanded and aria-controls — wire both and sidebarOpen to one piece of state), a dark sub-themed sidebar holding a NavTree, and a main region that scrolls on its own so nothing hides under the header. A closed sidebar is hidden, not narrowed. A desktop frame, not a responsive drawer. Put a link of your own router in each NavItem instead of href, and aria-current=\"page\" on the current one",
|
|
4099
|
+
"match": [
|
|
4100
|
+
"app shell",
|
|
4101
|
+
"application shell",
|
|
4102
|
+
"sidebar layout",
|
|
4103
|
+
"admin layout",
|
|
4104
|
+
"side navigation"
|
|
4105
|
+
],
|
|
4106
|
+
"compose": {
|
|
4107
|
+
"component": "AppShell",
|
|
4108
|
+
"props": {
|
|
4109
|
+
"sidebarOpen": true,
|
|
4110
|
+
"sidebarTheme": "dark"
|
|
4111
|
+
},
|
|
4112
|
+
"slots": {
|
|
4113
|
+
"skipLink": [
|
|
4114
|
+
{
|
|
4115
|
+
"component": "SkipLink",
|
|
4116
|
+
"props": {
|
|
4117
|
+
"href": "#main"
|
|
4118
|
+
},
|
|
4119
|
+
"content": "skip-label"
|
|
4120
|
+
}
|
|
4121
|
+
],
|
|
4122
|
+
"header": [
|
|
4123
|
+
{
|
|
4124
|
+
"component": "NavBar",
|
|
4125
|
+
"props": {
|
|
4126
|
+
"fluid": true
|
|
4127
|
+
},
|
|
4128
|
+
"slots": {
|
|
4129
|
+
"brand": [
|
|
4130
|
+
{
|
|
4131
|
+
"component": "Button",
|
|
4132
|
+
"props": {
|
|
4133
|
+
"variant": "ghost",
|
|
4134
|
+
"size": 32,
|
|
4135
|
+
"aria-expanded": true,
|
|
4136
|
+
"aria-controls": "sidebar"
|
|
4137
|
+
},
|
|
4138
|
+
"content": "menu-label"
|
|
4139
|
+
},
|
|
4140
|
+
"{content:brand}"
|
|
4141
|
+
],
|
|
4142
|
+
"actions": [
|
|
4143
|
+
{
|
|
4144
|
+
"component": "Button",
|
|
4145
|
+
"props": {
|
|
4146
|
+
"variant": "neutral",
|
|
4147
|
+
"size": 32
|
|
4148
|
+
},
|
|
4149
|
+
"content": "account-label"
|
|
4150
|
+
}
|
|
4151
|
+
]
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
],
|
|
4155
|
+
"sidebar": [
|
|
4156
|
+
{
|
|
4157
|
+
"component": "NavTree",
|
|
4158
|
+
"props": {
|
|
4159
|
+
"aria-label": "{content:nav-label}"
|
|
4160
|
+
},
|
|
4161
|
+
"slots": {
|
|
4162
|
+
"body": [
|
|
4163
|
+
{
|
|
4164
|
+
"component": "NavItem",
|
|
4165
|
+
"props": {
|
|
4166
|
+
"href": "#overview",
|
|
4167
|
+
"current": true
|
|
4168
|
+
},
|
|
4169
|
+
"content": "overview-label"
|
|
4170
|
+
},
|
|
4171
|
+
{
|
|
4172
|
+
"component": "NavGroup",
|
|
4173
|
+
"props": {
|
|
4174
|
+
"label": "{content:group-1-label}",
|
|
4175
|
+
"open": true
|
|
4176
|
+
},
|
|
4177
|
+
"slots": {
|
|
4178
|
+
"body": [
|
|
4179
|
+
{
|
|
4180
|
+
"component": "NavItem",
|
|
4181
|
+
"props": {
|
|
4182
|
+
"href": "#group-1-first"
|
|
4183
|
+
},
|
|
4184
|
+
"content": "group-1-first-label"
|
|
4185
|
+
},
|
|
4186
|
+
{
|
|
4187
|
+
"component": "NavItem",
|
|
4188
|
+
"props": {
|
|
4189
|
+
"href": "#group-1-second"
|
|
4190
|
+
},
|
|
4191
|
+
"content": "group-1-second-label"
|
|
4192
|
+
}
|
|
4193
|
+
]
|
|
4194
|
+
}
|
|
4195
|
+
},
|
|
4196
|
+
{
|
|
4197
|
+
"component": "NavGroup",
|
|
4198
|
+
"props": {
|
|
4199
|
+
"label": "{content:group-2-label}",
|
|
4200
|
+
"open": true
|
|
4201
|
+
},
|
|
4202
|
+
"slots": {
|
|
4203
|
+
"body": [
|
|
4204
|
+
{
|
|
4205
|
+
"component": "NavItem",
|
|
4206
|
+
"props": {
|
|
4207
|
+
"href": "#group-2-first"
|
|
4208
|
+
},
|
|
4209
|
+
"content": "group-2-first-label"
|
|
4210
|
+
},
|
|
4211
|
+
{
|
|
4212
|
+
"component": "NavItem",
|
|
4213
|
+
"props": {
|
|
4214
|
+
"href": "#group-2-second"
|
|
4215
|
+
},
|
|
4216
|
+
"content": "group-2-second-label"
|
|
4217
|
+
}
|
|
4218
|
+
]
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
]
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
],
|
|
4225
|
+
"body": [
|
|
4226
|
+
"{content:page}"
|
|
4227
|
+
]
|
|
4228
|
+
}
|
|
4229
|
+
},
|
|
4230
|
+
"parameters": [],
|
|
4231
|
+
"content": {
|
|
4232
|
+
"skip-label": "Skip to content",
|
|
4233
|
+
"menu-label": "Menu",
|
|
4234
|
+
"brand": "[product name]",
|
|
4235
|
+
"account-label": "[account]",
|
|
4236
|
+
"nav-label": "Main",
|
|
4237
|
+
"overview-label": "Overview",
|
|
4238
|
+
"group-1-label": "[group name]",
|
|
4239
|
+
"group-1-first-label": "[page name]",
|
|
4240
|
+
"group-1-second-label": "[page name]",
|
|
4241
|
+
"group-2-label": "[group name]",
|
|
4242
|
+
"group-2-first-label": "[page name]",
|
|
4243
|
+
"group-2-second-label": "[page name]",
|
|
4244
|
+
"page": "[the page; it scrolls on its own]"
|
|
4245
|
+
},
|
|
4246
|
+
"gaps": [],
|
|
4247
|
+
"blocked": false,
|
|
4248
|
+
"preset": "<AppShell\n header={<NavBar\n actions={<Button size={32} variant=\"neutral\">[account]</Button>}\n brand={<>\n <Button aria-controls=\"sidebar\" aria-expanded={true} size={32} variant=\"ghost\">Menu</Button>\n [product name]\n </>}\n fluid={true}\n />}\n sidebar={<NavTree\n aria-label=\"Main\"\n >\n <NavItem current={true} href=\"#overview\">Overview</NavItem>\n <NavGroup\n label=\"[group name]\"\n open={true}\n >\n <NavItem href=\"#group-1-first\">[page name]</NavItem>\n <NavItem href=\"#group-1-second\">[page name]</NavItem>\n </NavGroup>\n <NavGroup\n label=\"[group name]\"\n open={true}\n >\n <NavItem href=\"#group-2-first\">[page name]</NavItem>\n <NavItem href=\"#group-2-second\">[page name]</NavItem>\n </NavGroup>\n </NavTree>}\n sidebarOpen={true}\n sidebarTheme=\"dark\"\n skipLink={<SkipLink href=\"#main\">Skip to content</SkipLink>}\n>\n [the page; it scrolls on its own]\n</AppShell>\n"
|
|
4249
|
+
},
|
|
3197
4250
|
{
|
|
3198
4251
|
"id": "bulk-action-bar",
|
|
3199
4252
|
"intent": "Bar that appears when table rows are selected: selection count, bulk actions, and a clear-selection control",
|
|
@@ -3226,36 +4279,137 @@
|
|
|
3226
4279
|
"content": "export-label"
|
|
3227
4280
|
},
|
|
3228
4281
|
{
|
|
3229
|
-
"component": "Button",
|
|
3230
|
-
"props": {
|
|
3231
|
-
"variant": "danger-subtle"
|
|
3232
|
-
},
|
|
3233
|
-
"content": "delete-label"
|
|
4282
|
+
"component": "Button",
|
|
4283
|
+
"props": {
|
|
4284
|
+
"variant": "danger-subtle"
|
|
4285
|
+
},
|
|
4286
|
+
"content": "delete-label"
|
|
4287
|
+
},
|
|
4288
|
+
{
|
|
4289
|
+
"component": "Button",
|
|
4290
|
+
"props": {
|
|
4291
|
+
"variant": "ghost"
|
|
4292
|
+
},
|
|
4293
|
+
"content": "clear-label"
|
|
4294
|
+
}
|
|
4295
|
+
]
|
|
4296
|
+
}
|
|
4297
|
+
},
|
|
4298
|
+
"parameters": [],
|
|
4299
|
+
"content": {
|
|
4300
|
+
"count-label": "[n] selected",
|
|
4301
|
+
"export-label": "Export",
|
|
4302
|
+
"delete-label": "Delete",
|
|
4303
|
+
"clear-label": "Clear selection"
|
|
4304
|
+
},
|
|
4305
|
+
"gaps": [],
|
|
4306
|
+
"blocked": false,
|
|
4307
|
+
"preset": "<Toolbar\n gap={12}\n>\n <Tag subtle={true} variant=\"accent\">[n] selected</Tag>\n <Button variant=\"neutral\">Export</Button>\n <Button variant=\"danger-subtle\">Delete</Button>\n <Button variant=\"ghost\">Clear selection</Button>\n</Toolbar>\n"
|
|
4308
|
+
},
|
|
4309
|
+
{
|
|
4310
|
+
"id": "copyable-id",
|
|
4311
|
+
"intent": "A labelled identifier with a button that copies it; the button announces nothing, the application speaks the result",
|
|
4312
|
+
"match": [
|
|
4313
|
+
"copy id",
|
|
4314
|
+
"copyable identifier",
|
|
4315
|
+
"copy to clipboard",
|
|
4316
|
+
"reference number",
|
|
4317
|
+
"request id"
|
|
4318
|
+
],
|
|
4319
|
+
"compose": {
|
|
4320
|
+
"component": "DescriptionList",
|
|
4321
|
+
"props": {
|
|
4322
|
+
"layout": "inline"
|
|
4323
|
+
},
|
|
4324
|
+
"slots": {
|
|
4325
|
+
"body": [
|
|
4326
|
+
{
|
|
4327
|
+
"component": "DescriptionItem",
|
|
4328
|
+
"props": {
|
|
4329
|
+
"term": "{content:term}"
|
|
4330
|
+
},
|
|
4331
|
+
"slots": {
|
|
4332
|
+
"body": [
|
|
4333
|
+
"{content:value}",
|
|
4334
|
+
{
|
|
4335
|
+
"component": "CopyButton",
|
|
4336
|
+
"props": {
|
|
4337
|
+
"value": "{content:value}",
|
|
4338
|
+
"size": 24
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
]
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
]
|
|
4345
|
+
}
|
|
4346
|
+
},
|
|
4347
|
+
"parameters": [],
|
|
4348
|
+
"content": {
|
|
4349
|
+
"term": "[identifier name]",
|
|
4350
|
+
"value": "[the identifier — set it in a mono class such as psi-text-mono-14-20-regular]"
|
|
4351
|
+
},
|
|
4352
|
+
"gaps": [],
|
|
4353
|
+
"blocked": false,
|
|
4354
|
+
"preset": "<DescriptionList\n layout=\"inline\"\n>\n <DescriptionItem\n term=\"[identifier name]\"\n >\n [the identifier — set it in a mono class such as psi-text-mono-14-20-regular]\n <CopyButton size={24} value=\"[the identifier — set it in a mono class such as psi-text-mono-14-20-regular]\" />\n </DescriptionItem>\n</DescriptionList>\n"
|
|
4355
|
+
},
|
|
4356
|
+
{
|
|
4357
|
+
"id": "cursor-pagination",
|
|
4358
|
+
"intent": "Page-size selector and previous/next control beneath a list that pages by opaque token — no page count, no page numbers; pair with a TableCaption whose detail states the range and takes focus after a page change",
|
|
4359
|
+
"match": [
|
|
4360
|
+
"cursor pagination",
|
|
4361
|
+
"token pagination",
|
|
4362
|
+
"next page token",
|
|
4363
|
+
"previous next",
|
|
4364
|
+
"load more",
|
|
4365
|
+
"keyset pagination",
|
|
4366
|
+
"page size"
|
|
4367
|
+
],
|
|
4368
|
+
"compose": {
|
|
4369
|
+
"component": "Toolbar",
|
|
4370
|
+
"props": {
|
|
4371
|
+
"gap": 12,
|
|
4372
|
+
"align": "end"
|
|
4373
|
+
},
|
|
4374
|
+
"slots": {
|
|
4375
|
+
"body": [
|
|
4376
|
+
{
|
|
4377
|
+
"component": "Field",
|
|
4378
|
+
"slots": {
|
|
4379
|
+
"label": [
|
|
4380
|
+
"{content:page-size-label}"
|
|
4381
|
+
],
|
|
4382
|
+
"body": [
|
|
4383
|
+
{
|
|
4384
|
+
"component": "Select",
|
|
4385
|
+
"props": {
|
|
4386
|
+
"size": 32
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
]
|
|
4390
|
+
}
|
|
3234
4391
|
},
|
|
3235
4392
|
{
|
|
3236
|
-
"component": "
|
|
4393
|
+
"component": "CursorPagination",
|
|
3237
4394
|
"props": {
|
|
3238
|
-
"
|
|
3239
|
-
|
|
3240
|
-
|
|
4395
|
+
"hasPrevious": true,
|
|
4396
|
+
"hasNext": true
|
|
4397
|
+
}
|
|
3241
4398
|
}
|
|
3242
4399
|
]
|
|
3243
4400
|
}
|
|
3244
4401
|
},
|
|
3245
4402
|
"parameters": [],
|
|
3246
4403
|
"content": {
|
|
3247
|
-
"
|
|
3248
|
-
"export-label": "Export",
|
|
3249
|
-
"delete-label": "Delete",
|
|
3250
|
-
"clear-label": "Clear selection"
|
|
4404
|
+
"page-size-label": "Rows per page"
|
|
3251
4405
|
},
|
|
3252
4406
|
"gaps": [],
|
|
3253
4407
|
"blocked": false,
|
|
3254
|
-
"preset": "<Toolbar\n gap={12}\n>\n <
|
|
4408
|
+
"preset": "<Toolbar\n align=\"end\"\n gap={12}\n>\n <Field\n label=\"Rows per page\"\n >\n <Select size={32} />\n </Field>\n <CursorPagination hasNext={true} hasPrevious={true} />\n</Toolbar>\n"
|
|
3255
4409
|
},
|
|
3256
4410
|
{
|
|
3257
4411
|
"id": "data-table",
|
|
3258
|
-
"intent": "Sortable, selectable table of records with numeric columns and a per-row actions menu — compose the row menu from the row-actions pattern",
|
|
4412
|
+
"intent": "Sortable, selectable table of records with a caption stating the range, numeric columns and a per-row actions menu — compose the row menu from the row-actions pattern",
|
|
3259
4413
|
"match": [
|
|
3260
4414
|
"data table",
|
|
3261
4415
|
"transactions table",
|
|
@@ -3271,6 +4425,13 @@
|
|
|
3271
4425
|
},
|
|
3272
4426
|
"slots": {
|
|
3273
4427
|
"body": [
|
|
4428
|
+
{
|
|
4429
|
+
"component": "TableCaption",
|
|
4430
|
+
"props": {
|
|
4431
|
+
"detail": "{content:range}"
|
|
4432
|
+
},
|
|
4433
|
+
"content": "caption"
|
|
4434
|
+
},
|
|
3274
4435
|
{
|
|
3275
4436
|
"component": "TableHead",
|
|
3276
4437
|
"slots": {
|
|
@@ -3352,6 +4513,8 @@
|
|
|
3352
4513
|
},
|
|
3353
4514
|
"parameters": [],
|
|
3354
4515
|
"content": {
|
|
4516
|
+
"caption": "[what the table lists — its visible name]",
|
|
4517
|
+
"range": "[the rows shown, e.g. Rows 1–50 of 1,204; focus moves here after a page change]",
|
|
3355
4518
|
"date-label": "Date",
|
|
3356
4519
|
"payee-label": "Payee",
|
|
3357
4520
|
"amount-label": "Amount",
|
|
@@ -3363,7 +4526,7 @@
|
|
|
3363
4526
|
},
|
|
3364
4527
|
"gaps": [],
|
|
3365
4528
|
"blocked": false,
|
|
3366
|
-
"preset": "<Table\n selectable={true}\n sortable={true}\n>\n <TableHead>\n <TableRow>\n <TableHeaderCell sortKey=\"date\">Date</TableHeaderCell>\n <TableHeaderCell sortKey=\"payee\">Payee</TableHeaderCell>\n <TableHeaderCell numeric={true} sortKey=\"amount\">Amount</TableHeaderCell>\n <TableHeaderCell>[a visible \"Actions\" header — the column needs an accessible name, so never an empty th]</TableHeaderCell>\n </TableRow>\n </TableHead>\n <TableBody>\n <TableRow\n rowId=\"row-id\"\n >\n <TableCell>[one row per record]</TableCell>\n <TableCell>[the record's payee]</TableCell>\n <TableCell numeric={true}>[numeric columns align right and use tabular figures]</TableCell>\n <TableCell>[the row-actions menu — compose it from the row-actions pattern]</TableCell>\n </TableRow>\n </TableBody>\n</Table>\n"
|
|
4529
|
+
"preset": "<Table\n selectable={true}\n sortable={true}\n>\n <TableCaption detail=\"[the rows shown, e.g. Rows 1–50 of 1,204; focus moves here after a page change]\">[what the table lists — its visible name]</TableCaption>\n <TableHead>\n <TableRow>\n <TableHeaderCell sortKey=\"date\">Date</TableHeaderCell>\n <TableHeaderCell sortKey=\"payee\">Payee</TableHeaderCell>\n <TableHeaderCell numeric={true} sortKey=\"amount\">Amount</TableHeaderCell>\n <TableHeaderCell>[a visible \"Actions\" header — the column needs an accessible name, so never an empty th]</TableHeaderCell>\n </TableRow>\n </TableHead>\n <TableBody>\n <TableRow\n rowId=\"row-id\"\n >\n <TableCell>[one row per record]</TableCell>\n <TableCell>[the record's payee]</TableCell>\n <TableCell numeric={true}>[numeric columns align right and use tabular figures]</TableCell>\n <TableCell>[the row-actions menu — compose it from the row-actions pattern]</TableCell>\n </TableRow>\n </TableBody>\n</Table>\n"
|
|
3367
4530
|
},
|
|
3368
4531
|
{
|
|
3369
4532
|
"id": "date-range-filter",
|
|
@@ -3613,9 +4776,85 @@
|
|
|
3613
4776
|
"blocked": false,
|
|
3614
4777
|
"preset": "<Panel\n padding={24}\n>\n [nothing matches these filters]\n [say whether the dataset is empty or the filters are too narrow]\n <Button variant=\"neutral\">Clear filters</Button>\n</Panel>\n"
|
|
3615
4778
|
},
|
|
4779
|
+
{
|
|
4780
|
+
"id": "filter-form",
|
|
4781
|
+
"intent": "Labelled filters above a list, submitted together with a Find button: a Toolbar rendered as a named form (as=\"form\", so Find and Enter submit — wire onSubmit), aligned to the start so every control sits on one line under its visible Field label and Find drops onto that line. A field's description or error line hangs below its control and moves nothing else; the error line is the Field's own, with its usual aria-describedby wiring — set error on the Field, not beside the row. Every label in the row must be one line: a label that wraps pushes its control off the shared line. Add IconSearch before the Find label. For filters that apply as they change, named without a visible label, use filter-toolbar instead; never name a control by its placeholder alone",
|
|
4782
|
+
"match": [
|
|
4783
|
+
"filter form",
|
|
4784
|
+
"search form",
|
|
4785
|
+
"labelled filters",
|
|
4786
|
+
"filters with labels",
|
|
4787
|
+
"find button",
|
|
4788
|
+
"submit filters"
|
|
4789
|
+
],
|
|
4790
|
+
"compose": {
|
|
4791
|
+
"component": "Toolbar",
|
|
4792
|
+
"props": {
|
|
4793
|
+
"as": "form",
|
|
4794
|
+
"align": "start",
|
|
4795
|
+
"gap": 12,
|
|
4796
|
+
"aria-label": "{content:form-label}"
|
|
4797
|
+
},
|
|
4798
|
+
"slots": {
|
|
4799
|
+
"body": [
|
|
4800
|
+
{
|
|
4801
|
+
"component": "Field",
|
|
4802
|
+
"slots": {
|
|
4803
|
+
"label": [
|
|
4804
|
+
"{content:text-label}"
|
|
4805
|
+
],
|
|
4806
|
+
"body": [
|
|
4807
|
+
{
|
|
4808
|
+
"component": "Input",
|
|
4809
|
+
"props": {
|
|
4810
|
+
"size": 32
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
]
|
|
4814
|
+
}
|
|
4815
|
+
},
|
|
4816
|
+
{
|
|
4817
|
+
"component": "Field",
|
|
4818
|
+
"slots": {
|
|
4819
|
+
"label": [
|
|
4820
|
+
"{content:select-label}"
|
|
4821
|
+
],
|
|
4822
|
+
"body": [
|
|
4823
|
+
{
|
|
4824
|
+
"component": "Select",
|
|
4825
|
+
"props": {
|
|
4826
|
+
"size": 32
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
]
|
|
4830
|
+
}
|
|
4831
|
+
},
|
|
4832
|
+
{
|
|
4833
|
+
"component": "Button",
|
|
4834
|
+
"props": {
|
|
4835
|
+
"type": "submit",
|
|
4836
|
+
"variant": "accent",
|
|
4837
|
+
"size": 32
|
|
4838
|
+
},
|
|
4839
|
+
"content": "find-label"
|
|
4840
|
+
}
|
|
4841
|
+
]
|
|
4842
|
+
}
|
|
4843
|
+
},
|
|
4844
|
+
"parameters": [],
|
|
4845
|
+
"content": {
|
|
4846
|
+
"form-label": "Filters",
|
|
4847
|
+
"text-label": "[field name]",
|
|
4848
|
+
"select-label": "[field name]",
|
|
4849
|
+
"find-label": "Find"
|
|
4850
|
+
},
|
|
4851
|
+
"gaps": [],
|
|
4852
|
+
"blocked": false,
|
|
4853
|
+
"preset": "<Toolbar\n align=\"start\"\n aria-label=\"Filters\"\n as=\"form\"\n gap={12}\n>\n <Field\n label=\"[field name]\"\n >\n <Input size={32} />\n </Field>\n <Field\n label=\"[field name]\"\n >\n <Select size={32} />\n </Field>\n <Button size={32} type=\"submit\" variant=\"accent\">Find</Button>\n</Toolbar>\n"
|
|
4854
|
+
},
|
|
3616
4855
|
{
|
|
3617
4856
|
"id": "filter-toolbar",
|
|
3618
|
-
"intent": "Toolbar of filter controls: search, category select, active-filter tags — controls are named by aria-label/placeholder, not a visible label, so no Field (contrast table-pagination)",
|
|
4857
|
+
"intent": "Toolbar of filter controls that apply as they change: search, category select, active-filter tags — controls are named by aria-label/placeholder, not a visible label, so no Field (contrast table-pagination). For filters with visible labels, submitted together with a Find button, use filter-form",
|
|
3619
4858
|
"match": [
|
|
3620
4859
|
"filter toolbar",
|
|
3621
4860
|
"filter bar",
|
|
@@ -3658,6 +4897,261 @@
|
|
|
3658
4897
|
"blocked": false,
|
|
3659
4898
|
"preset": "<Toolbar>\n <Input aria-label=\"Search transactions\" placeholder=\"Search transactions\" type=\"search\" />\n <Select aria-label=\"Category\" />\n <Tag>[active filter]</Tag>\n</Toolbar>\n"
|
|
3660
4899
|
},
|
|
4900
|
+
{
|
|
4901
|
+
"id": "form-feedback",
|
|
4902
|
+
"intent": "A message above a form after a rejected submission, holding the reason and a copyable identifier; not a live region — the application speaks it",
|
|
4903
|
+
"match": [
|
|
4904
|
+
"form error summary",
|
|
4905
|
+
"submission failed",
|
|
4906
|
+
"rejected form",
|
|
4907
|
+
"error id",
|
|
4908
|
+
"support reference"
|
|
4909
|
+
],
|
|
4910
|
+
"compose": {
|
|
4911
|
+
"component": "InlineAlert",
|
|
4912
|
+
"props": {
|
|
4913
|
+
"variant": "danger"
|
|
4914
|
+
},
|
|
4915
|
+
"slots": {
|
|
4916
|
+
"title": [
|
|
4917
|
+
"{content:title}"
|
|
4918
|
+
],
|
|
4919
|
+
"body": [
|
|
4920
|
+
"{content:message}",
|
|
4921
|
+
{
|
|
4922
|
+
"component": "CopyButton",
|
|
4923
|
+
"props": {
|
|
4924
|
+
"value": "{content:identifier}",
|
|
4925
|
+
"label": "{content:copy-label}"
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
]
|
|
4929
|
+
}
|
|
4930
|
+
},
|
|
4931
|
+
"parameters": [],
|
|
4932
|
+
"content": {
|
|
4933
|
+
"title": "[what was rejected]",
|
|
4934
|
+
"message": "[why, and what to change]",
|
|
4935
|
+
"identifier": "[the request or error id, for support]",
|
|
4936
|
+
"copy-label": "Copy id"
|
|
4937
|
+
},
|
|
4938
|
+
"gaps": [],
|
|
4939
|
+
"blocked": false,
|
|
4940
|
+
"preset": "<InlineAlert\n title=\"[what was rejected]\"\n variant=\"danger\"\n>\n [why, and what to change]\n <CopyButton label=\"Copy id\" value=\"[the request or error id, for support]\" />\n</InlineAlert>\n"
|
|
4941
|
+
},
|
|
4942
|
+
{
|
|
4943
|
+
"id": "loading-table",
|
|
4944
|
+
"intent": "A table whose rows are still loading — Skeleton lines in the cells, aria-busy on the body; the skeletons are hidden from assistive tech, the busy state is what it reads",
|
|
4945
|
+
"match": [
|
|
4946
|
+
"loading table",
|
|
4947
|
+
"table skeleton",
|
|
4948
|
+
"placeholder rows",
|
|
4949
|
+
"loading state",
|
|
4950
|
+
"busy table"
|
|
4951
|
+
],
|
|
4952
|
+
"compose": {
|
|
4953
|
+
"component": "Table",
|
|
4954
|
+
"props": {
|
|
4955
|
+
"aria-label": "{content:table-label}"
|
|
4956
|
+
},
|
|
4957
|
+
"slots": {
|
|
4958
|
+
"body": [
|
|
4959
|
+
{
|
|
4960
|
+
"component": "TableHead",
|
|
4961
|
+
"slots": {
|
|
4962
|
+
"body": [
|
|
4963
|
+
{
|
|
4964
|
+
"component": "TableRow",
|
|
4965
|
+
"slots": {
|
|
4966
|
+
"body": [
|
|
4967
|
+
{
|
|
4968
|
+
"component": "TableHeaderCell",
|
|
4969
|
+
"content": "col-1"
|
|
4970
|
+
},
|
|
4971
|
+
{
|
|
4972
|
+
"component": "TableHeaderCell",
|
|
4973
|
+
"content": "col-2"
|
|
4974
|
+
},
|
|
4975
|
+
{
|
|
4976
|
+
"component": "TableHeaderCell",
|
|
4977
|
+
"content": "col-3"
|
|
4978
|
+
}
|
|
4979
|
+
]
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
]
|
|
4983
|
+
}
|
|
4984
|
+
},
|
|
4985
|
+
{
|
|
4986
|
+
"component": "TableBody",
|
|
4987
|
+
"props": {
|
|
4988
|
+
"aria-busy": "true"
|
|
4989
|
+
},
|
|
4990
|
+
"slots": {
|
|
4991
|
+
"body": [
|
|
4992
|
+
{
|
|
4993
|
+
"component": "TableRow",
|
|
4994
|
+
"slots": {
|
|
4995
|
+
"body": [
|
|
4996
|
+
{
|
|
4997
|
+
"component": "TableCell",
|
|
4998
|
+
"slots": {
|
|
4999
|
+
"body": [
|
|
5000
|
+
{
|
|
5001
|
+
"component": "Skeleton"
|
|
5002
|
+
}
|
|
5003
|
+
]
|
|
5004
|
+
}
|
|
5005
|
+
},
|
|
5006
|
+
{
|
|
5007
|
+
"component": "TableCell",
|
|
5008
|
+
"slots": {
|
|
5009
|
+
"body": [
|
|
5010
|
+
{
|
|
5011
|
+
"component": "Skeleton"
|
|
5012
|
+
}
|
|
5013
|
+
]
|
|
5014
|
+
}
|
|
5015
|
+
},
|
|
5016
|
+
{
|
|
5017
|
+
"component": "TableCell",
|
|
5018
|
+
"slots": {
|
|
5019
|
+
"body": [
|
|
5020
|
+
{
|
|
5021
|
+
"component": "Skeleton"
|
|
5022
|
+
}
|
|
5023
|
+
]
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
]
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
]
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
]
|
|
5033
|
+
}
|
|
5034
|
+
},
|
|
5035
|
+
"parameters": [],
|
|
5036
|
+
"content": {
|
|
5037
|
+
"table-label": "[what the table lists], loading",
|
|
5038
|
+
"col-1": "[first column]",
|
|
5039
|
+
"col-2": "[second column]",
|
|
5040
|
+
"col-3": "[third column]"
|
|
5041
|
+
},
|
|
5042
|
+
"gaps": [],
|
|
5043
|
+
"blocked": false,
|
|
5044
|
+
"preset": "<Table\n aria-label=\"[what the table lists], loading\"\n>\n <TableHead>\n <TableRow>\n <TableHeaderCell>[first column]</TableHeaderCell>\n <TableHeaderCell>[second column]</TableHeaderCell>\n <TableHeaderCell>[third column]</TableHeaderCell>\n </TableRow>\n </TableHead>\n <TableBody\n aria-busy=\"true\"\n >\n <TableRow>\n <TableCell>\n <Skeleton />\n </TableCell>\n <TableCell>\n <Skeleton />\n </TableCell>\n <TableCell>\n <Skeleton />\n </TableCell>\n </TableRow>\n </TableBody>\n</Table>\n"
|
|
5045
|
+
},
|
|
5046
|
+
{
|
|
5047
|
+
"id": "page-banner",
|
|
5048
|
+
"intent": "A full-width, page-level message above the content, with a Refresh action; it is what the page shows, and announcing stays the application's (route the same event through the announcer)",
|
|
5049
|
+
"match": [
|
|
5050
|
+
"page banner",
|
|
5051
|
+
"site-wide message",
|
|
5052
|
+
"stale data",
|
|
5053
|
+
"service notice",
|
|
5054
|
+
"maintenance banner"
|
|
5055
|
+
],
|
|
5056
|
+
"compose": {
|
|
5057
|
+
"component": "Banner",
|
|
5058
|
+
"props": {
|
|
5059
|
+
"variant": "{param:variant}"
|
|
5060
|
+
},
|
|
5061
|
+
"slots": {
|
|
5062
|
+
"title": [
|
|
5063
|
+
"{content:title}"
|
|
5064
|
+
],
|
|
5065
|
+
"body": [
|
|
5066
|
+
"{content:message}"
|
|
5067
|
+
],
|
|
5068
|
+
"action": [
|
|
5069
|
+
{
|
|
5070
|
+
"component": "Button",
|
|
5071
|
+
"props": {
|
|
5072
|
+
"variant": "ghost",
|
|
5073
|
+
"size": 32
|
|
5074
|
+
},
|
|
5075
|
+
"content": "action-label"
|
|
5076
|
+
}
|
|
5077
|
+
]
|
|
5078
|
+
}
|
|
5079
|
+
},
|
|
5080
|
+
"parameters": [
|
|
5081
|
+
{
|
|
5082
|
+
"key": "variant",
|
|
5083
|
+
"ask": "What kind of message is this?",
|
|
5084
|
+
"options": [
|
|
5085
|
+
"neutral",
|
|
5086
|
+
"warning",
|
|
5087
|
+
"danger",
|
|
5088
|
+
"success"
|
|
5089
|
+
],
|
|
5090
|
+
"default": "neutral"
|
|
5091
|
+
}
|
|
5092
|
+
],
|
|
5093
|
+
"content": {
|
|
5094
|
+
"title": "[what changed, in a few words]",
|
|
5095
|
+
"message": "[what it means for the user and what to do]",
|
|
5096
|
+
"action-label": "Refresh"
|
|
5097
|
+
},
|
|
5098
|
+
"gaps": [],
|
|
5099
|
+
"blocked": false,
|
|
5100
|
+
"preset": "<Banner action={<Button size={32} variant=\"ghost\">Refresh</Button>} title=\"[what changed, in a few words]\" variant=\"neutral\">[what it means for the user and what to do]</Banner>\n"
|
|
5101
|
+
},
|
|
5102
|
+
{
|
|
5103
|
+
"id": "required-field",
|
|
5104
|
+
"intent": "A required field that says so in text, not by a glyph alone: a Field with required and requiredText around an Input, with a description",
|
|
5105
|
+
"match": [
|
|
5106
|
+
"required field",
|
|
5107
|
+
"mandatory field",
|
|
5108
|
+
"required in text",
|
|
5109
|
+
"required label",
|
|
5110
|
+
"(Required)"
|
|
5111
|
+
],
|
|
5112
|
+
"compose": {
|
|
5113
|
+
"component": "Field",
|
|
5114
|
+
"props": {
|
|
5115
|
+
"required": true,
|
|
5116
|
+
"requiredText": "{content:required-text}"
|
|
5117
|
+
},
|
|
5118
|
+
"slots": {
|
|
5119
|
+
"label": [
|
|
5120
|
+
"{content:label}"
|
|
5121
|
+
],
|
|
5122
|
+
"body": [
|
|
5123
|
+
{
|
|
5124
|
+
"component": "Input",
|
|
5125
|
+
"props": {
|
|
5126
|
+
"size": "{param:size}"
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
],
|
|
5130
|
+
"description": [
|
|
5131
|
+
"{content:description}"
|
|
5132
|
+
]
|
|
5133
|
+
}
|
|
5134
|
+
},
|
|
5135
|
+
"parameters": [
|
|
5136
|
+
{
|
|
5137
|
+
"key": "size",
|
|
5138
|
+
"ask": "Control size?",
|
|
5139
|
+
"options": [
|
|
5140
|
+
32,
|
|
5141
|
+
40
|
|
5142
|
+
],
|
|
5143
|
+
"default": 32
|
|
5144
|
+
}
|
|
5145
|
+
],
|
|
5146
|
+
"content": {
|
|
5147
|
+
"label": "[field name]",
|
|
5148
|
+
"required-text": "(Required)",
|
|
5149
|
+
"description": "[why it is needed]"
|
|
5150
|
+
},
|
|
5151
|
+
"gaps": [],
|
|
5152
|
+
"blocked": false,
|
|
5153
|
+
"preset": "<Field\n description=\"[why it is needed]\"\n label=\"[field name]\"\n required={true}\n requiredText=\"(Required)\"\n>\n <Input size={32} />\n</Field>\n"
|
|
5154
|
+
},
|
|
3661
5155
|
{
|
|
3662
5156
|
"id": "row-actions",
|
|
3663
5157
|
"intent": "List or table row with an icon-only actions menu, including a destructive item — hand the destructive item's confirmation off to the destructive-confirm pattern",
|
|
@@ -4001,6 +5495,11 @@
|
|
|
4001
5495
|
"tooltip": 1100
|
|
4002
5496
|
}
|
|
4003
5497
|
},
|
|
5498
|
+
"focusRing": {
|
|
5499
|
+
"width": 2,
|
|
5500
|
+
"offset": 2,
|
|
5501
|
+
"offsetInset": -2
|
|
5502
|
+
},
|
|
4004
5503
|
"scopes": {
|
|
4005
5504
|
"space": [
|
|
4006
5505
|
"gap"
|
|
@@ -4063,14 +5562,14 @@
|
|
|
4063
5562
|
"Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.",
|
|
4064
5563
|
"--psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).",
|
|
4065
5564
|
"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.",
|
|
4066
|
-
"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).",
|
|
5565
|
+
"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).",
|
|
4067
5566
|
"Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group."
|
|
4068
5567
|
],
|
|
4069
5568
|
"states": {
|
|
4070
5569
|
"hover": "L - 0.04",
|
|
4071
5570
|
"active": "L - 0.08",
|
|
4072
5571
|
"disabled": "element opacity 0.4 (keeps hue)",
|
|
4073
|
-
"focus": "
|
|
5572
|
+
"focus": "outline: var(--psi-focus-ring-width) solid var(--psi-{component}-focus-ring) on :focus-visible, at --psi-focus-ring-offset (outside) or --psi-focus-ring-offset-inset (inside) — D82"
|
|
4074
5573
|
},
|
|
4075
5574
|
"typographyDefaults": {
|
|
4076
5575
|
"body": "16-24-regular",
|
|
@@ -4802,6 +6301,11 @@
|
|
|
4802
6301
|
"tooltip": 1100
|
|
4803
6302
|
}
|
|
4804
6303
|
},
|
|
6304
|
+
"focusRing": {
|
|
6305
|
+
"width": 2,
|
|
6306
|
+
"offset": 2,
|
|
6307
|
+
"offsetInset": -2
|
|
6308
|
+
},
|
|
4805
6309
|
"scopes": {
|
|
4806
6310
|
"space": [
|
|
4807
6311
|
"gap"
|