@handamade/psi-mcp 0.22.0 → 0.24.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 +145 -13
- package/llms.txt +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @handamade/psi-mcp
|
|
2
2
|
|
|
3
|
-
MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the
|
|
3
|
+
MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the 47 React 19 components, 23 composition patterns (D47) that recipe them together with gap-annotated compose trees, every semantic token with its OKLCH derivation formula resolved across all four themes, and the usage guidance — through two read-only tools. It does not write code.
|
|
4
4
|
|
|
5
5
|
## Connect
|
|
6
6
|
|
package/dist/psi-index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.24.0",
|
|
3
3
|
"themes": [
|
|
4
4
|
"light",
|
|
5
5
|
"dark",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"type": "ReactNode",
|
|
17
17
|
"required": false,
|
|
18
18
|
"default": null,
|
|
19
|
-
"description": "Label content."
|
|
19
|
+
"description": "Label content. A direct icon child is sized by the button, half its\nheight (12/16/20/24), whatever the icon's own `size` (D97). A label in a\ndirect `<span>` is capped by `--psi-button-label-max-width` (default\n`none`) and ends in an ellipsis past it (D98)."
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"name": "variant",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
}
|
|
77
77
|
],
|
|
78
78
|
"slots": [],
|
|
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"
|
|
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. A direct icon child is sized by the button, half its height (12/16/20/24), whatever the icon's own `size` (D97). A label in a direct `<span>` is capped by `--psi-button-label-max-width` (default `none`) and ends in an ellipsis past it (D98). |\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"
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"name": "IconButton",
|
|
@@ -1043,7 +1043,7 @@
|
|
|
1043
1043
|
},
|
|
1044
1044
|
{
|
|
1045
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.",
|
|
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. A child link you mark `aria-current=\"true\"` is the entry the current screen sits under (D96): medium weight and the current colour, with no surface and no bar. `NavItem` itself never sets `true`.",
|
|
1047
1047
|
"props": [
|
|
1048
1048
|
{
|
|
1049
1049
|
"name": "children",
|
|
@@ -1082,7 +1082,7 @@
|
|
|
1082
1082
|
}
|
|
1083
1083
|
],
|
|
1084
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
|
|
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. A child link you mark `aria-current=\"true\"` is the entry the current screen sits under (D96): medium weight and the current colour, with no surface and no bar. `NavItem` itself never sets `true`.\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
1086
|
},
|
|
1087
1087
|
{
|
|
1088
1088
|
"name": "AppShell",
|
|
@@ -1274,12 +1274,19 @@
|
|
|
1274
1274
|
"default": null,
|
|
1275
1275
|
"description": "Accessible name for the menu when there is no visible label."
|
|
1276
1276
|
},
|
|
1277
|
+
{
|
|
1278
|
+
"name": "aria-describedby",
|
|
1279
|
+
"type": "string",
|
|
1280
|
+
"required": false,
|
|
1281
|
+
"default": null,
|
|
1282
|
+
"description": "The id of a `MenuText` (or any element) that describes the menu (D99).\nUnset by default: a description may be spoken every time the menu opens."
|
|
1283
|
+
},
|
|
1277
1284
|
{
|
|
1278
1285
|
"name": "children",
|
|
1279
1286
|
"type": "ReactNode",
|
|
1280
1287
|
"required": true,
|
|
1281
1288
|
"default": null,
|
|
1282
|
-
"description": "MenuItem and
|
|
1289
|
+
"description": "MenuItem, MenuSeparator and MenuText children."
|
|
1283
1290
|
},
|
|
1284
1291
|
{
|
|
1285
1292
|
"name": "className",
|
|
@@ -1312,14 +1319,15 @@
|
|
|
1312
1319
|
"accepts": {
|
|
1313
1320
|
"components": [
|
|
1314
1321
|
"MenuItem",
|
|
1315
|
-
"MenuSeparator"
|
|
1322
|
+
"MenuSeparator",
|
|
1323
|
+
"MenuText"
|
|
1316
1324
|
]
|
|
1317
1325
|
},
|
|
1318
1326
|
"cardinality": "1..*",
|
|
1319
1327
|
"order": 2
|
|
1320
1328
|
}
|
|
1321
1329
|
],
|
|
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
|
|
1330
|
+
"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| `aria-describedby` | `string` | — | no | The id of a `MenuText` (or any element) that describes the menu (D99). Unset by default: a description may be spoken every time the menu opens. |\n| `children` | `ReactNode` | — | yes | MenuItem, MenuSeparator and MenuText 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
1331
|
},
|
|
1324
1332
|
{
|
|
1325
1333
|
"name": "MenuItem",
|
|
@@ -1364,6 +1372,28 @@
|
|
|
1364
1372
|
"slots": [],
|
|
1365
1373
|
"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
1374
|
},
|
|
1375
|
+
{
|
|
1376
|
+
"name": "MenuText",
|
|
1377
|
+
"description": "A line of text in a Menu that is not an action (D99): a build identifier, a signed-in account. It sits inside the `role=\"menu\"` element with no role of its own, so it is in the accessibility tree as a child of the menu; it is never focused, and the arrows, Home, End and typeahead skip it, since Menu's keyboard moves only between `[data-psi-menu-item]` elements. It is never `aria-hidden`: it exists to be read. In a menu's focus mode a screen reader reaches it by browse mode or the VoiceOver cursor; to have it spoken on entering the menu, make it the menu's description.",
|
|
1378
|
+
"props": [
|
|
1379
|
+
{
|
|
1380
|
+
"name": "children",
|
|
1381
|
+
"type": "ReactNode",
|
|
1382
|
+
"required": true,
|
|
1383
|
+
"default": null,
|
|
1384
|
+
"description": "The line's text: information, not an action."
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
"name": "id",
|
|
1388
|
+
"type": "string",
|
|
1389
|
+
"required": false,
|
|
1390
|
+
"default": null,
|
|
1391
|
+
"description": "Give it an id and pass the same id to Menu's `aria-describedby` to make\nthe line the menu's description (D99). Off by default."
|
|
1392
|
+
}
|
|
1393
|
+
],
|
|
1394
|
+
"slots": [],
|
|
1395
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# MenuText\n\nA line of text in a Menu that is not an action (D99): a build identifier, a signed-in account. It sits inside the `role=\"menu\"` element with no role of its own, so it is in the accessibility tree as a child of the menu; it is never focused, and the arrows, Home, End and typeahead skip it, since Menu's keyboard moves only between `[data-psi-menu-item]` elements. It is never `aria-hidden`: it exists to be read. In a menu's focus mode a screen reader reaches it by browse mode or the VoiceOver cursor; to have it spoken on entering the menu, make it the menu's description.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | yes | The line's text: information, not an action. |\n| `id` | `string` | — | no | Give it an id and pass the same id to Menu's `aria-describedby` to make the line the menu's description (D99). Off by default. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n\n\nA line of text that is not an action (D99): inside the role=\"menu\" element with no role of its own, never focused, skipped by the arrows, Home, End and typeahead, and never aria-hidden. In a menu's focus mode a screen reader reaches it by browse mode or the VoiceOver cursor. To have it spoken on entering the menu, give it an id and pass that id to Menu's aria-describedby; that is off by default, because a description may be spoken on every open.\n\n## Theming\n\nThis component has no `--psi-menu-text-*` 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"
|
|
1396
|
+
},
|
|
1367
1397
|
{
|
|
1368
1398
|
"name": "Table",
|
|
1369
1399
|
"description": "Data table on native table semantics. Holds no state: sorting, selection and pagination are the consumer's (D62, extending D50/D53).",
|
|
@@ -2049,7 +2079,7 @@
|
|
|
2049
2079
|
"type": "ReactNode",
|
|
2050
2080
|
"required": true,
|
|
2051
2081
|
"default": null,
|
|
2052
|
-
"description": "The message."
|
|
2082
|
+
"description": "The message: bare text, or one or more `<p>`. A direct `<p>` has no\nblock margin, and consecutive paragraphs are 8px apart (D95)."
|
|
2053
2083
|
},
|
|
2054
2084
|
{
|
|
2055
2085
|
"name": "action",
|
|
@@ -2114,7 +2144,7 @@
|
|
|
2114
2144
|
"order": 3
|
|
2115
2145
|
}
|
|
2116
2146
|
],
|
|
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"
|
|
2147
|
+
"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: bare text, or one or more `<p>`. A direct `<p>` has no block margin, and consecutive paragraphs are 8px apart (D95). |\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"
|
|
2118
2148
|
},
|
|
2119
2149
|
{
|
|
2120
2150
|
"name": "InlineAlert",
|
|
@@ -2139,7 +2169,7 @@
|
|
|
2139
2169
|
"type": "ReactNode",
|
|
2140
2170
|
"required": true,
|
|
2141
2171
|
"default": null,
|
|
2142
|
-
"description": "The message."
|
|
2172
|
+
"description": "The message: bare text, or one or more `<p>`. A direct `<p>` has no\nblock margin, and consecutive paragraphs are 8px apart (D95)."
|
|
2143
2173
|
},
|
|
2144
2174
|
{
|
|
2145
2175
|
"name": "action",
|
|
@@ -2197,7 +2227,7 @@
|
|
|
2197
2227
|
"order": 3
|
|
2198
2228
|
}
|
|
2199
2229
|
],
|
|
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"
|
|
2230
|
+
"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: bare text, or one or more `<p>`. A direct `<p>` has no block margin, and consecutive paragraphs are 8px apart (D95). |\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"
|
|
2201
2231
|
},
|
|
2202
2232
|
{
|
|
2203
2233
|
"name": "Skeleton",
|
|
@@ -3969,6 +3999,67 @@
|
|
|
3969
3999
|
}
|
|
3970
4000
|
],
|
|
3971
4001
|
"patterns": [
|
|
4002
|
+
{
|
|
4003
|
+
"id": "account-menu",
|
|
4004
|
+
"intent": "The signed-in user's menu: account actions, sign out, and a read-only line such as the build, which the keyboard skips",
|
|
4005
|
+
"match": [
|
|
4006
|
+
"account menu",
|
|
4007
|
+
"user menu",
|
|
4008
|
+
"profile menu",
|
|
4009
|
+
"sign out",
|
|
4010
|
+
"build number"
|
|
4011
|
+
],
|
|
4012
|
+
"compose": {
|
|
4013
|
+
"component": "Menu",
|
|
4014
|
+
"props": {
|
|
4015
|
+
"placement": "bottom-end",
|
|
4016
|
+
"aria-label": "{content:menu-label}"
|
|
4017
|
+
},
|
|
4018
|
+
"slots": {
|
|
4019
|
+
"trigger": [
|
|
4020
|
+
{
|
|
4021
|
+
"component": "Button",
|
|
4022
|
+
"props": {
|
|
4023
|
+
"variant": "ghost",
|
|
4024
|
+
"size": 32
|
|
4025
|
+
},
|
|
4026
|
+
"content": "user-name"
|
|
4027
|
+
}
|
|
4028
|
+
],
|
|
4029
|
+
"body": [
|
|
4030
|
+
{
|
|
4031
|
+
"component": "MenuItem",
|
|
4032
|
+
"content": "profile-label"
|
|
4033
|
+
},
|
|
4034
|
+
{
|
|
4035
|
+
"component": "MenuSeparator"
|
|
4036
|
+
},
|
|
4037
|
+
{
|
|
4038
|
+
"component": "MenuItem",
|
|
4039
|
+
"content": "sign-out-label"
|
|
4040
|
+
},
|
|
4041
|
+
{
|
|
4042
|
+
"component": "MenuSeparator"
|
|
4043
|
+
},
|
|
4044
|
+
{
|
|
4045
|
+
"component": "MenuText",
|
|
4046
|
+
"content": "build-line"
|
|
4047
|
+
}
|
|
4048
|
+
]
|
|
4049
|
+
}
|
|
4050
|
+
},
|
|
4051
|
+
"parameters": [],
|
|
4052
|
+
"content": {
|
|
4053
|
+
"menu-label": "Account",
|
|
4054
|
+
"user-name": "[the signed-in user's display name — wrap it in a span and cap it with --psi-button-label-max-width]",
|
|
4055
|
+
"profile-label": "Profile",
|
|
4056
|
+
"sign-out-label": "Sign out",
|
|
4057
|
+
"build-line": "[read-only information, such as the build identifier]"
|
|
4058
|
+
},
|
|
4059
|
+
"gaps": [],
|
|
4060
|
+
"blocked": false,
|
|
4061
|
+
"preset": "<Menu\n aria-label=\"Account\"\n placement=\"bottom-end\"\n trigger={<Button size={32} variant=\"ghost\">[the signed-in user's display name — wrap it in a span and cap it with --psi-button-label-max-width]</Button>}\n>\n <MenuItem>Profile</MenuItem>\n <MenuSeparator />\n <MenuItem>Sign out</MenuItem>\n <MenuSeparator />\n <MenuText>[read-only information, such as the build identifier]</MenuText>\n</Menu>\n"
|
|
4062
|
+
},
|
|
3972
4063
|
{
|
|
3973
4064
|
"id": "action-feedback",
|
|
3974
4065
|
"intent": "Transient confirmation after a completed action, with an optional undo affordance",
|
|
@@ -5659,6 +5750,12 @@
|
|
|
5659
5750
|
12,
|
|
5660
5751
|
16
|
|
5661
5752
|
],
|
|
5753
|
+
"iconSize": [
|
|
5754
|
+
12,
|
|
5755
|
+
16,
|
|
5756
|
+
20,
|
|
5757
|
+
24
|
|
5758
|
+
],
|
|
5662
5759
|
"gap": [
|
|
5663
5760
|
4,
|
|
5664
5761
|
8,
|
|
@@ -5694,11 +5791,13 @@
|
|
|
5694
5791
|
},
|
|
5695
5792
|
"note": "Per-size geometry is data, not CSS literals (D54). Arrays are indexed by sizes[]. Read a value as --psi-control-{size}-{prop} for the label ramp and --psi-control-value-{size}-{prop} for the value ramp; components alias these as --psi-{component}-{size}-{prop}, which is the layer to override. The value ramp is one step tighter than the label ramp because a left-aligned value wants less air than a centred label (D55).",
|
|
5696
5793
|
"iconInset": "A leading icon sits one step closer to the edge than text — an icon is a solid shape with no side bearing (D55). Applied by .size{n}:has(> svg:first-child) on Button. IconButton is height-only: it is square with padding 0, so no padding/gap/font token applies to it.",
|
|
5794
|
+
"iconSize": "A direct svg child of a Button renders at --psi-button-{size}-icon-size, half the button's height (12/16/20/24 at 24/32/40/48), whatever the icon's own size prop: CSS width and height beat the attributes an icon writes (D97). Pass icons without a size. A wrapped icon is not sized, as it gets no inset. IconButton does not bind the ramp; its icon keeps its own size (20 by default).",
|
|
5795
|
+
"labelMaxWidth": "A label wrapped in a direct <span> is capped by --psi-button-label-max-width, default none, and ends in an ellipsis past the cap (D98). Set the token on your own class; it is size-invariant. A bare text label is not capped (wrap it, as a trailing icon already requires). The full text stays the accessible name, and Psi adds no title.",
|
|
5697
5796
|
"iconInsetLimits": "The selector matches on element order, and :first-child ignores text nodes. Two consequences. (1) An icon wrapped in a <span> never matches — fails safe, no inset; set --psi-button-{size}-padding-inline-icon yourself. (2) A TRAILING icon (<Button>Next<IconChevronRight /></Button>) makes the svg the first ELEMENT child, so the rule fires and narrows the start (text) side while the icon keeps full padding — the inverse of the intent. CSS cannot tell the two apart without a DOM signal. Put the icon first, or override --psi-button-{size}-padding-inline-icon to match --psi-button-{size}-padding-inline to disable the inset.",
|
|
5698
5797
|
"radius": "Control radius is size-invariant (D56) — it does NOT ride the size ramp, because border-radius sits on each component's base rule and all four sizes resolve to radius-8. One dial, --psi-control-radius, is the layer to override, aliased per component: --psi-button-radius (Button, and IconButton via the same token), --psi-input-radius, --psi-select-radius, --psi-checkbox-box-radius, and --psi-tooltip-radius. Checkbox and Tooltip cap themselves with min(var(--psi-control-radius), var(--psi-radius-4)) and min(…, var(--psi-radius-6)) respectively so neither over-rounds a small object — but the published radius scale's floor is radius-4, so Checkbox's cap is a no-op for every on-scale value a theme can set; it only bites at an off-scale value like 0px. Tooltip's higher ceiling does track a sharper theme, down to radius-4. Tag and Switch stay --psi-radius-full: pill-ness is component identity, not theme expression. A theme retunes every control's shape by setting --psi-control-radius once. Psi now has two radius dials, not one: --psi-control-radius (here, for interactive controls) and --psi-surface-radius (for elevated surfaces — Dialog, Panel, Menu). --psi-card-radius sits on neither dial, so a theme that retunes controls or surfaces leaves Card at its own default of radius-8."
|
|
5699
5798
|
},
|
|
5700
5799
|
"utilities": {
|
|
5701
|
-
"note": "Every utility class shipped in utilities.css, generated from the same source as the CSS (D79). Scaled utility families set one property each and create no layout context: .psi-gap-* sets gap only, so pair it with display:flex or grid yourself. The bare/fixed utilities (.psi-container, .psi-media-tint, .psi-sr-only) set several. Import @handamade/psi-tokens/utilities.css — it is required, not optional; .psi-container and the reduced-motion duration zeroing live there.",
|
|
5800
|
+
"note": "Every utility class shipped in utilities.css, generated from the same source as the CSS (D79). Scaled utility families set one property each and create no layout context: .psi-gap-* sets gap only, so pair it with display:flex or grid yourself. The bare/fixed utilities (.psi-container, .psi-media-tint, .psi-sr-only) set several. Every utility sits in @layer psi.utilities, above @layer psi.components, so on a component root it beats the component's own value (D94). Import @handamade/psi-tokens/utilities.css — it is required, not optional; .psi-container and the reduced-motion duration zeroing live there.",
|
|
5702
5801
|
"families": [
|
|
5703
5802
|
{
|
|
5704
5803
|
"prefix": "psi-gap-*",
|
|
@@ -5994,6 +6093,26 @@
|
|
|
5994
6093
|
"psi-my-144"
|
|
5995
6094
|
]
|
|
5996
6095
|
},
|
|
6096
|
+
{
|
|
6097
|
+
"prefix": "psi-fg-*",
|
|
6098
|
+
"property": "color",
|
|
6099
|
+
"scale": null,
|
|
6100
|
+
"classes": [
|
|
6101
|
+
"psi-fg-primary",
|
|
6102
|
+
"psi-fg-secondary",
|
|
6103
|
+
"psi-fg-tertiary",
|
|
6104
|
+
"psi-fg-quaternary",
|
|
6105
|
+
"psi-fg-primary-inverted",
|
|
6106
|
+
"psi-fg-static-white",
|
|
6107
|
+
"psi-fg-static-black",
|
|
6108
|
+
"psi-fg-on-accent",
|
|
6109
|
+
"psi-fg-accent",
|
|
6110
|
+
"psi-fg-success",
|
|
6111
|
+
"psi-fg-warning",
|
|
6112
|
+
"psi-fg-danger",
|
|
6113
|
+
"psi-fg-on-inverted"
|
|
6114
|
+
]
|
|
6115
|
+
},
|
|
5997
6116
|
{
|
|
5998
6117
|
"prefix": "psi-text-*",
|
|
5999
6118
|
"property": "font",
|
|
@@ -6070,6 +6189,19 @@
|
|
|
6070
6189
|
"psi-display-32-32-extrabold",
|
|
6071
6190
|
"psi-display-36-64-black",
|
|
6072
6191
|
"psi-display-56-128-black",
|
|
6192
|
+
"psi-fg-accent",
|
|
6193
|
+
"psi-fg-danger",
|
|
6194
|
+
"psi-fg-on-accent",
|
|
6195
|
+
"psi-fg-on-inverted",
|
|
6196
|
+
"psi-fg-primary",
|
|
6197
|
+
"psi-fg-primary-inverted",
|
|
6198
|
+
"psi-fg-quaternary",
|
|
6199
|
+
"psi-fg-secondary",
|
|
6200
|
+
"psi-fg-static-black",
|
|
6201
|
+
"psi-fg-static-white",
|
|
6202
|
+
"psi-fg-success",
|
|
6203
|
+
"psi-fg-tertiary",
|
|
6204
|
+
"psi-fg-warning",
|
|
6073
6205
|
"psi-gap-0",
|
|
6074
6206
|
"psi-gap-12",
|
|
6075
6207
|
"psi-gap-120",
|
package/llms.txt
CHANGED
|
@@ -7,4 +7,4 @@ Ids: component:<Name>, token:<name>, topic:<key>, pattern:<id>; bare names resol
|
|
|
7
7
|
|
|
8
8
|
Connect: https://psi.kurkin.de/mcp (Streamable HTTP) or `npx @handamade/psi-mcp` (stdio).
|
|
9
9
|
Agent context: `npx @handamade/psi-mcp init [--claude]` writes a marker-delimited block into AGENTS.md/CLAUDE.md (D44).
|
|
10
|
-
The index bakes packages/react/dist/{manifest.json,patterns.json} (patterns.json:
|
|
10
|
+
The index bakes packages/react/dist/{manifest.json,patterns.json} (patterns.json: 23 composition patterns with clarifying parameters, blocked/gaps flags, preset JSX) + packages/tokens/dist/{guidance.json,resolved/*.json} at publish; lockstep versioning keeps it truthful (D43).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handamade/psi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "MCP server and agent-context generator for the Psi (Ψ) design system",
|
|
6
6
|
"type": "module",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"tsx": "^4.22.4",
|
|
34
34
|
"typescript": "^5.7.0",
|
|
35
35
|
"vitest": "^3.2.6",
|
|
36
|
-
"@handamade/psi-
|
|
37
|
-
"@handamade/psi-
|
|
36
|
+
"@handamade/psi-tokens": "0.24.0",
|
|
37
|
+
"@handamade/psi-react": "0.24.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"
|