@handamade/psi-mcp 0.12.0 → 0.13.1

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 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.
3
+ MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the 32 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.12.0",
2
+ "version": "0.13.1",
3
3
  "themes": [
4
4
  "light",
5
5
  "dark",
@@ -1249,6 +1249,183 @@
1249
1249
  ],
1250
1250
  "slots": [],
1251
1251
  "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"
1252
+ },
1253
+ {
1254
+ "name": "Tabs",
1255
+ "description": "Tab set root (D67) — holds no selection state of its own, following D50, D53 and D62: `value` and `onValueChange` are required and there is no `defaultValue`. Values are strings rather than indices because an index breaks the moment a tab is inserted, and real tab sets map to ids. `Tab` and `TabPanel` pair by value, so their source order need not match. Activation is automatic: arrow keys move focus and selection together. That is the APG default where panel content is already available, and manual activation is deliberately not offered as a mode — a consumer for whom activating a panel is expensive already controls what the panel renders.",
1256
+ "props": [
1257
+ {
1258
+ "name": "value",
1259
+ "type": "string",
1260
+ "required": true,
1261
+ "default": null,
1262
+ "description": "Controlled selected tab, matched against each `Tab`/`TabPanel` value."
1263
+ },
1264
+ {
1265
+ "name": "onValueChange",
1266
+ "type": "(value: string) => void",
1267
+ "required": true,
1268
+ "default": null,
1269
+ "description": "Fires with the newly selected value; the consumer flips `value`."
1270
+ },
1271
+ {
1272
+ "name": "orientation",
1273
+ "type": "\"horizontal\" | \"vertical\"",
1274
+ "required": false,
1275
+ "default": "horizontal",
1276
+ "description": "Axis of the tab list, which also picks the arrow keys."
1277
+ },
1278
+ {
1279
+ "name": "children",
1280
+ "type": "ReactNode",
1281
+ "required": true,
1282
+ "default": null,
1283
+ "description": "A `TabList` and one `TabPanel` per tab."
1284
+ },
1285
+ {
1286
+ "name": "className",
1287
+ "type": "string",
1288
+ "required": false,
1289
+ "default": null,
1290
+ "description": "Additional CSS class name(s) merged onto the component's root element."
1291
+ },
1292
+ {
1293
+ "name": "ref",
1294
+ "type": "Ref<HTMLDivElement>",
1295
+ "required": false,
1296
+ "default": null,
1297
+ "description": "Forwarded ref to the wrapper `<div>`."
1298
+ }
1299
+ ],
1300
+ "slots": [
1301
+ {
1302
+ "name": "body",
1303
+ "accepts": {
1304
+ "components": [
1305
+ "TabList",
1306
+ "TabPanel"
1307
+ ]
1308
+ },
1309
+ "cardinality": "1..*",
1310
+ "order": 1
1311
+ }
1312
+ ],
1313
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tabs\n\nTab set root (D67) — holds no selection state of its own, following D50, D53 and D62: `value` and `onValueChange` are required and there is no `defaultValue`. Values are strings rather than indices because an index breaks the moment a tab is inserted, and real tab sets map to ids. `Tab` and `TabPanel` pair by value, so their source order need not match. Activation is automatic: arrow keys move focus and selection together. That is the APG default where panel content is already available, and manual activation is deliberately not offered as a mode — a consumer for whom activating a panel is expensive already controls what the panel renders.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | Controlled selected tab, matched against each `Tab`/`TabPanel` value. |\n| `onValueChange` | `(value: string) => void` | — | yes | Fires with the newly selected value; the consumer flips `value`. |\n| `orientation` | `\"horizontal\" \\| \"vertical\"` | horizontal | no | Axis of the tab list, which also picks the arrow keys. |\n| `children` | `ReactNode` | — | yes | A `TabList` and one `TabPanel` per tab. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the wrapper `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Enters the tab list at its selected tab (one stop for the whole list), then moves on to the active panel. |\n\nControlled-only (D67): value and onValueChange are required and Tabs never selects itself. Tab and TabPanel pair by string value, not index, so their source order need not match. Every panel renders and unselected ones carry `hidden`, so aria-controls always resolves and panel DOM state survives a switch.\n\n## Theming\n\nOverride `--psi-tabs-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
1314
+ },
1315
+ {
1316
+ "name": "TabList",
1317
+ "description": "The `role=\"tablist\"` container (D67). Owns the roving tabindex: the whole list is one Tab stop, and the arrow keys move between tabs inside it.",
1318
+ "props": [
1319
+ {
1320
+ "name": "aria-label",
1321
+ "type": "string",
1322
+ "required": false,
1323
+ "default": null,
1324
+ "description": "Accessible name for the tab set. Declared here rather than inherited so\ndocgen keeps it in the manifest (D60) — a tablist without a name is\nannounced as an unlabelled group, and the prop would otherwise be\ninvisible to an agent reading the manifest."
1325
+ },
1326
+ {
1327
+ "name": "children",
1328
+ "type": "ReactNode",
1329
+ "required": true,
1330
+ "default": null,
1331
+ "description": "One `Tab` per view."
1332
+ },
1333
+ {
1334
+ "name": "className",
1335
+ "type": "string",
1336
+ "required": false,
1337
+ "default": null,
1338
+ "description": "Additional CSS class name(s) merged onto the component's root element."
1339
+ },
1340
+ {
1341
+ "name": "ref",
1342
+ "type": "Ref<HTMLDivElement>",
1343
+ "required": false,
1344
+ "default": null,
1345
+ "description": "Forwarded ref to the tablist element."
1346
+ }
1347
+ ],
1348
+ "slots": [],
1349
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TabList\n\nThe `role=\"tablist\"` container (D67). Owns the roving tabindex: the whole list is one Tab stop, and the arrow keys move between tabs inside it.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `aria-label` | `string` | — | no | Accessible name for the tab set. Declared here rather than inherited so docgen keeps it in the manifest (D60) — a tablist without a name is announced as an unlabelled group, and the prop would otherwise be invisible to an agent reading the manifest. |\n| `children` | `ReactNode` | — | yes | One `Tab` per view. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the tablist element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Arrow Left / Arrow Right | Horizontal orientation: moves to the previous/next enabled tab, wrapping at both ends. Selection follows focus (automatic activation). |\n| Arrow Up / Arrow Down | Vertical orientation: the same, on the block axis. The cross-axis arrows are ignored rather than swallowed. |\n| Home / End | Jumps to the first or last enabled tab. |\n\nRenders role=\"tablist\" with aria-orientation. Requires an accessible name — pass aria-label, which is promoted onto its own props interface (D60) so the manifest shows it. Owns the roving tabindex: exactly one tab is in the page tab order at a time.\n\n## Theming\n\nThis component has no `--psi-tab-list-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
1350
+ },
1351
+ {
1352
+ "name": "Tab",
1353
+ "description": "One `role=\"tab\"` (D67). Renders a real `<button>`. `disabled` sets `aria-disabled` rather than the `disabled` attribute, so the tab stays discoverable to assistive tech while being skipped by roving navigation — the same choice MenuItem made in D53.",
1354
+ "props": [
1355
+ {
1356
+ "name": "value",
1357
+ "type": "string",
1358
+ "required": true,
1359
+ "default": null,
1360
+ "description": "Pairs this tab with the `TabPanel` of the same value."
1361
+ },
1362
+ {
1363
+ "name": "children",
1364
+ "type": "ReactNode",
1365
+ "required": true,
1366
+ "default": null,
1367
+ "description": "Label — keep it to one or two words."
1368
+ },
1369
+ {
1370
+ "name": "disabled",
1371
+ "type": "boolean",
1372
+ "required": false,
1373
+ "default": false,
1374
+ "description": "Skipped by arrow navigation and not selectable, but still announced."
1375
+ },
1376
+ {
1377
+ "name": "className",
1378
+ "type": "string",
1379
+ "required": false,
1380
+ "default": null,
1381
+ "description": "Additional CSS class name(s) merged onto the component's root element."
1382
+ },
1383
+ {
1384
+ "name": "ref",
1385
+ "type": "Ref<HTMLButtonElement>",
1386
+ "required": false,
1387
+ "default": null,
1388
+ "description": "Forwarded ref to the underlying `<button>`."
1389
+ }
1390
+ ],
1391
+ "slots": [],
1392
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tab\n\nOne `role=\"tab\"` (D67). Renders a real `<button>`. `disabled` sets `aria-disabled` rather than the `disabled` attribute, so the tab stays discoverable to assistive tech while being skipped by roving navigation — the same choice MenuItem made in D53.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | Pairs this tab with the `TabPanel` of the same value. |\n| `children` | `ReactNode` | — | yes | Label — keep it to one or two words. |\n| `disabled` | `boolean` | false | no | Skipped by arrow navigation and not selectable, but still announced. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLButtonElement>` | — | no | Forwarded ref to the underlying `<button>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Enter / Space | Activates (native button behavior); arrow keys already select on focus. |\n\nRenders a real <button> with role=\"tab\", aria-selected and aria-controls. disabled sets aria-disabled (not the disabled attribute) so the tab stays discoverable to assistive tech while being skipped by roving navigation and refused for selection — the same choice MenuItem made in D53.\n\n## Theming\n\nThis component has no `--psi-tab-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
1393
+ },
1394
+ {
1395
+ "name": "TabPanel",
1396
+ "description": "One `role=\"tabpanel\"` (D67). Every panel renders; unselected ones carry `hidden`. Returning null instead would leave `aria-controls` on every unselected tab pointing at an element that does not exist, and would throw away DOM state — a half-filled form in a panel would lose its values on each tab switch. The cost is a heavier DOM when panels are large; the answer to that is rendering less inside the panel, not unmounting it behind the consumer's back. `tabIndex={0}` because a panel whose content has no focusable element would otherwise be unreachable by keyboard.",
1397
+ "props": [
1398
+ {
1399
+ "name": "value",
1400
+ "type": "string",
1401
+ "required": true,
1402
+ "default": null,
1403
+ "description": "Pairs this panel with the `Tab` of the same value."
1404
+ },
1405
+ {
1406
+ "name": "children",
1407
+ "type": "ReactNode",
1408
+ "required": true,
1409
+ "default": null,
1410
+ "description": "The view for this tab. Rendered even when unselected, then `hidden`."
1411
+ },
1412
+ {
1413
+ "name": "className",
1414
+ "type": "string",
1415
+ "required": false,
1416
+ "default": null,
1417
+ "description": "Additional CSS class name(s) merged onto the component's root element."
1418
+ },
1419
+ {
1420
+ "name": "ref",
1421
+ "type": "Ref<HTMLDivElement>",
1422
+ "required": false,
1423
+ "default": null,
1424
+ "description": "Forwarded ref to the panel element."
1425
+ }
1426
+ ],
1427
+ "slots": [],
1428
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# TabPanel\n\nOne `role=\"tabpanel\"` (D67). Every panel renders; unselected ones carry `hidden`. Returning null instead would leave `aria-controls` on every unselected tab pointing at an element that does not exist, and would throw away DOM state — a half-filled form in a panel would lose its values on each tab switch. The cost is a heavier DOM when panels are large; the answer to that is rendering less inside the panel, not unmounting it behind the consumer's back. `tabIndex={0}` because a panel whose content has no focusable element would otherwise be unreachable by keyboard.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `value` | `string` | — | yes | Pairs this panel with the `Tab` of the same value. |\n| `children` | `ReactNode` | — | yes | The view for this tab. Rendered even when unselected, then `hidden`. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the panel element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | The panel itself is a tab stop (tabIndex=0), so a panel whose content has no focusable element is still reachable. |\n\nRenders role=\"tabpanel\" with aria-labelledby pointing back at its tab. Unselected panels stay in the DOM with `hidden`.\n\n## Theming\n\nThis component has no `--psi-tab-panel-*` tokens — its styling binds scale tokens only, so there is nothing component-scoped to override. Theme changes reach it through the semantic tokens of its children and surroundings.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
1252
1429
  }
