@handamade/psi-mcp 0.9.1 → 0.11.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 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 18 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.
3
+ MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the 28 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
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.9.1",
2
+ "version": "0.11.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,412 @@
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"
1089
+ },
1090
+ {
1091
+ "name": "Toast",
1092
+ "description": "A single transient notification card (D64). Controlled and presentational: it holds no state, runs no timer, and never removes itself — `onDismiss` reports and the owner disposes, exactly as Dialog (D50) and Menu (D53) do. Toast does not carry its own `role`/`aria-live`. Politeness belongs to the two persistent wrappers inside ToastRegion, because a live region must pre-exist the content it announces; a role on the toast itself would reintroduce the mount-with-content problem one level down. Composed by hand with ToastRegion for full control, or driven by ToastProvider's queue.",
1093
+ "props": [
1094
+ {
1095
+ "name": "variant",
1096
+ "type": "\"neutral\" | \"success\" | \"warning\" | \"danger\"",
1097
+ "required": false,
1098
+ "default": "neutral",
1099
+ "description": "Status variant."
1100
+ },
1101
+ {
1102
+ "name": "children",
1103
+ "type": "ReactNode",
1104
+ "required": true,
1105
+ "default": null,
1106
+ "description": "The message."
1107
+ },
1108
+ {
1109
+ "name": "action",
1110
+ "type": "ReactNode",
1111
+ "required": false,
1112
+ "default": null,
1113
+ "description": "Trailing affordance — a ghost Button (Undo, Retry, View)."
1114
+ },
1115
+ {
1116
+ "name": "onDismiss",
1117
+ "type": "(() => void)",
1118
+ "required": false,
1119
+ "default": null,
1120
+ "description": "When provided, renders the dismiss button and calls this. Toast does not\nremove itself; the owner disposes."
1121
+ },
1122
+ {
1123
+ "name": "className",
1124
+ "type": "string",
1125
+ "required": false,
1126
+ "default": null,
1127
+ "description": "Additional CSS class name(s) merged onto the component's root element."
1128
+ },
1129
+ {
1130
+ "name": "ref",
1131
+ "type": "Ref<HTMLDivElement>",
1132
+ "required": false,
1133
+ "default": null,
1134
+ "description": "Forwarded ref to the underlying `<div>`."
1135
+ }
1136
+ ],
1137
+ "slots": [
1138
+ {
1139
+ "name": "body",
1140
+ "accepts": {},
1141
+ "cardinality": "1..*",
1142
+ "order": 1
1143
+ },
1144
+ {
1145
+ "name": "action",
1146
+ "accepts": {
1147
+ "components": [
1148
+ "Button"
1149
+ ],
1150
+ "contracts": [
1151
+ "inline-content"
1152
+ ]
1153
+ },
1154
+ "cardinality": "0..1",
1155
+ "order": 2
1156
+ }
1157
+ ],
1158
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Toast\n\nA single transient notification card (D64). Controlled and presentational: it holds no state, runs no timer, and never removes itself — `onDismiss` reports and the owner disposes, exactly as Dialog (D50) and Menu (D53) do. Toast does not carry its own `role`/`aria-live`. Politeness belongs to the two persistent wrappers inside ToastRegion, because a live region must pre-exist the content it announces; a role on the toast itself would reintroduce the mount-with-content problem one level down. Composed by hand with ToastRegion for full control, or driven by ToastProvider's queue.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"neutral\" \\| \"success\" \\| \"warning\" \\| \"danger\"` | neutral | no | Status variant. |\n| `children` | `ReactNode` | — | yes | The message. |\n| `action` | `ReactNode` | — | no | Trailing affordance — a ghost Button (Undo, Retry, View). |\n| `onDismiss` | `(() => void)` | — | no | When provided, renders the dismiss button and calls this. Toast does not remove itself; the owner disposes. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the underlying `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Reaches the action and the dismiss button in DOM order. Esc is not a dismissal — a toast is not modal and traps nothing. |\n| Enter / Space | Activates the focused action or dismiss button. |\n\nPresentational and controlled (D64): onDismiss reports and the owner disposes; Toast never removes itself. It carries no role/aria-live of its own — politeness belongs to ToastRegion's two persistent wrappers. The variant's meaning is announced by a visually hidden status word (\"Success:\", \"Warning:\", \"Error:\"), never by colour and icon shape alone; the icon is aria-hidden. neutral has no status and gets no prefix. The dismiss button requires no props — it is labelled \"Dismiss notification\".\n\n## Theming\n\nOverride `--psi-toast-*` custom properties at any scope.\n\n## Variant guidance\n\n- **accent** — Primary action, draws attention. Submit, CTA, main action in a group.\n- **accent-subtle** — Accent tone, lower visual weight. Selected state, active filter, soft CTA.\n- **neutral** — Default, structurally present. Secondary actions, toolbar buttons.\n- **neutral-subtle** — Minimal chrome. Inline actions, table row actions.\n- **ghost** — No visible container until hover. Icon-only triggers, compact toolbars.\n- **outline** — Bordered ghost — visible structure, no fill until hover. Marketing CTA, download button; hover fills accent.\n- **danger** — Destructive action. Delete, remove, disconnect.\n- **danger-subtle** — Destructive context, low urgency. Warning badges, soft destructive hints.\n- **success | warning** — Status communication (Tag only). Status badges, labels.\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
1159
+ },
1160
+ {
1161
+ "name": "ToastRegion",
1162
+ "description": "The positioned live region that holds the toast stack (D64). Two things here are load-bearing and both look like details: 1. **The two live wrappers are always rendered**, empty queue included. A live region announces mutations to a subtree that already existed; a wrapper that mounts together with its first toast reads as a new subtree, and that first toast is never announced. 2. **`popover=\"manual\"`, not `\"auto\"`.** Manual puts the region in the native top layer without light dismiss. The top layer is required because Dialog uses showModal() — also top layer — so a fixed region at --psi-z-overlay would paint *under* the modal backdrop, hiding the confirmation for the action a user just took inside a dialog. And `auto` would be dismissed by the very click that raised the toast. Routing reads `variant` off each child, the same Children.map technique Table uses for select-all injection. Consequence of the split, visible in the `InRegion` VR baseline: the stack is grouped by politeness, not strictly chronological — every assertive toast sorts below every polite one regardless of arrival order. Chronological order is preserved *within* each group. Keeping it exact across both would mean one live region with a politeness that changes per message, which is the thing the two wrappers exist to avoid. Accepted: at `limit` 3 the grouping reads as severity ordering, and the newest toast still lands nearest the screen edge within its group.",
1163
+ "props": [
1164
+ {
1165
+ "name": "placement",
1166
+ "type": "\"top-start\" | \"top-end\" | \"bottom-start\" | \"bottom-end\"",
1167
+ "required": false,
1168
+ "default": "bottom-end",
1169
+ "description": "Corner the stack occupies."
1170
+ },
1171
+ {
1172
+ "name": "aria-label",
1173
+ "type": "string",
1174
+ "required": false,
1175
+ "default": "Notifications",
1176
+ "description": "Accessible name for the region."
1177
+ },
1178
+ {
1179
+ "name": "children",
1180
+ "type": "ReactNode",
1181
+ "required": true,
1182
+ "default": null,
1183
+ "description": "The toast stack — `Toast` elements, routed to a live wrapper by variant."
1184
+ },
1185
+ {
1186
+ "name": "className",
1187
+ "type": "string",
1188
+ "required": false,
1189
+ "default": null,
1190
+ "description": "Additional CSS class name(s) merged onto the component's root element."
1191
+ },
1192
+ {
1193
+ "name": "ref",
1194
+ "type": "Ref<HTMLDivElement>",
1195
+ "required": false,
1196
+ "default": null,
1197
+ "description": "Forwarded ref to the region element."
1198
+ }
1199
+ ],
1200
+ "slots": [],
1201
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# ToastRegion\n\nThe positioned live region that holds the toast stack (D64). Two things here are load-bearing and both look like details: 1. **The two live wrappers are always rendered**, empty queue included. A live region announces mutations to a subtree that already existed; a wrapper that mounts together with its first toast reads as a new subtree, and that first toast is never announced. 2. **`popover=\"manual\"`, not `\"auto\"`.** Manual puts the region in the native top layer without light dismiss. The top layer is required because Dialog uses showModal() — also top layer — so a fixed region at --psi-z-overlay would paint *under* the modal backdrop, hiding the confirmation for the action a user just took inside a dialog. And `auto` would be dismissed by the very click that raised the toast. Routing reads `variant` off each child, the same Children.map technique Table uses for select-all injection. Consequence of the split, visible in the `InRegion` VR baseline: the stack is grouped by politeness, not strictly chronological — every assertive toast sorts below every polite one regardless of arrival order. Chronological order is preserved *within* each group. Keeping it exact across both would mean one live region with a politeness that changes per message, which is the thing the two wrappers exist to avoid. Accepted: at `limit` 3 the grouping reads as severity ordering, and the newest toast still lands nearest the screen edge within its group.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `placement` | `\"top-start\" \\| \"top-end\" \\| \"bottom-start\" \\| \"bottom-end\"` | bottom-end | no | Corner the stack occupies. |\n| `aria-label` | `string` | Notifications | no | Accessible name for the region. |\n| `children` | `ReactNode` | — | yes | The toast stack — `Toast` elements, routed to a live wrapper by variant. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the region element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Moves into the stacked toasts' controls in DOM order; the region itself is not focusable. |\n\nRenders two always-present live wrappers — role=\"status\"/aria-live=\"polite\" and role=\"alert\"/aria-live=\"assertive\" — and routes each toast into one by variant (neutral/success polite, warning/danger assertive). Both stay in the DOM when the queue is empty: a live region announces mutations to a subtree that already existed, so a wrapper mounting with its first toast would leave that toast unannounced. Sits on the native top layer via popover=\"manual\", so a toast raised from inside a modal Dialog is still painted above the backdrop and still announced — though showModal() makes everything outside the dialog inert, so it cannot be clicked until the dialog closes; manual (not auto) means no light dismiss, so the click that raised the toast cannot close it. The region is click-through (pointer-events: none) and each toast takes its own clicks back.\n\n## Theming\n\nThis component has no `--psi-toast-region-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
1202
+ },
1203
+ {
1204
+ "name": "ToastProvider",
1205
+ "description": "Owns the toast queue, its auto-dismiss timers, and the single ToastRegion (D65). This is the library's one stateful container, and the exception is deliberately narrow. D50 and D53 rejected internal state for Dialog and Menu because the consumer already owned the state that decided visibility — a menu is open because a user clicked a trigger the consumer rendered. A toast has no such owner: it is created by an outcome, not by a UI state, and it disappears on a timer nobody is watching. The rule that survives is the useful half — presentational components stay controlled (`Toast` still holds nothing), a stateful container may exist when the state has no natural owner, and it must be opt-in. Timers pause while the pointer or focus is inside the region (WCAG 2.2.1), and resume with the time *remaining* rather than a fresh full duration — restarting would let a user hold a toast open indefinitely by jiggling the mouse.",
1206
+ "props": [
1207
+ {
1208
+ "name": "limit",
1209
+ "type": "number",
1210
+ "required": false,
1211
+ "default": 3,
1212
+ "description": "Max simultaneous toasts; the oldest is evicted first."
1213
+ },
1214
+ {
1215
+ "name": "duration",
1216
+ "type": "number",
1217
+ "required": false,
1218
+ "default": 5000,
1219
+ "description": "Auto-dismiss for toasts with no action, in ms."
1220
+ },
1221
+ {
1222
+ "name": "actionDuration",
1223
+ "type": "number",
1224
+ "required": false,
1225
+ "default": 10000,
1226
+ "description": "Auto-dismiss for toasts carrying an action, in ms. An affordance that\nvanishes before it can be reached is not an affordance."
1227
+ },
1228
+ {
1229
+ "name": "placement",
1230
+ "type": "\"top-start\" | \"top-end\" | \"bottom-start\" | \"bottom-end\"",
1231
+ "required": false,
1232
+ "default": "bottom-end",
1233
+ "description": "Corner the stack occupies."
1234
+ },
1235
+ {
1236
+ "name": "children",
1237
+ "type": "ReactNode",
1238
+ "required": true,
1239
+ "default": null,
1240
+ "description": "The subtree that may call `useToast()`. The region is rendered alongside it."
1241
+ }
1242
+ ],
1243
+ "slots": [],
1244
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# ToastProvider\n\nOwns the toast queue, its auto-dismiss timers, and the single ToastRegion (D65). This is the library's one stateful container, and the exception is deliberately narrow. D50 and D53 rejected internal state for Dialog and Menu because the consumer already owned the state that decided visibility — a menu is open because a user clicked a trigger the consumer rendered. A toast has no such owner: it is created by an outcome, not by a UI state, and it disappears on a timer nobody is watching. The rule that survives is the useful half — presentational components stay controlled (`Toast` still holds nothing), a stateful container may exist when the state has no natural owner, and it must be opt-in. Timers pause while the pointer or focus is inside the region (WCAG 2.2.1), and resume with the time *remaining* rather than a fresh full duration — restarting would let a user hold a toast open indefinitely by jiggling the mouse.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `limit` | `number` | 3 | no | Max simultaneous toasts; the oldest is evicted first. |\n| `duration` | `number` | 5000 | no | Auto-dismiss for toasts with no action, in ms. |\n| `actionDuration` | `number` | 10000 | no | Auto-dismiss for toasts carrying an action, in ms. An affordance that vanishes before it can be reached is not an affordance. |\n| `placement` | `\"top-start\" \\| \"top-end\" \\| \"bottom-start\" \\| \"bottom-end\"` | bottom-end | no | Corner the stack occupies. |\n| `children` | `ReactNode` | — | yes | The subtree that may call `useToast()`. The region is rendered alongside it. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focus entering the region pauses every auto-dismiss timer; leaving resumes them. |\n\nOwns the queue, the auto-dismiss timers and the single ToastRegion (D65). Timers pause while the pointer or focus is inside the region and resume with the time remaining, satisfying WCAG 2.2.1 for content that disappears on a timer. Toasts carrying an action get a longer default lifetime, so the affordance cannot vanish before it is reached. useToast() throws outside a provider rather than silently no-opping.\n\n## Theming\n\nThis component has no `--psi-toast-provider-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
832
1245
  }
