@handamade/psi-mcp 0.11.0 → 0.13.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 +243 -27
- 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 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
|
|
package/dist/psi-index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.13.0",
|
|
3
3
|
"themes": [
|
|
4
4
|
"light",
|
|
5
5
|
"dark",
|
|
@@ -378,7 +378,7 @@
|
|
|
378
378
|
},
|
|
379
379
|
{
|
|
380
380
|
"name": "Dialog",
|
|
381
|
-
"description": "Modal dialog on the native <dialog> top layer: focus trap, aria-modal and focus restore come from the platform; title/footer slots, Esc/backdrop/ close-button dismissal via onClose(reason) (D50).",
|
|
381
|
+
"description": "Modal dialog on the native <dialog> top layer: focus trap, aria-modal and focus restore come from the platform; title/footer slots, Esc/backdrop/ close-button dismissal via onClose(reason) (D50). `placement` moves the panel and nothing else (D66): an `inline-start` / `inline-end` Dialog is a drawer, pinned full-height to that edge, with the same modality, focus trap, focus restore and dismissal reasons as a centered one. There is deliberately no separate Drawer component — a sibling would have to duplicate or wrap all of that.",
|
|
382
382
|
"props": [
|
|
383
383
|
{
|
|
384
384
|
"name": "open",
|
|
@@ -413,7 +413,14 @@
|
|
|
413
413
|
"type": "400 | 560 | 720",
|
|
414
414
|
"required": false,
|
|
415
415
|
"default": 560,
|
|
416
|
-
"description": "Panel width in px (400 | 560 | 720)."
|
|
416
|
+
"description": "Panel width in px (400 | 560 | 720). For an `inline-*` placement this is\nthe drawer's width; the height is always the full viewport."
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"name": "placement",
|
|
420
|
+
"type": "\"center\" | \"inline-start\" | \"inline-end\"",
|
|
421
|
+
"required": false,
|
|
422
|
+
"default": "center",
|
|
423
|
+
"description": "Where the panel sits. `inline-start` / `inline-end` pin it full-height to\nthat edge, making it a drawer (D66)."
|
|
417
424
|
},
|
|
418
425
|
{
|
|
419
426
|
"name": "dismissible",
|
|
@@ -468,7 +475,7 @@
|
|
|
468
475
|
"order": 3
|
|
469
476
|
}
|
|
470
477
|
],
|
|
471
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Dialog\n\nModal dialog on the native <dialog> top layer: focus trap, aria-modal and focus restore come from the platform; title/footer slots, Esc/backdrop/ close-button dismissal via onClose(reason) (D50).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `open` | `boolean` | — | yes | Controlled open state; syncs to showModal()/close(). |\n| `onClose` | `(reason: \"esc\" \\| \"backdrop\" \\| \"close-button\") => void` | — | yes | Called on every dismissal attempt with its source; the consumer flips `open`. |\n| `title` | `ReactNode` | — | no | Heading; renders an <h2> wired to aria-labelledby. Without it, pass aria-label. |\n| `footer` | `ReactNode` | — | no | Action row (Buttons — one accent per group, danger for destructive). |\n| `width` | `400 \\| 560 \\| 720` | 560 | no | Panel width in px (400 \\| 560 \\| 720). |\n| `dismissible` | `boolean` | true | no | false = no close button, Esc and backdrop swallowed — footer is the only exit. |\n| `ref` | `Ref<HTMLDialogElement>` | — | no | Forwarded ref to the underlying <dialog> element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Esc | Dismisses via onClose('esc') when dismissible; swallowed otherwise. |\n| Tab | Focus is trapped inside by the native <dialog> top layer; restored on close. |\n\nRendered with showModal(): aria-modal, inert background and focus restore come from the platform. title wires aria-labelledby; without title, pass aria-label. Backdrop click dismisses only when dismissible.\n\n## Theming\n\nOverride `--psi-dialog-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
478
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Dialog\n\nModal dialog on the native <dialog> top layer: focus trap, aria-modal and focus restore come from the platform; title/footer slots, Esc/backdrop/ close-button dismissal via onClose(reason) (D50). `placement` moves the panel and nothing else (D66): an `inline-start` / `inline-end` Dialog is a drawer, pinned full-height to that edge, with the same modality, focus trap, focus restore and dismissal reasons as a centered one. There is deliberately no separate Drawer component — a sibling would have to duplicate or wrap all of that.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `open` | `boolean` | — | yes | Controlled open state; syncs to showModal()/close(). |\n| `onClose` | `(reason: \"esc\" \\| \"backdrop\" \\| \"close-button\") => void` | — | yes | Called on every dismissal attempt with its source; the consumer flips `open`. |\n| `title` | `ReactNode` | — | no | Heading; renders an <h2> wired to aria-labelledby. Without it, pass aria-label. |\n| `footer` | `ReactNode` | — | no | Action row (Buttons — one accent per group, danger for destructive). |\n| `width` | `400 \\| 560 \\| 720` | 560 | no | Panel width in px (400 \\| 560 \\| 720). For an `inline-*` placement this is the drawer's width; the height is always the full viewport. |\n| `placement` | `\"center\" \\| \"inline-start\" \\| \"inline-end\"` | center | no | Where the panel sits. `inline-start` / `inline-end` pin it full-height to that edge, making it a drawer (D66). |\n| `dismissible` | `boolean` | true | no | false = no close button, Esc and backdrop swallowed — footer is the only exit. |\n| `ref` | `Ref<HTMLDialogElement>` | — | no | Forwarded ref to the underlying <dialog> element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Esc | Dismisses via onClose('esc') when dismissible; swallowed otherwise. |\n| Tab | Focus is trapped inside by the native <dialog> top layer; restored on close. |\n\nRendered with showModal(): aria-modal, inert background and focus restore come from the platform. title wires aria-labelledby; without title, pass aria-label. Backdrop click dismisses only when dismissible. placement=\"inline-start\"/\"inline-end\" pins the panel full-height to that edge — that is Psi's drawer, and it changes nothing about modality, the focus trap, focus restore or the dismissal reasons (D66). A drawer's panel scrolls internally so a dismissible={false} footer stays reachable.\n\n## Theming\n\nOverride `--psi-dialog-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
|
|
472
479
|
},
|
|
473
480
|
{
|
|
474
481
|
"name": "Checkbox",
|
|
@@ -1242,6 +1249,183 @@
|
|
|
1242
1249
|
],
|
|
1243
1250
|
"slots": [],
|
|
1244
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"
|
|
1245
1429
|
}
|
|
1246
1430
|
],
|
|
1247
1431
|
"tokens": [
|
|
@@ -3088,9 +3272,9 @@
|
|
|
3088
3272
|
"record detail panel"
|
|
3089
3273
|
],
|
|
3090
3274
|
"compose": {
|
|
3091
|
-
"component": "
|
|
3275
|
+
"component": "Dialog",
|
|
3092
3276
|
"props": {
|
|
3093
|
-
"
|
|
3277
|
+
"placement": "{param:placement}"
|
|
3094
3278
|
},
|
|
3095
3279
|
"slots": {
|
|
3096
3280
|
"title": [
|
|
@@ -3119,13 +3303,13 @@
|
|
|
3119
3303
|
},
|
|
3120
3304
|
"parameters": [
|
|
3121
3305
|
{
|
|
3122
|
-
"key": "
|
|
3306
|
+
"key": "placement",
|
|
3123
3307
|
"ask": "Which edge should the drawer open from?",
|
|
3124
3308
|
"options": [
|
|
3125
|
-
"end",
|
|
3126
|
-
"start"
|
|
3309
|
+
"inline-end",
|
|
3310
|
+
"inline-start"
|
|
3127
3311
|
],
|
|
3128
|
-
"default": "end"
|
|
3312
|
+
"default": "inline-end"
|
|
3129
3313
|
}
|
|
3130
3314
|
],
|
|
3131
3315
|
"content": {
|
|
@@ -3134,11 +3318,9 @@
|
|
|
3134
3318
|
"primary-label": "Save",
|
|
3135
3319
|
"dismiss-label": "Close"
|
|
3136
3320
|
},
|
|
3137
|
-
"gaps": [
|
|
3138
|
-
|
|
3139
|
-
]
|
|
3140
|
-
"blocked": true,
|
|
3141
|
-
"preset": null
|
|
3321
|
+
"gaps": [],
|
|
3322
|
+
"blocked": false,
|
|
3323
|
+
"preset": "<Dialog\n footer={<>\n <Button variant=\"accent\">Save</Button>\n <Button variant=\"neutral\">Close</Button>\n </>}\n placement=\"inline-end\"\n title=\"[record title]\"\n>\n [key-value summary of the selected record]\n</Dialog>\n"
|
|
3142
3324
|
},
|
|
3143
3325
|
{
|
|
3144
3326
|
"id": "empty-state",
|
|
@@ -3382,11 +3564,45 @@
|
|
|
3382
3564
|
"orientation": "{param:orientation}"
|
|
3383
3565
|
},
|
|
3384
3566
|
"slots": {
|
|
3385
|
-
"
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
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
|
+
}
|
|
3390
3606
|
]
|
|
3391
3607
|
}
|
|
3392
3608
|
},
|
|
@@ -3402,14 +3618,14 @@
|
|
|
3402
3618
|
}
|
|
3403
3619
|
],
|
|
3404
3620
|
"content": {
|
|
3405
|
-
"
|
|
3406
|
-
"
|
|
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]"
|
|
3407
3625
|
},
|
|
3408
|
-
"gaps": [
|
|
3409
|
-
|
|
3410
|
-
],
|
|
3411
|
-
"blocked": true,
|
|
3412
|
-
"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"
|
|
3413
3629
|
},
|
|
3414
3630
|
{
|
|
3415
3631
|
"id": "table-pagination",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handamade/psi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.13.0",
|
|
37
|
+
"@handamade/psi-react": "0.13.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"
|