1253
1430
  ],
1254
1431
  "tokens": [
@@ -3387,11 +3564,45 @@
3387
3564
  "orientation": "{param:orientation}"
3388
3565
  },
3389
3566
  "slots": {
3390
- "list": [
3391
- "{content:tabs-note}"
3392
- ],
3393
- "panels": [
3394
- "{content:panels-note}"
3567
+ "body": [
3568
+ {
3569
+ "component": "TabList",
3570
+ "props": {
3571
+ "aria-label": "Views"
3572
+ },
3573
+ "slots": {
3574
+ "body": [
3575
+ {
3576
+ "component": "Tab",
3577
+ "props": {
3578
+ "value": "all"
3579
+ },
3580
+ "content": "first-label"
3581
+ },
3582
+ {
3583
+ "component": "Tab",
3584
+ "props": {
3585
+ "value": "flagged"
3586
+ },
3587
+ "content": "second-label"
3588
+ }
3589
+ ]
3590
+ }
3591
+ },
3592
+ {
3593
+ "component": "TabPanel",
3594
+ "props": {
3595
+ "value": "all"
3596
+ },
3597
+ "content": "first-panel"
3598
+ },
3599
+ {
3600
+ "component": "TabPanel",
3601
+ "props": {
3602
+ "value": "flagged"
3603
+ },
3604
+ "content": "second-panel"
3605
+ }
3395
3606
  ]
3396
3607
  }
3397
3608
  },