833
1246
  ],
834
1247
  "tokens": [
@@ -2366,6 +2779,7 @@
2366
2779
  "ask": "What kind of outcome is being confirmed?",
2367
2780
  "options": [
2368
2781
  "success",
2782
+ "warning",
2369
2783
  "danger",
2370
2784
  "neutral"
2371
2785
  ],
@@ -2376,11 +2790,9 @@
2376
2790
  "message": "[what just happened]",
2377
2791
  "undo-label": "Undo"
2378
2792
  },
2379
- "gaps": [
2380
- "Toast"
2381
- ],
2382
- "blocked": true,
2383
- "preset": null
2793
+ "gaps": [],
2794
+ "blocked": false,
2795
+ "preset": "<Toast action={<Button variant=\"ghost\">Undo</Button>} variant=\"success\">[what just happened]</Toast>\n"
2384
2796
  },
2385
2797
  {
2386
2798
  "id": "bulk-action-bar",
@@ -2458,24 +2870,90 @@
2458
2870
  "sortable": true
2459
2871
  },
2460
2872
  "slots": {
2461
- "head": [
2462
- "{content:columns-note}"
2463
- ],
2464
2873
  "body": [
2465
- "{content:rows-note}"
2874
+ {
2875
+ "component": "TableHead",
2876
+ "slots": {
2877
+ "body": [
2878
+ {
2879
+ "component": "TableRow",
2880
+ "slots": {
2881
+ "body": [
2882
+ {
2883
+ "component": "TableHeaderCell",
2884
+ "props": {
2885
+ "sortKey": "date"
2886
+ },
2887
+ "content": "date-label"
2888
+ },
2889
+ {
2890
+ "component": "TableHeaderCell",
2891
+ "props": {
2892
+ "sortKey": "payee"
2893
+ },
2894
+ "content": "payee-label"
2895
+ },
2896
+ {
2897
+ "component": "TableHeaderCell",
2898
+ "props": {
2899
+ "sortKey": "amount",
2900
+ "numeric": true
2901
+ },
2902
+ "content": "amount-label"
2903
+ }
2904
+ ]
2905
+ }
2906
+ }
2907
+ ]
2908
+ }
2909
+ },
2910
+ {
2911
+ "component": "TableBody",
2912
+ "slots": {
2913
+ "body": [
2914
+ {
2915
+ "component": "TableRow",
2916
+ "props": {
2917
+ "rowId": "row-id"
2918
+ },
2919
+ "slots": {
2920
+ "body": [
2921
+ {
2922
+ "component": "TableCell",
2923
+ "content": "date-cell"
2924
+ },
2925
+ {
2926
+ "component": "TableCell",
2927
+ "content": "payee-cell"
2928
+ },
2929
+ {
2930
+ "component": "TableCell",
2931
+ "props": {
2932
+ "numeric": true
2933
+ },
2934
+ "content": "amount-cell"
2935
+ }
2936
+ ]
2937
+ }
2938
+ }
2939
+ ]
2940
+ }
2941
+ }
2466
2942
  ]
2467
2943
  }
