@handamade/psi-mcp 0.9.0 → 0.10.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 +342 -18
- package/dist/store.js +51 -35
- 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 25 React 19 components, 13 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.10.0",
|
|
3
3
|
"themes": [
|
|
4
4
|
"light",
|
|
5
5
|
"dark",
|
|
@@ -481,6 +481,13 @@
|
|
|
481
481
|
"default": null,
|
|
482
482
|
"description": "Label text."
|
|
483
483
|
},
|
|
484
|
+
{
|
|
485
|
+
"name": "aria-label",
|
|
486
|
+
"type": "string",
|
|
487
|
+
"required": false,
|
|
488
|
+
"default": null,
|
|
489
|
+
"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."
|
|
490
|
+
},
|
|
484
491
|
{
|
|
485
492
|
"name": "ref",
|
|
486
493
|
"type": "Ref<HTMLInputElement>",
|
|
@@ -497,7 +504,7 @@
|
|
|
497
504
|
}
|
|
498
505
|
],
|
|
499
506
|
"slots": [],
|
|
500
|
-
"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| `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- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
507
|
+
"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- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
501
508
|
},
|
|
502
509
|
{
|
|
503
510
|
"name": "Switch",
|
|
@@ -829,6 +836,256 @@
|
|
|
829
836
|
"props": [],
|
|
830
837
|
"slots": [],
|
|
831
838
|
"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- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
"name": "Table",
|
|
842
|
+
"description": "Data table on native table semantics. Holds no state: sorting, selection and pagination are the consumer's (D62, extending D50/D53).",
|
|
843
|
+
"props": [
|
|
844
|
+
{
|
|
845
|
+
"name": "size",
|
|
846
|
+
"type": "32 | 40 | 48",
|
|
847
|
+
"required": false,
|
|
848
|
+
"default": 40,
|
|
849
|
+
"description": "Row height in px."
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
"name": "stickyHeader",
|
|
853
|
+
"type": "boolean",
|
|
854
|
+
"required": false,
|
|
855
|
+
"default": null,
|
|
856
|
+
"description": "Pins the header while the body scrolls."
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
"name": "sortable",
|
|
860
|
+
"type": "boolean",
|
|
861
|
+
"required": false,
|
|
862
|
+
"default": false,
|
|
863
|
+
"description": "Enables the sort affordance on header cells that declare a `sortKey`."
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
"name": "sort",
|
|
867
|
+
"type": "TableSortState | null",
|
|
868
|
+
"required": false,
|
|
869
|
+
"default": null,
|
|
870
|
+
"description": "Controlled sort state; `null` when nothing is sorted."
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
"name": "onSortChange",
|
|
874
|
+
"type": "((sort: TableSortState) => void)",
|
|
875
|
+
"required": false,
|
|
876
|
+
"default": null,
|
|
877
|
+
"description": "Called when a header's sort control is activated. Optional in the type\nbecause `sortable` may be false; a discriminated union expressing the real\ncontract does not survive docgen's flat prop extraction, which would strip\nthese props from the manifest entirely (D62)."
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
"name": "selectable",
|
|
881
|
+
"type": "boolean",
|
|
882
|
+
"required": false,
|
|
883
|
+
"default": false,
|
|
884
|
+
"description": "Renders the row-selection checkbox column."
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
"name": "selected",
|
|
888
|
+
"type": "ReadonlySet<string>",
|
|
889
|
+
"required": false,
|
|
890
|
+
"default": "new Set<string>()",
|
|
891
|
+
"description": "Controlled selection, keyed by each `TableRow`'s `rowId`."
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
"name": "onSelectionChange",
|
|
895
|
+
"type": "((selected: ReadonlySet<string>) => void)",
|
|
896
|
+
"required": false,
|
|
897
|
+
"default": null,
|
|
898
|
+
"description": "Called with the next selection. See `onSortChange` on why it is optional."
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
"name": "className",
|
|
902
|
+
"type": "string",
|
|
903
|
+
"required": false,
|
|
904
|
+
"default": null,
|
|
905
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
"name": "ref",
|
|
909
|
+
"type": "Ref<HTMLTableElement>",
|
|
910
|
+
"required": false,
|
|
911
|
+
"default": null,
|
|
912
|
+
"description": "Forwarded ref to the underlying `<table>` element."
|
|
913
|
+
}
|
|
914
|
+
],
|
|
915
|
+
"slots": [
|
|
916
|
+
{
|
|
917
|
+
"name": "body",
|
|
918
|
+
"accepts": {
|
|
919
|
+
"components": [
|
|
920
|
+
"TableHead",
|
|
921
|
+
"TableBody"
|
|
922
|
+
]
|
|
923
|
+
},
|
|
924
|
+
"cardinality": "1..*",
|
|
925
|
+
"order": 1
|
|
926
|
+
}
|
|
927
|
+
],
|
|
928
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Table\n\nData table on native table semantics. Holds no state: sorting, selection and pagination are the consumer's (D62, extending D50/D53).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `size` | `32 \\| 40 \\| 48` | 40 | no | Row height in px. |\n| `stickyHeader` | `boolean` | — | no | Pins the header while the body scrolls. |\n| `sortable` | `boolean` | false | no | Enables the sort affordance on header cells that declare a `sortKey`. |\n| `sort` | `TableSortState \\| null` | — | no | Controlled sort state; `null` when nothing is sorted. |\n| `onSortChange` | `((sort: TableSortState) => void)` | — | no | Called when a header's sort control is activated. Optional in the type because `sortable` may be false; a discriminated union expressing the real contract does not survive docgen's flat prop extraction, which would strip these props from the manifest entirely (D62). |\n| `selectable` | `boolean` | false | no | Renders the row-selection checkbox column. |\n| `selected` | `ReadonlySet<string>` | new Set<string>() | no | Controlled selection, keyed by each `TableRow`'s `rowId`. |\n| `onSelectionChange` | `((selected: ReadonlySet<string>) => void)` | — | no | Called with the next selection. See `onSortChange` on why it is optional. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLTableElement>` | — | no | Forwarded ref to the underlying `<table>` element. |\n\n## Theming\n\nOverride `--psi-table-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
"name": "TableHead",
|
|
932
|
+
"description": "`<thead>`. When the table is selectable, prepends the select-all cell to the FIRST header row so the header's column count matches the body's — the checkbox column is Table's, not the consumer's schema. Clones the row rather than rebuilding it, so the consumer's own props on that `TableRow` survive.",
|
|
933
|
+
"props": [
|
|
934
|
+
{
|
|
935
|
+
"name": "className",
|
|
936
|
+
"type": "string",
|
|
937
|
+
"required": false,
|
|
938
|
+
"default": null,
|
|
939
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
940
|
+
}
|
|
941
|
+
],
|
|
942
|
+
"slots": [],
|
|
943
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableHead\n\n`<thead>`. When the table is selectable, prepends the select-all cell to the FIRST header row so the header's column count matches the body's — the checkbox column is Table's, not the consumer's schema. Clones the row rather than rebuilding it, so the consumer's own props on that `TableRow` survive.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-table-head-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
"name": "TableBody",
|
|
947
|
+
"description": "`<tbody>` wrapper.",
|
|
948
|
+
"props": [
|
|
949
|
+
{
|
|
950
|
+
"name": "className",
|
|
951
|
+
"type": "string",
|
|
952
|
+
"required": false,
|
|
953
|
+
"default": null,
|
|
954
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
955
|
+
}
|
|
956
|
+
],
|
|
957
|
+
"slots": [],
|
|
958
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableBody\n\n`<tbody>` wrapper.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-table-body-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
"name": "TableRow",
|
|
962
|
+
"description": "`<tr>`. `rowId` is the key the `selected` set holds.",
|
|
963
|
+
"props": [
|
|
964
|
+
{
|
|
965
|
+
"name": "rowId",
|
|
966
|
+
"type": "string",
|
|
967
|
+
"required": false,
|
|
968
|
+
"default": null,
|
|
969
|
+
"description": "Stable identity for selection. Required for selectable tables (D62)."
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
"name": "selectLabel",
|
|
973
|
+
"type": "string",
|
|
974
|
+
"required": false,
|
|
975
|
+
"default": null,
|
|
976
|
+
"description": "Accessible name for this row's selection checkbox."
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
"name": "className",
|
|
980
|
+
"type": "string",
|
|
981
|
+
"required": false,
|
|
982
|
+
"default": null,
|
|
983
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
984
|
+
}
|
|
985
|
+
],
|
|
986
|
+
"slots": [],
|
|
987
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableRow\n\n`<tr>`. `rowId` is the key the `selected` set holds.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `rowId` | `string` | — | no | Stable identity for selection. Required for selectable tables (D62). |\n| `selectLabel` | `string` | — | no | Accessible name for this row's selection checkbox. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-table-row-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
"name": "TableHeaderCell",
|
|
991
|
+
"description": "`<th scope=\"col\">`. `aria-sort` belongs on the th, never on the inner button — assistive tech reads the sort state from the column header.",
|
|
992
|
+
"props": [
|
|
993
|
+
{
|
|
994
|
+
"name": "sortKey",
|
|
995
|
+
"type": "string",
|
|
996
|
+
"required": false,
|
|
997
|
+
"default": null,
|
|
998
|
+
"description": "Sort key this column emits. Enables the sort control when the table is `sortable`."
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
"name": "numeric",
|
|
1002
|
+
"type": "boolean",
|
|
1003
|
+
"required": false,
|
|
1004
|
+
"default": null,
|
|
1005
|
+
"description": "Right-aligns and renders tabular figures (D62)."
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
"name": "className",
|
|
1009
|
+
"type": "string",
|
|
1010
|
+
"required": false,
|
|
1011
|
+
"default": null,
|
|
1012
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1013
|
+
}
|
|
1014
|
+
],
|
|
1015
|
+
"slots": [],
|
|
1016
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableHeaderCell\n\n`<th scope=\"col\">`. `aria-sort` belongs on the th, never on the inner button — assistive tech reads the sort state from the column header.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `sortKey` | `string` | — | no | Sort key this column emits. Enables the sort control when the table is `sortable`. |\n| `numeric` | `boolean` | — | no | Right-aligns and renders tabular figures (D62). |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-table-header-cell-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
"name": "TableCell",
|
|
1020
|
+
"description": "`<td>`. `numeric` means right-aligned *and* tabular — a column that aligns but whose digits jitter between rows defeats the purpose.",
|
|
1021
|
+
"props": [
|
|
1022
|
+
{
|
|
1023
|
+
"name": "numeric",
|
|
1024
|
+
"type": "boolean",
|
|
1025
|
+
"required": false,
|
|
1026
|
+
"default": null,
|
|
1027
|
+
"description": "Right-aligns and renders tabular figures (D62)."
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
"name": "className",
|
|
1031
|
+
"type": "string",
|
|
1032
|
+
"required": false,
|
|
1033
|
+
"default": null,
|
|
1034
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1035
|
+
}
|
|
1036
|
+
],
|
|
1037
|
+
"slots": [],
|
|
1038
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TableCell\n\n`<td>`. `numeric` means right-aligned *and* tabular — a column that aligns but whose digits jitter between rows defeats the purpose.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `numeric` | `boolean` | — | no | Right-aligns and renders tabular figures (D62). |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-table-cell-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
"name": "Pagination",
|
|
1042
|
+
"description": "Numbered pager with ellipsis truncation (D63). Standalone rather than a Table family member: `table-pagination` composes it as a Toolbar sibling of the page-size Select.",
|
|
1043
|
+
"props": [
|
|
1044
|
+
{
|
|
1045
|
+
"name": "page",
|
|
1046
|
+
"type": "number",
|
|
1047
|
+
"required": true,
|
|
1048
|
+
"default": null,
|
|
1049
|
+
"description": "Current page, 1-based."
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
"name": "pageCount",
|
|
1053
|
+
"type": "number",
|
|
1054
|
+
"required": true,
|
|
1055
|
+
"default": null,
|
|
1056
|
+
"description": "Total number of pages."
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
"name": "onPageChange",
|
|
1060
|
+
"type": "(page: number) => void",
|
|
1061
|
+
"required": true,
|
|
1062
|
+
"default": null,
|
|
1063
|
+
"description": "Called with the requested page. Required — unlike Table's `onSortChange`/\n`onSelectionChange`, which are optional because `sortable`/`selectable`\ngate whether they're meaningful, `Pagination` has no such gating boolean:\nthe prop is unconditionally meaningful, so a `Pagination` without it is a\ndead control (final review finding, D62)."
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
"name": "siblingCount",
|
|
1067
|
+
"type": "number",
|
|
1068
|
+
"required": false,
|
|
1069
|
+
"default": 1,
|
|
1070
|
+
"description": "Pages shown either side of the current one before truncating."
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
"name": "aria-label",
|
|
1074
|
+
"type": "string",
|
|
1075
|
+
"required": false,
|
|
1076
|
+
"default": "Pagination",
|
|
1077
|
+
"description": "Accessible name for the nav landmark."
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
"name": "className",
|
|
1081
|
+
"type": "string",
|
|
1082
|
+
"required": false,
|
|
1083
|
+
"default": null,
|
|
1084
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
1085
|
+
}
|
|
1086
|
+
],
|
|
1087
|
+
"slots": [],
|
|
1088
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Pagination\n\nNumbered pager with ellipsis truncation (D63). Standalone rather than a Table family member: `table-pagination` composes it as a Toolbar sibling of the page-size Select.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `page` | `number` | — | yes | Current page, 1-based. |\n| `pageCount` | `number` | — | yes | Total number of pages. |\n| `onPageChange` | `(page: number) => void` | — | yes | Called with the requested page. Required — unlike Table's `onSortChange`/ `onSelectionChange`, which are optional because `sortable`/`selectable` gate whether they're meaningful, `Pagination` has no such gating boolean: the prop is unconditionally meaningful, so a `Pagination` without it is a dead control (final review finding, D62). |\n| `siblingCount` | `number` | 1 | no | Pages shown either side of the current one before truncating. |\n| `aria-label` | `string` | Pagination | no | Accessible name for the nav landmark. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nThis component has no `--psi-pagination-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
832
1089
|
}
|
|
833
1090
|
],
|
|
834
1091
|
"tokens": [
|
|
@@ -2458,24 +2715,90 @@
|
|
|
2458
2715
|
"sortable": true
|
|
2459
2716
|
},
|
|
2460
2717
|
"slots": {
|
|
2461
|
-
"head": [
|
|
2462
|
-
"{content:columns-note}"
|
|
2463
|
-
],
|
|
2464
2718
|
"body": [
|
|
2465
|
-
|
|
2719
|
+
{
|
|
2720
|
+
"component": "TableHead",
|
|
2721
|
+
"slots": {
|
|
2722
|
+
"body": [
|
|
2723
|
+
{
|
|
2724
|
+
"component": "TableRow",
|
|
2725
|
+
"slots": {
|
|
2726
|
+
"body": [
|
|
2727
|
+
{
|
|
2728
|
+
"component": "TableHeaderCell",
|
|
2729
|
+
"props": {
|
|
2730
|
+
"sortKey": "date"
|
|
2731
|
+
},
|
|
2732
|
+
"content": "date-label"
|
|
2733
|
+
},
|
|
2734
|
+
{
|
|
2735
|
+
"component": "TableHeaderCell",
|
|
2736
|
+
"props": {
|
|
2737
|
+
"sortKey": "payee"
|
|
2738
|
+
},
|
|
2739
|
+
"content": "payee-label"
|
|
2740
|
+
},
|
|
2741
|
+
{
|
|
2742
|
+
"component": "TableHeaderCell",
|
|
2743
|
+
"props": {
|
|
2744
|
+
"sortKey": "amount",
|
|
2745
|
+
"numeric": true
|
|
2746
|
+
},
|
|
2747
|
+
"content": "amount-label"
|
|
2748
|
+
}
|
|
2749
|
+
]
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
]
|
|
2753
|
+
}
|
|
2754
|
+
},
|
|
2755
|
+
{
|
|
2756
|
+
"component": "TableBody",
|
|
2757
|
+
"slots": {
|
|
2758
|
+
"body": [
|
|
2759
|
+
{
|
|
2760
|
+
"component": "TableRow",
|
|
2761
|
+
"props": {
|
|
2762
|
+
"rowId": "row-id"
|
|
2763
|
+
},
|
|
2764
|
+
"slots": {
|
|
2765
|
+
"body": [
|
|
2766
|
+
{
|
|
2767
|
+
"component": "TableCell",
|
|
2768
|
+
"content": "date-cell"
|
|
2769
|
+
},
|
|
2770
|
+
{
|
|
2771
|
+
"component": "TableCell",
|
|
2772
|
+
"content": "payee-cell"
|
|
2773
|
+
},
|
|
2774
|
+
{
|
|
2775
|
+
"component": "TableCell",
|
|
2776
|
+
"props": {
|
|
2777
|
+
"numeric": true
|
|
2778
|
+
},
|
|
2779
|
+
"content": "amount-cell"
|
|
2780
|
+
}
|
|
2781
|
+
]
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
]
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2466
2787
|
]
|
|
2467
2788
|
}
|
|
2468
2789
|
},
|
|
2469
2790
|
"parameters": [],
|
|
2470
2791
|
"content": {
|
|
2471
|
-
"
|
|
2472
|
-
"
|
|
2792
|
+
"date-label": "Date",
|
|
2793
|
+
"payee-label": "Payee",
|
|
2794
|
+
"amount-label": "Amount",
|
|
2795
|
+
"date-cell": "[one row per record]",
|
|
2796
|
+
"payee-cell": "[put the row-actions menu in the last cell]",
|
|
2797
|
+
"amount-cell": "[numeric columns align right and use tabular figures]"
|
|
2473
2798
|
},
|
|
2474
|
-
"gaps": [
|
|
2475
|
-
|
|
2476
|
-
]
|
|
2477
|
-
"blocked": true,
|
|
2478
|
-
"preset": null
|
|
2799
|
+
"gaps": [],
|
|
2800
|
+
"blocked": false,
|
|
2801
|
+
"preset": "<Table\n selectable={true}\n sortable={true}\n>\n <TableHead>\n <TableRow>\n <TableHeaderCell sortKey=\"date\">Date</TableHeaderCell>\n <TableHeaderCell sortKey=\"payee\">Payee</TableHeaderCell>\n <TableHeaderCell numeric={true} sortKey=\"amount\">Amount</TableHeaderCell>\n </TableRow>\n </TableHead>\n <TableBody>\n <TableRow\n rowId=\"row-id\"\n >\n <TableCell>[one row per record]</TableCell>\n <TableCell>[put the row-actions menu in the last cell]</TableCell>\n <TableCell numeric={true}>[numeric columns align right and use tabular figures]</TableCell>\n </TableRow>\n </TableBody>\n</Table>\n"
|
|
2479
2802
|
},
|
|
2480
2803
|
{
|
|
2481
2804
|
"id": "date-range-filter",
|
|
@@ -2976,11 +3299,9 @@
|
|
|
2976
3299
|
"content": {
|
|
2977
3300
|
"page-size-label": "Rows per page"
|
|
2978
3301
|
},
|
|
2979
|
-
"gaps": [
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
"blocked": true,
|
|
2983
|
-
"preset": null
|
|
3302
|
+
"gaps": [],
|
|
3303
|
+
"blocked": false,
|
|
3304
|
+
"preset": "<Toolbar\n gap={12}\n>\n <Field\n label=\"Rows per page\"\n >\n <Select size={32} />\n </Field>\n <Pagination />\n</Toolbar>\n"
|
|
2984
3305
|
}
|
|
2985
3306
|
],
|
|
2986
3307
|
"scales": {
|
|
@@ -3040,6 +3361,7 @@
|
|
|
3040
3361
|
"gutterNarrow": 24
|
|
3041
3362
|
},
|
|
3042
3363
|
"zIndex": {
|
|
3364
|
+
"sticky": 1,
|
|
3043
3365
|
"nav": 100,
|
|
3044
3366
|
"overlay": 1000,
|
|
3045
3367
|
"tooltip": 1100
|
|
@@ -3171,6 +3493,7 @@
|
|
|
3171
3493
|
},
|
|
3172
3494
|
"recipes": {
|
|
3173
3495
|
"mediaTint": "Apply .psi-media-tint to media elements; the brand defines --psi-media-tint (D35). Hover/focus reveals true color over --psi-duration-450 --psi-ease-soft.",
|
|
3496
|
+
"tabularNumerals": "Apply .psi-tabular to numeric content for fixed-width digits; --psi-font-variant-numeric is tabular-nums (D62). Table's numeric columns bind the token directly rather than via the utility class.",
|
|
3174
3497
|
"sectionHeader": "SectionHeader ships as a recipe, not a component (v1.2 non-goal): baseline-aligned flex row — mono annotation (--psi-text-mono-14-20-regular, --psi-fg-accent) + h2 (.psi-display-32-32-extrabold) + optional trailing meta, border-bottom 1px var(--psi-border-faint), padding-bottom var(--psi-space-20)."
|
|
3175
3498
|
},
|
|
3176
3499
|
"tags": {
|
|
@@ -3320,6 +3643,7 @@
|
|
|
3320
3643
|
"gutterNarrow": 24
|
|
3321
3644
|
},
|
|
3322
3645
|
"zIndex": {
|
|
3646
|
+
"sticky": 1,
|
|
3323
3647
|
"nav": 100,
|
|
3324
3648
|
"overlay": 1000,
|
|
3325
3649
|
"tooltip": 1100
|
package/dist/store.js
CHANGED
|
@@ -3,28 +3,32 @@ const MAX_RESULTS = 20;
|
|
|
3
3
|
* (~1.5k tokens)" (docs/superpowers/plans/2026-07-18-agent-access-plan.md),
|
|
4
4
|
* asserted by __tests__/store.test.ts's "stays within the response budget". */
|
|
5
5
|
const RESPONSE_BUDGET = 6000;
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
6
|
+
/** How the empty-query overview divides its budget across kinds. Topics,
|
|
7
|
+
* patterns, and components grow and cost very differently:
|
|
8
8
|
*
|
|
9
9
|
* topics — a fixed, slow-growing set of terse briefs (~90 chars each).
|
|
10
10
|
* Carries the rules an agent cannot guess, `getting-started`
|
|
11
|
-
* (the five required CSS imports) above all.
|
|
12
|
-
*
|
|
13
|
-
* phrases for keyword ranking and can run long, so the
|
|
14
|
-
* overview projects a trimmed copy (see overviewBrief) capped
|
|
15
|
-
* near topic size — the stored, untrimmed brief is still what
|
|
16
|
-
* score() ranks against.
|
|
11
|
+
* (the five required CSS imports) above all. Takes its full
|
|
12
|
+
* cost, unconditionally.
|
|
17
13
|
* components — verbose (up to 220 chars each) and the only axis that grows
|
|
18
14
|
* with every release. Also the axis a keyword query recovers
|
|
19
15
|
* best: search("menu") finds Menu; no query re-derives a house
|
|
20
|
-
* rule an agent doesn't know exists.
|
|
16
|
+
* rule an agent doesn't know exists. Reserved a floor of
|
|
17
|
+
* `COMPONENT_FLOOR` items before patterns spend the remainder,
|
|
18
|
+
* so catalog growth cannot starve them below the floor.
|
|
19
|
+
* patterns — composition recipes; their stored summaries carry match
|
|
20
|
+
* phrases for keyword ranking and can run long, so the overview
|
|
21
|
+
* projects a trimmed copy (see overviewBrief) at a *derived*
|
|
22
|
+
* per-call cap — the largest that lets every pattern fit
|
|
23
|
+
* alongside topics and the component floor. The stored,
|
|
24
|
+
* untrimmed brief is still what score() ranks against.
|
|
21
25
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
const
|
|
26
|
+
* Previously the overview filled topics, then patterns, then components
|
|
27
|
+
* until the budget ran out, so whichever kind was filled last absorbed all
|
|
28
|
+
* catalog growth — three more patterns was enough to starve components from
|
|
29
|
+
* six down to four (D61). Allocating by kind first means catalog growth
|
|
30
|
+
* shortens pattern summaries instead of evicting items off the tail. */
|
|
31
|
+
const COMPONENT_FLOOR = 8;
|
|
28
32
|
const TOPIC_SUMMARIES = {
|
|
29
33
|
variants: "Variant intent and typical use for all 8 flat variants",
|
|
30
34
|
rules: "House rules: accent budget, sizing, color binding",
|
|
@@ -61,12 +65,11 @@ function patternSummary(p) {
|
|
|
61
65
|
summary += `, blocked (gaps: ${p.gaps.join(", ")})`;
|
|
62
66
|
return summary;
|
|
63
67
|
}
|
|
64
|
-
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* shapes a copy used when search("") projects the overview list.
|
|
68
|
+
/** Most an overview pattern intro (intent + match phrases + parameter count)
|
|
69
|
+
* may cost. The effective limit is derived per-call (see the empty-query
|
|
70
|
+
* branch): patterns take whatever the budget leaves after topics and the
|
|
71
|
+
* component floor, so a growing catalog shortens summaries instead of
|
|
72
|
+
* evicting items (D61).
|
|
70
73
|
*
|
|
71
74
|
* The `, blocked (gaps: …)` tail is never subject to this cap: it is the
|
|
72
75
|
* backlog signal this cycle exists to publish, and patternSummary() puts it
|
|
@@ -75,7 +78,7 @@ function patternSummary(p) {
|
|
|
75
78
|
* 120 chars), which is exactly the bug this trims to avoid. So the overview
|
|
76
79
|
* projects a pattern's summary from the underlying PatternEntry directly:
|
|
77
80
|
* trim the intro, then always append the tail. */
|
|
78
|
-
const
|
|
81
|
+
const OVERVIEW_PATTERN_INTRO_MAX = 120;
|
|
79
82
|
/** Trims `s` to at most `limit` chars, backing off to the last word boundary
|
|
80
83
|
* and marking the cut with an ellipsis — never a mid-word fragment that
|
|
81
84
|
* could read as a complete (if coincidental) match phrase. */
|
|
@@ -86,8 +89,8 @@ function truncateAtWord(s, limit) {
|
|
|
86
89
|
const lastSpace = cut.lastIndexOf(" ");
|
|
87
90
|
return `${lastSpace > 0 ? cut.slice(0, lastSpace) : cut}…`;
|
|
88
91
|
}
|
|
89
|
-
function overviewPatternSummary(p) {
|
|
90
|
-
const intro = truncateAtWord(`${p.intent} — ${p.match.join(", ")}, ${p.parameters.length} parameters`,
|
|
92
|
+
function overviewPatternSummary(p, limit) {
|
|
93
|
+
const intro = truncateAtWord(`${p.intent} — ${p.match.join(", ")}, ${p.parameters.length} parameters`, limit);
|
|
91
94
|
return p.blocked ? `${intro}, blocked (gaps: ${p.gaps.join(", ")})` : intro;
|
|
92
95
|
}
|
|
93
96
|
export function createStore(index) {
|
|
@@ -98,14 +101,16 @@ export function createStore(index) {
|
|
|
98
101
|
* already-assembled, full-length `Brief.summary` — so the `blocked (gaps:
|
|
99
102
|
* …)` tail always survives the overview's trim. Always a shallow copy for
|
|
100
103
|
* patterns: the stored brief kept in `briefs` (and used by score()) is
|
|
101
|
-
* never mutated.
|
|
102
|
-
|
|
104
|
+
* never mutated. `limit` is derived per-call by the empty-query branch
|
|
105
|
+
* (D61), not a module constant, so a growing catalog shortens summaries
|
|
106
|
+
* instead of evicting items. */
|
|
107
|
+
function overviewBrief(b, limit) {
|
|
103
108
|
if (b.kind !== "pattern")
|
|
104
109
|
return b;
|
|
105
110
|
const pattern = patternsById.get(b.title);
|
|
106
111
|
if (!pattern)
|
|
107
112
|
return b;
|
|
108
|
-
return { ...b, summary: overviewPatternSummary(pattern) };
|
|
113
|
+
return { ...b, summary: overviewPatternSummary(pattern, limit) };
|
|
109
114
|
}
|
|
110
115
|
const briefs = [
|
|
111
116
|
...index.components.map((c) => ({
|
|
@@ -150,14 +155,25 @@ export function createStore(index) {
|
|
|
150
155
|
function search(query) {
|
|
151
156
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
152
157
|
if (terms.length === 0) {
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
// D61: allocate before filling. Topics take their full cost, components
|
|
159
|
+
// are reserved a floor, and patterns take the remainder via a derived
|
|
160
|
+
// cap — so catalog growth shortens summaries instead of evicting the
|
|
161
|
+
// tail. Whichever kind was filled last used to absorb all growth;
|
|
162
|
+
// that was the bug. (Tokens are omitted; they are discoverable via
|
|
163
|
+
// query.)
|
|
164
|
+
const topics = briefs.filter((b) => b.kind === "topic");
|
|
165
|
+
const patterns = briefs.filter((b) => b.kind === "pattern");
|
|
166
|
+
const components = briefs.filter((b) => b.kind === "component");
|
|
167
|
+
const reserve = JSON.stringify(components.slice(0, COMPONENT_FLOOR)).length;
|
|
168
|
+
const fits = (limit) => JSON.stringify([...topics, ...patterns.map((b) => overviewBrief(b, limit))]).length +
|
|
169
|
+
reserve <=
|
|
170
|
+
RESPONSE_BUDGET;
|
|
171
|
+
let limit = OVERVIEW_PATTERN_INTRO_MAX;
|
|
172
|
+
while (limit > 0 && !fits(limit))
|
|
173
|
+
limit -= 10;
|
|
174
|
+
const out = [...topics, ...patterns.map((b) => overviewBrief(b, limit))];
|
|
175
|
+
for (const component of components) {
|
|
176
|
+
out.push(component);
|
|
161
177
|
if (JSON.stringify(out).length > RESPONSE_BUDGET) {
|
|
162
178
|
out.pop();
|
|
163
179
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handamade/psi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.10.0",
|
|
37
|
+
"@handamade/psi-react": "0.10.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"
|