@@ -3407,14 +3618,14 @@
3407
3618
  }
3408
3619
  ],
3409
3620
  "content": {
3410
- "tabs-note": "[one tab per view; keep labels to one or two words]",
3411
- "panels-note": "[one panel per tab, in the same order]"
3621
+ "first-label": "All",
3622
+ "second-label": "Flagged",
3623
+ "first-panel": "[the default view]",
3624
+ "second-panel": "[the second view; one panel per tab, paired by value]"
3412
3625
  },
3413
- "gaps": [
3414
- "Tabs"
3415
- ],
3416
- "blocked": true,
3417
- "preset": null
3626
+ "gaps": [],
3627
+ "blocked": false,
3628
+ "preset": "<Tabs\n orientation=\"horizontal\"\n>\n <TabList\n aria-label=\"Views\"\n >\n <Tab value=\"all\">All</Tab>\n <Tab value=\"flagged\">Flagged</Tab>\n </TabList>\n <TabPanel value=\"all\">[the default view]</TabPanel>\n <TabPanel value=\"flagged\">[the second view; one panel per tab, paired by value]</TabPanel>\n</Tabs>\n"
3418
3629
  },
3419
3630
  {
3420
3631
  "id": "table-pagination",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handamade/psi-mcp",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
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.12.0",
37
- "@handamade/psi-react": "0.12.0"
36
+ "@handamade/psi-tokens": "0.13.1",
37
+ "@handamade/psi-react": "0.13.1"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"