2468
2944
  },
2469
2945
  "parameters": [],
2470
2946
  "content": {
2471
- "columns-note": "[one header cell per column; numeric columns align right]",
2472
- "rows-note": "[one row per record; put the row-actions menu in the last cell]"
2947
+ "date-label": "Date",
2948
+ "payee-label": "Payee",
2949
+ "amount-label": "Amount",
2950
+ "date-cell": "[one row per record]",
2951
+ "payee-cell": "[put the row-actions menu in the last cell]",
2952
+ "amount-cell": "[numeric columns align right and use tabular figures]"
2473
2953
  },
2474
- "gaps": [
2475
- "Table"
2476
- ],
2477
- "blocked": true,
2478
- "preset": null
2954
+ "gaps": [],
2955
+ "blocked": false,
2956
+ "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
2957
  },
2480
2958
  {
2481
2959
  "id": "date-range-filter",
@@ -2976,11 +3454,9 @@
2976
3454
  "content": {
2977
3455
  "page-size-label": "Rows per page"
2978
3456
  },
2979
- "gaps": [
2980
- "Pagination"
2981
- ],
2982
- "blocked": true,
2983
- "preset": null
3457
+ "gaps": [],
3458
+ "blocked": false,
3459
+ "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
3460
  }
2985
3461
  ],
2986
3462
  "scales": {
@@ -3040,6 +3516,7 @@
3040
3516
  "gutterNarrow": 24
3041
3517
  },
3042
3518
  "zIndex": {
3519
+ "sticky": 1,
3043
3520
  "nav": 100,
3044
3521
  "overlay": 1000,
3045
3522
  "tooltip": 1100
@@ -3171,6 +3648,7 @@
3171
3648
  },
3172
3649
  "recipes": {
3173
3650
  "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.",
3651
+ "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
3652
  "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
3653
  },
3176
3654
  "tags": {
@@ -3320,6 +3798,7 @@
3320
3798
  "gutterNarrow": 24
3321
3799
  },
3322
3800
  "zIndex": {
3801
+ "sticky": 1,
3323
3802
  "nav": 100,
3324
3803
  "overlay": 1000,
3325
3804
  "tooltip": 1100
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handamade/psi-mcp",
3
- "version": "0.9.1",
3
+ "version": "0.11.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.9.1",
37
- "@handamade/psi-react": "0.9.1"
36
+ "@handamade/psi-tokens": "0.11.0",
37
+ "@handamade/psi-react": "0.11.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"