@handamade/psi-mcp 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/psi-index.json +343 -7
- package/dist/store.d.ts +9 -1
- package/dist/store.js +8 -6
- package/llms.txt +2 -2
- 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 50 React 19 components, 24 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/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { PsiIndex } from "./types.js";
|
|
|
2
2
|
export type { PsiIndex, ComponentEntry, TokenEntry, PropDoc, PatternEntry, PatternNode, PatternParameter, } from "./types.js";
|
|
3
3
|
export { buildIndex, GETTING_STARTED } from "./index-builder.js";
|
|
4
4
|
export { createStore } from "./store.js";
|
|
5
|
-
export type { Store, Brief, Detail } from "./store.js";
|
|
5
|
+
export type { Store, Brief, OverviewPatternBrief, SearchBrief, Detail } from "./store.js";
|
|
6
6
|
export { createPsiServer } from "./server.js";
|
|
7
7
|
/** Loads the index baked next to the compiled module at publish time. */
|
|
8
8
|
export declare function loadIndex(): Promise<PsiIndex>;
|
package/dist/psi-index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.26.0",
|
|
3
3
|
"themes": [
|
|
4
4
|
"light",
|
|
5
5
|
"dark",
|
|
@@ -587,7 +587,28 @@
|
|
|
587
587
|
"type": "boolean",
|
|
588
588
|
"required": false,
|
|
589
589
|
"default": true,
|
|
590
|
-
"description": "false = no close button, Esc and backdrop swallowed — footer is the only exit."
|
|
590
|
+
"description": "false = no close button, Esc and backdrop swallowed — footer is the only exit.\n`closeOnEscape` can give Escape back on its own (D101)."
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
"name": "closeOnEscape",
|
|
594
|
+
"type": "boolean",
|
|
595
|
+
"required": false,
|
|
596
|
+
"default": "dismissible",
|
|
597
|
+
"description": "Whether Escape reports onClose(\"esc\"), separately from the close button\nand the backdrop (D101). A consequential confirmation is\n`dismissible={false} closeOnEscape`: no close button, no backdrop\ndismissal, and Escape cancels."
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
"name": "initialFocus",
|
|
601
|
+
"type": "RefObject<HTMLElement | null>",
|
|
602
|
+
"required": false,
|
|
603
|
+
"default": null,
|
|
604
|
+
"description": "The element to focus each time the dialog opens, after the platform's own\nfocusing (D101), e.g. a ref to the footer's Cancel button. Without it, the\nplatform focuses the first focusable control, as before."
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"name": "closeLabel",
|
|
608
|
+
"type": "string",
|
|
609
|
+
"required": false,
|
|
610
|
+
"default": null,
|
|
611
|
+
"description": "Visible text for the close button (D101). When set, the close button is a\nghost Button with this text instead of the icon-only \"Close dialog\"\nbutton."
|
|
591
612
|
},
|
|
592
613
|
{
|
|
593
614
|
"name": "ref",
|
|
@@ -635,7 +656,7 @@
|
|
|
635
656
|
"order": 3
|
|
636
657
|
}
|
|
637
658
|
],
|
|
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"
|
|
659
|
+
"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. `closeOnEscape` can give Escape back on its own (D101). |\n| `closeOnEscape` | `boolean` | dismissible | no | Whether Escape reports onClose(\"esc\"), separately from the close button and the backdrop (D101). A consequential confirmation is `dismissible={false} closeOnEscape`: no close button, no backdrop dismissal, and Escape cancels. |\n| `initialFocus` | `RefObject<HTMLElement \\| null>` | — | no | The element to focus each time the dialog opens, after the platform's own focusing (D101), e.g. a ref to the footer's Cancel button. Without it, the platform focuses the first focusable control, as before. |\n| `closeLabel` | `string` | — | no | Visible text for the close button (D101). When set, the close button is a ghost Button with this text instead of the icon-only \"Close dialog\" button. |\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 closeOnEscape (default: dismissible); swallowed otherwise (D101). |\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"
|
|
639
660
|
},
|
|
640
661
|
{
|
|
641
662
|
"name": "Checkbox",
|
|
@@ -655,6 +676,13 @@
|
|
|
655
676
|
"default": null,
|
|
656
677
|
"description": "Accessible name when there is no visible label — a table's row-selection\ncheckbox, say. Optional, unlike IconButton's required form (D60): a\nlabelled checkbox is still the common case."
|
|
657
678
|
},
|
|
679
|
+
{
|
|
680
|
+
"name": "indeterminate",
|
|
681
|
+
"type": "boolean",
|
|
682
|
+
"required": false,
|
|
683
|
+
"default": null,
|
|
684
|
+
"description": "The mixed state, e.g. a \"select all\" over a partial selection (D103).\nSets the input's DOM property, which the platform exposes as\naria-checked=\"mixed\", and draws a dash. Re-applied on every render,\nbecause a click clears the property before the consumer re-renders.\nWhen absent, the property is left alone, so a state set through the ref\nsurvives."
|
|
685
|
+
},
|
|
658
686
|
{
|
|
659
687
|
"name": "ref",
|
|
660
688
|
"type": "Ref<HTMLInputElement>",
|
|
@@ -671,7 +699,7 @@
|
|
|
671
699
|
}
|
|
672
700
|
],
|
|
673
701
|
"slots": [],
|
|
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"
|
|
702
|
+
"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| `indeterminate` | `boolean` | — | no | The mixed state, e.g. a \"select all\" over a partial selection (D103). Sets the input's DOM property, which the platform exposes as aria-checked=\"mixed\", and draws a dash. Re-applied on every render, because a click clears the property before the consumer re-renders. When absent, the property is left alone, so a state set through the ref survives. |\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"
|
|
675
703
|
},
|
|
676
704
|
{
|
|
677
705
|
"name": "Switch",
|
|
@@ -702,6 +730,202 @@
|
|
|
702
730
|
"slots": [],
|
|
703
731
|
"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"
|
|
704
732
|
},
|
|
733
|
+
{
|
|
734
|
+
"name": "Textarea",
|
|
735
|
+
"description": "Multi-line text input (D104): the Input recipe on a native `<textarea>`. Inside a Field, id/aria-describedby/aria-invalid/required are wired as for Input (D49). `minLength` and `maxLength` are the native attributes. With `maxLength`, a counter under the control shows the length; it is not a live region (a count spoken on every keystroke is noise) and joins aria-describedby, so it is read with the field on focus.",
|
|
736
|
+
"props": [
|
|
737
|
+
{
|
|
738
|
+
"name": "rows",
|
|
739
|
+
"type": "number",
|
|
740
|
+
"required": false,
|
|
741
|
+
"default": 3,
|
|
742
|
+
"description": "Visible rows. A textarea's height is its rows; there is no `size`."
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
"name": "minLength",
|
|
746
|
+
"type": "number",
|
|
747
|
+
"required": false,
|
|
748
|
+
"default": null,
|
|
749
|
+
"description": "Native minimum length, reported by constraint validation."
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
"name": "maxLength",
|
|
753
|
+
"type": "number",
|
|
754
|
+
"required": false,
|
|
755
|
+
"default": null,
|
|
756
|
+
"description": "Native maximum length, enforced while typing. With it, the counter shows\n`{length} / {maxLength}` unless `showCounter={false}`."
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
"name": "error",
|
|
760
|
+
"type": "boolean",
|
|
761
|
+
"required": false,
|
|
762
|
+
"default": false,
|
|
763
|
+
"description": "Show error styling. Inside a Field, the Field's error also lights this."
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
"name": "showCounter",
|
|
767
|
+
"type": "boolean",
|
|
768
|
+
"required": false,
|
|
769
|
+
"default": true,
|
|
770
|
+
"description": "With `maxLength`, show the `{length} / {maxLength}` counter under the\ncontrol. It is not a live region; it describes the textarea."
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
"name": "ref",
|
|
774
|
+
"type": "Ref<HTMLTextAreaElement>",
|
|
775
|
+
"required": false,
|
|
776
|
+
"default": null,
|
|
777
|
+
"description": "Forwarded ref to the underlying `<textarea>` element."
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
"name": "placeholder",
|
|
781
|
+
"type": "string",
|
|
782
|
+
"required": false,
|
|
783
|
+
"default": null,
|
|
784
|
+
"description": "Native placeholder text shown while the field is empty (only on components whose host element supports it)."
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
"name": "className",
|
|
788
|
+
"type": "string",
|
|
789
|
+
"required": false,
|
|
790
|
+
"default": null,
|
|
791
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
792
|
+
}
|
|
793
|
+
],
|
|
794
|
+
"slots": [],
|
|
795
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Textarea\n\nMulti-line text input (D104): the Input recipe on a native `<textarea>`. Inside a Field, id/aria-describedby/aria-invalid/required are wired as for Input (D49). `minLength` and `maxLength` are the native attributes. With `maxLength`, a counter under the control shows the length; it is not a live region (a count spoken on every keystroke is noise) and joins aria-describedby, so it is read with the field on focus.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `rows` | `number` | 3 | no | Visible rows. A textarea's height is its rows; there is no `size`. |\n| `minLength` | `number` | — | no | Native minimum length, reported by constraint validation. |\n| `maxLength` | `number` | — | no | Native maximum length, enforced while typing. With it, the counter shows `{length} / {maxLength}` unless `showCounter={false}`. |\n| `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |\n| `showCounter` | `boolean` | true | no | With `maxLength`, show the `{length} / {maxLength}` counter under the control. It is not a live region; it describes the textarea. |\n| `ref` | `Ref<HTMLTextAreaElement>` | — | no | Forwarded ref to the underlying `<textarea>` 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 | A native <textarea>; Enter adds a line, it does not submit the form. |\n\nInside a Field, id/aria-describedby/aria-invalid/required are wired as for Input (D104). minLength and maxLength are native. With maxLength, a counter under the control shows {length} / {maxLength}; it is not a live region, and it joins aria-describedby so it is read with the field on focus, with a visually hidden \"characters\".\n\n## Theming\n\nOverride `--psi-textarea-*` 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"
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
"name": "RadioGroup",
|
|
799
|
+
"description": "A set of native radios sharing one name (D104). The browser supplies the keyboard: Tab enters on the checked radio, and the arrows move and select. Inside `Field group` the fieldset and legend form and name the group, so this adds no role; standalone it is `role=\"radiogroup\"` and needs `aria-label` or `aria-labelledby`. A required or invalid Field makes every radio `required` and `aria-invalid`.",
|
|
800
|
+
"props": [
|
|
801
|
+
{
|
|
802
|
+
"name": "children",
|
|
803
|
+
"type": "ReactNode",
|
|
804
|
+
"required": false,
|
|
805
|
+
"default": null,
|
|
806
|
+
"description": "`Radio` children."
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
"name": "name",
|
|
810
|
+
"type": "string",
|
|
811
|
+
"required": false,
|
|
812
|
+
"default": null,
|
|
813
|
+
"description": "The radios' shared `name`. Generated when absent."
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
"name": "value",
|
|
817
|
+
"type": "string",
|
|
818
|
+
"required": false,
|
|
819
|
+
"default": null,
|
|
820
|
+
"description": "The selected value (controlled)."
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
"name": "defaultValue",
|
|
824
|
+
"type": "string",
|
|
825
|
+
"required": false,
|
|
826
|
+
"default": null,
|
|
827
|
+
"description": "The initially selected value (uncontrolled)."
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
"name": "onValueChange",
|
|
831
|
+
"type": "((value: string) => void)",
|
|
832
|
+
"required": false,
|
|
833
|
+
"default": null,
|
|
834
|
+
"description": "Called with the value of the radio the user selects."
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
"name": "disabled",
|
|
838
|
+
"type": "boolean",
|
|
839
|
+
"required": false,
|
|
840
|
+
"default": false,
|
|
841
|
+
"description": "Disables every radio in the group."
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
"name": "orientation",
|
|
845
|
+
"type": "\"vertical\" | \"horizontal\"",
|
|
846
|
+
"required": false,
|
|
847
|
+
"default": "vertical",
|
|
848
|
+
"description": "Stack the radios, or lay them in a row."
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
"name": "aria-label",
|
|
852
|
+
"type": "string",
|
|
853
|
+
"required": false,
|
|
854
|
+
"default": null,
|
|
855
|
+
"description": "Accessible name when the group stands alone. Inside `Field group` the\nlegend names the group instead."
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
"name": "ref",
|
|
859
|
+
"type": "Ref<HTMLDivElement>",
|
|
860
|
+
"required": false,
|
|
861
|
+
"default": null,
|
|
862
|
+
"description": "Forwarded ref to the root element."
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
"name": "className",
|
|
866
|
+
"type": "string",
|
|
867
|
+
"required": false,
|
|
868
|
+
"default": null,
|
|
869
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
870
|
+
}
|
|
871
|
+
],
|
|
872
|
+
"slots": [
|
|
873
|
+
{
|
|
874
|
+
"name": "body",
|
|
875
|
+
"accepts": {
|
|
876
|
+
"components": [
|
|
877
|
+
"Radio"
|
|
878
|
+
]
|
|
879
|
+
},
|
|
880
|
+
"cardinality": "1..*",
|
|
881
|
+
"order": 1
|
|
882
|
+
}
|
|
883
|
+
],
|
|
884
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# RadioGroup\n\nA set of native radios sharing one name (D104). The browser supplies the keyboard: Tab enters on the checked radio, and the arrows move and select. Inside `Field group` the fieldset and legend form and name the group, so this adds no role; standalone it is `role=\"radiogroup\"` and needs `aria-label` or `aria-labelledby`. A required or invalid Field makes every radio `required` and `aria-invalid`.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | `Radio` children. |\n| `name` | `string` | — | no | The radios' shared `name`. Generated when absent. |\n| `value` | `string` | — | no | The selected value (controlled). |\n| `defaultValue` | `string` | — | no | The initially selected value (uncontrolled). |\n| `onValueChange` | `((value: string) => void)` | — | no | Called with the value of the radio the user selects. |\n| `disabled` | `boolean` | false | no | Disables every radio in the group. |\n| `orientation` | `\"vertical\" \\| \"horizontal\"` | vertical | no | Stack the radios, or lay them in a row. |\n| `aria-label` | `string` | — | no | Accessible name when the group stands alone. Inside `Field group` the legend names the group instead. |\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 | Enters the group on the checked radio, or the first one; one tab stop for the group. |\n| Arrow keys | Move to the next or previous radio and select it (native radios sharing one name). |\n\nNative radios sharing one name (D104). Inside Field group the fieldset and legend form and name the group, so RadioGroup adds no role; standalone it is role=\"radiogroup\" and needs aria-label or aria-labelledby. A required or invalid Field makes every radio required and aria-invalid.\n\n## Theming\n\nOverride `--psi-radio-group-*` 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"
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
"name": "Radio",
|
|
888
|
+
"description": "One choice in a RadioGroup (D104): a native radio with a built-in label, drawn like Checkbox on a circle. Takes its name, checked state, required and aria-invalid from the group.",
|
|
889
|
+
"props": [
|
|
890
|
+
{
|
|
891
|
+
"name": "value",
|
|
892
|
+
"type": "string",
|
|
893
|
+
"required": true,
|
|
894
|
+
"default": null,
|
|
895
|
+
"description": "The value this radio selects."
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
"name": "children",
|
|
899
|
+
"type": "ReactNode",
|
|
900
|
+
"required": false,
|
|
901
|
+
"default": null,
|
|
902
|
+
"description": "Label text."
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
"name": "disabled",
|
|
906
|
+
"type": "boolean",
|
|
907
|
+
"required": false,
|
|
908
|
+
"default": null,
|
|
909
|
+
"description": "Disables this radio; a disabled RadioGroup disables all of them."
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
"name": "className",
|
|
913
|
+
"type": "string",
|
|
914
|
+
"required": false,
|
|
915
|
+
"default": null,
|
|
916
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
"name": "ref",
|
|
920
|
+
"type": "Ref<HTMLInputElement>",
|
|
921
|
+
"required": false,
|
|
922
|
+
"default": null,
|
|
923
|
+
"description": "Forwarded ref to the underlying `<input type=\"radio\">` element."
|
|
924
|
+
}
|
|
925
|
+
],
|
|
926
|
+
"slots": [],
|
|
927
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Radio\n\nOne choice in a RadioGroup (D104): a native radio with a built-in label, drawn like Checkbox on a circle. Takes its name, checked state, required and aria-invalid from the group.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | The value this radio selects. |\n| `children` | `ReactNode` | — | no | Label text. |\n| `disabled` | `boolean` | — | no | Disables this radio; a disabled RadioGroup disables all of them. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type=\"radio\">` element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Space | Selects the focused radio when none is selected. |\n\nNative <input type=radio> underneath (visually hidden), drawn like Checkbox on a circle. Takes its name, checked state, required and aria-invalid from its RadioGroup.\n\n## Theming\n\nThis component has no `--psi-radio-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\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"
|
|
928
|
+
},
|
|
705
929
|
{
|
|
706
930
|
"name": "Tag",
|
|
707
931
|
"description": "Compact label for status or category; semantic variants, optional subtle tint and dismiss button.",
|
|
@@ -1248,10 +1472,10 @@
|
|
|
1248
1472
|
},
|
|
1249
1473
|
{
|
|
1250
1474
|
"name": "onClose",
|
|
1251
|
-
"type": "(reason:
|
|
1475
|
+
"type": "(reason: MenuCloseReason) => void",
|
|
1252
1476
|
"required": true,
|
|
1253
1477
|
"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."
|
|
1478
|
+
"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.\n\n`\"tab\"` (D102): Tab or Shift Tab inside the open menu. Focus moves on to\nthe next control as the browser moves it; the consumer flips `open`."
|
|
1255
1479
|
},
|
|
1256
1480
|
{
|
|
1257
1481
|
"name": "trigger",
|
|
@@ -1267,6 +1491,13 @@
|
|
|
1267
1491
|
"default": "bottom-start",
|
|
1268
1492
|
"description": "Placement relative to the trigger."
|
|
1269
1493
|
},
|
|
1494
|
+
{
|
|
1495
|
+
"name": "id",
|
|
1496
|
+
"type": "string",
|
|
1497
|
+
"required": false,
|
|
1498
|
+
"default": null,
|
|
1499
|
+
"description": "The menu element's id, which the trigger's `aria-controls` names (D102).\nGenerated when absent."
|
|
1500
|
+
},
|
|
1270
1501
|
{
|
|
1271
1502
|
"name": "aria-label",
|
|
1272
1503
|
"type": "string",
|
|
@@ -1327,7 +1558,7 @@
|
|
|
1327
1558
|
"order": 2
|
|
1328
1559
|
}
|
|
1329
1560
|
],
|
|
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:
|
|
1561
|
+
"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: MenuCloseReason) => void` | — | yes | Called when a dismissal is *requested*; the consumer flips `open`. Menu never closes itself on `\"esc\"` or `\"item-select\"` — the popover stays open until `open` becomes false (D50). `\"outside\"` is the one exception: light dismiss is performed by the browser before it tells us, so by the time that reason is reported the popover is already closed. Flipping `open` to false is still required, to keep React's state in step. Invariant (D58): a dismissal is only ever reported for a menu that is currently open according to its own `open` prop. The platform can close an auto popover before the consumer's state catches up — clicking another menu's trigger light-dismisses this one before the consumer's click handler runs — and a report for an already-closed menu would clear a selection that has since moved on. `\"tab\"` (D102): Tab or Shift Tab inside the open menu. Focus moves on to the next control as the browser moves it; the consumer flips `open`. |\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| `id` | `string` | — | no | The menu element's id, which the trigger's `aria-controls` names (D102). Generated when absent. |\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| Arrow Down / Arrow Up on the closed trigger | Opens the menu on its first / last enabled item by clicking the trigger, since Menu cannot open itself; the consumer's onClick flips `open` (D102). |\n| Tab / Shift Tab | Reports onClose(\"tab\") and lets focus move on to the next control; the consumer flips `open`, and focus is not pulled back to the trigger (D102). |\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"
|
|
1331
1562
|
},
|
|
1332
1563
|
{
|
|
1333
1564
|
"name": "MenuItem",
|
|
@@ -4867,6 +5098,111 @@
|
|
|
4867
5098
|
"blocked": false,
|
|
4868
5099
|
"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"
|
|
4869
5100
|
},
|
|
5101
|
+
{
|
|
5102
|
+
"id": "feedback-form",
|
|
5103
|
+
"intent": "A short report form: a required choice of category as radios, a bounded description with a length counter, and a submit button",
|
|
5104
|
+
"match": [
|
|
5105
|
+
"feedback form",
|
|
5106
|
+
"report a problem",
|
|
5107
|
+
"radio buttons",
|
|
5108
|
+
"text area",
|
|
5109
|
+
"character counter"
|
|
5110
|
+
],
|
|
5111
|
+
"compose": {
|
|
5112
|
+
"component": "Panel",
|
|
5113
|
+
"props": {
|
|
5114
|
+
"padding": 24
|
|
5115
|
+
},
|
|
5116
|
+
"slots": {
|
|
5117
|
+
"body": [
|
|
5118
|
+
{
|
|
5119
|
+
"component": "Field",
|
|
5120
|
+
"props": {
|
|
5121
|
+
"group": true,
|
|
5122
|
+
"required": true,
|
|
5123
|
+
"requiredText": "(Required)"
|
|
5124
|
+
},
|
|
5125
|
+
"slots": {
|
|
5126
|
+
"label": [
|
|
5127
|
+
"{content:category-label}"
|
|
5128
|
+
],
|
|
5129
|
+
"body": [
|
|
5130
|
+
{
|
|
5131
|
+
"component": "RadioGroup",
|
|
5132
|
+
"slots": {
|
|
5133
|
+
"body": [
|
|
5134
|
+
{
|
|
5135
|
+
"component": "Radio",
|
|
5136
|
+
"props": {
|
|
5137
|
+
"value": "problem"
|
|
5138
|
+
},
|
|
5139
|
+
"content": "first-option"
|
|
5140
|
+
},
|
|
5141
|
+
{
|
|
5142
|
+
"component": "Radio",
|
|
5143
|
+
"props": {
|
|
5144
|
+
"value": "idea"
|
|
5145
|
+
},
|
|
5146
|
+
"content": "second-option"
|
|
5147
|
+
},
|
|
5148
|
+
{
|
|
5149
|
+
"component": "Radio",
|
|
5150
|
+
"props": {
|
|
5151
|
+
"value": "other"
|
|
5152
|
+
},
|
|
5153
|
+
"content": "third-option"
|
|
5154
|
+
}
|
|
5155
|
+
]
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
]
|
|
5159
|
+
}
|
|
5160
|
+
},
|
|
5161
|
+
{
|
|
5162
|
+
"component": "Field",
|
|
5163
|
+
"slots": {
|
|
5164
|
+
"label": [
|
|
5165
|
+
"{content:description-label}"
|
|
5166
|
+
],
|
|
5167
|
+
"body": [
|
|
5168
|
+
{
|
|
5169
|
+
"component": "Textarea",
|
|
5170
|
+
"props": {
|
|
5171
|
+
"maxLength": 500,
|
|
5172
|
+
"rows": 4
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
],
|
|
5176
|
+
"description": [
|
|
5177
|
+
"{content:description-help}"
|
|
5178
|
+
]
|
|
5179
|
+
}
|
|
5180
|
+
},
|
|
5181
|
+
{
|
|
5182
|
+
"component": "Button",
|
|
5183
|
+
"props": {
|
|
5184
|
+
"type": "submit",
|
|
5185
|
+
"variant": "accent"
|
|
5186
|
+
},
|
|
5187
|
+
"content": "submit-label"
|
|
5188
|
+
}
|
|
5189
|
+
]
|
|
5190
|
+
}
|
|
5191
|
+
},
|
|
5192
|
+
"parameters": [],
|
|
5193
|
+
"content": {
|
|
5194
|
+
"category-label": "Category",
|
|
5195
|
+
"first-option": "Something is broken",
|
|
5196
|
+
"second-option": "An idea",
|
|
5197
|
+
"third-option": "Something else",
|
|
5198
|
+
"description-label": "Description",
|
|
5199
|
+
"description-help": "[what happened, and what you expected]",
|
|
5200
|
+
"submit-label": "Send"
|
|
5201
|
+
},
|
|
5202
|
+
"gaps": [],
|
|
5203
|
+
"blocked": false,
|
|
5204
|
+
"preset": "<Panel\n padding={24}\n>\n <Field\n group={true}\n label=\"Category\"\n required={true}\n requiredText=\"(Required)\"\n >\n <RadioGroup>\n <Radio value=\"problem\">Something is broken</Radio>\n <Radio value=\"idea\">An idea</Radio>\n <Radio value=\"other\">Something else</Radio>\n </RadioGroup>\n </Field>\n <Field\n description=\"[what happened, and what you expected]\"\n label=\"Description\"\n >\n <Textarea maxLength={500} rows={4} />\n </Field>\n <Button type=\"submit\" variant=\"accent\">Send</Button>\n</Panel>\n"
|
|
5205
|
+
},
|
|
4870
5206
|
{
|
|
4871
5207
|
"id": "filter-form",
|
|
4872
5208
|
"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",
|
package/dist/store.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export interface Brief {
|
|
|
5
5
|
title: string;
|
|
6
6
|
summary: string;
|
|
7
7
|
}
|
|
8
|
+
/** A pattern row in the empty-query overview: a `Brief` without `title`,
|
|
9
|
+
* which for a pattern only repeats the id after `pattern:` (D100). At the
|
|
10
|
+
* derived intro cap of 0 the title was a quarter of the row, and the row is
|
|
11
|
+
* all the overview can no longer shorten. */
|
|
12
|
+
export type OverviewPatternBrief = Omit<Brief, "title">;
|
|
13
|
+
/** What `search()` returns: stored briefs for a keyword query; for the empty
|
|
14
|
+
* query, the overview, where pattern rows are `OverviewPatternBrief`. */
|
|
15
|
+
export type SearchBrief = Brief | OverviewPatternBrief;
|
|
8
16
|
export type Detail = ({
|
|
9
17
|
kind: "component";
|
|
10
18
|
} & ComponentEntry) | ({
|
|
@@ -17,7 +25,7 @@ export type Detail = ({
|
|
|
17
25
|
content: unknown;
|
|
18
26
|
};
|
|
19
27
|
export interface Store {
|
|
20
|
-
search(query: string):
|
|
28
|
+
search(query: string): SearchBrief[];
|
|
21
29
|
get(id: string): Detail | null;
|
|
22
30
|
}
|
|
23
31
|
export declare function createStore(index: PsiIndex): Store;
|
package/dist/store.js
CHANGED
|
@@ -93,24 +93,26 @@ function overviewPatternSummary(p, limit) {
|
|
|
93
93
|
const intro = truncateAtWord(`${p.intent} — ${p.match.join(", ")}, ${p.parameters.length} parameters`, limit);
|
|
94
94
|
return p.blocked ? `${intro}, blocked (gaps: ${p.gaps.join(", ")})` : intro;
|
|
95
95
|
}
|
|
96
|
+
const PATTERN_PREFIX = "pattern:";
|
|
96
97
|
export function createStore(index) {
|
|
97
98
|
const patternsById = new Map(index.patterns.map((p) => [p.id, p]));
|
|
98
99
|
/** Overview-only projection: returns `b` unchanged for every kind except
|
|
99
100
|
* pattern, where it re-derives the summary from the underlying
|
|
100
101
|
* `PatternEntry` (via `overviewPatternSummary`) rather than slicing the
|
|
101
102
|
* already-assembled, full-length `Brief.summary` — so the `blocked (gaps:
|
|
102
|
-
* …)` tail always survives the overview's trim.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
103
|
+
* …)` tail always survives the overview's trim. A pattern row is a new
|
|
104
|
+
* `{id, kind, summary}` object with no `title`, which would only repeat
|
|
105
|
+
* the id (D100); the stored brief kept in `briefs` (and used by score())
|
|
106
|
+
* is never mutated. `limit` is derived per-call by the empty-query branch
|
|
105
107
|
* (D61), not a module constant, so a growing catalog shortens summaries
|
|
106
108
|
* instead of evicting items. */
|
|
107
109
|
function overviewBrief(b, limit) {
|
|
108
110
|
if (b.kind !== "pattern")
|
|
109
111
|
return b;
|
|
110
|
-
const pattern = patternsById.get(b.
|
|
112
|
+
const pattern = patternsById.get(b.id.slice(PATTERN_PREFIX.length));
|
|
111
113
|
if (!pattern)
|
|
112
114
|
return b;
|
|
113
|
-
return {
|
|
115
|
+
return { id: b.id, kind: b.kind, summary: overviewPatternSummary(pattern, limit) };
|
|
114
116
|
}
|
|
115
117
|
const briefs = [
|
|
116
118
|
...index.components.map((c) => ({
|
|
@@ -120,7 +122,7 @@ export function createStore(index) {
|
|
|
120
122
|
summary: componentSummary(c),
|
|
121
123
|
})),
|
|
122
124
|
...index.patterns.map((p) => ({
|
|
123
|
-
id:
|
|
125
|
+
id: `${PATTERN_PREFIX}${p.id}`,
|
|
124
126
|
kind: "pattern",
|
|
125
127
|
title: p.id,
|
|
126
128
|
summary: patternSummary(p),
|
package/llms.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @handamade/psi-mcp — MCP access to Psi (Ψ)
|
|
2
2
|
|
|
3
3
|
Two read-only tools over a baked index of the generated artifacts:
|
|
4
|
-
- search(query): briefs {id, kind, title, summary}; empty query = overview
|
|
4
|
+
- search(query): briefs {id, kind, title, summary}; empty query = overview, where pattern rows are {id, kind, summary} (the id names the pattern, D100)
|
|
5
5
|
- get(id): component (props/defaults/a11y doc) | token (formula + OKLCH/hex per theme) | topic (guidance) | pattern (compose tree, clarifying parameters, content, gaps/blocked status, preset JSX)
|
|
6
6
|
Ids: component:<Name>, token:<name>, topic:<key>, pattern:<id>; bare names resolve case-insensitively.
|
|
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: 24 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.26.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-tokens": "0.
|
|
37
|
-
"@handamade/psi-react": "0.
|
|
36
|
+
"@handamade/psi-tokens": "0.26.0",
|
|
37
|
+
"@handamade/psi-react": "0.26.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"
|