@handamade/psi-mcp 0.8.1 → 0.9.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 +472 -8
- package/dist/store.js +51 -2
- package/llms.txt +1 -1
- 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 18 React 19 components,
|
|
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.
|
|
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.9.0",
|
|
3
3
|
"themes": [
|
|
4
4
|
"light",
|
|
5
5
|
"dark",
|
|
@@ -68,6 +68,13 @@
|
|
|
68
68
|
"name": "IconButton",
|
|
69
69
|
"description": "Square icon-only button; same variant/size/href contract as Button. Give it an accessible name (`aria-label`).",
|
|
70
70
|
"props": [
|
|
71
|
+
{
|
|
72
|
+
"name": "aria-label",
|
|
73
|
+
"type": "string",
|
|
74
|
+
"required": true,
|
|
75
|
+
"default": null,
|
|
76
|
+
"description": "Accessible name for the icon-only control. Required: an IconButton has\nno text to name it, so omitting this ships an unnameable button (D60).\nIf you also pass `aria-labelledby`, that wins per the accname algorithm."
|
|
77
|
+
},
|
|
71
78
|
{
|
|
72
79
|
"name": "variant",
|
|
73
80
|
"type": "\"accent\" | \"neutral\" | \"ghost\" | \"danger\"",
|
|
@@ -119,7 +126,7 @@
|
|
|
119
126
|
}
|
|
120
127
|
],
|
|
121
128
|
"slots": [],
|
|
122
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# IconButton\n\nSquare icon-only button; same variant/size/href contract as Button. Give it an accessible name (`aria-label`).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"accent\" \\| \"neutral\" \\| \"ghost\" \\| \"danger\"` | neutral | no | Visual variant. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Square size in px (24 \\| 32 \\| 40 \\| 48). |\n| `href` | `string` | — | no | Render as an anchor with this href (D33). disabled → aria-disabled, no href attribute, pointer-events: none. |\n| `target` | `string` | — | no | Anchor target; only used with href. |\n| `rel` | `string` | — | no | Anchor rel; only used with href. |\n| `ref` | `Ref<HTMLButtonElement \\| HTMLAnchorElement>` | — | no | Forwarded ref to the underlying 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| Enter / Space | Activates. |\n\nRequires an accessible name — pass aria-label. IconButton does not hide its icon for you; mark the icon aria-hidden yourself.\n\n## Theming\n\nOverride `--psi-button-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\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"
|
|
129
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# IconButton\n\nSquare icon-only button; same variant/size/href contract as Button. Give it an accessible name (`aria-label`).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `aria-label` | `string` | — | yes | Accessible name for the icon-only control. Required: an IconButton has no text to name it, so omitting this ships an unnameable button (D60). If you also pass `aria-labelledby`, that wins per the accname algorithm. |\n| `variant` | `\"accent\" \\| \"neutral\" \\| \"ghost\" \\| \"danger\"` | neutral | no | Visual variant. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Square size in px (24 \\| 32 \\| 40 \\| 48). |\n| `href` | `string` | — | no | Render as an anchor with this href (D33). disabled → aria-disabled, no href attribute, pointer-events: none. |\n| `target` | `string` | — | no | Anchor target; only used with href. |\n| `rel` | `string` | — | no | Anchor rel; only used with href. |\n| `ref` | `Ref<HTMLButtonElement \\| HTMLAnchorElement>` | — | no | Forwarded ref to the underlying 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| Enter / Space | Activates. |\n\nRequires an accessible name — pass aria-label. IconButton does not hide its icon for you; mark the icon aria-hidden yourself.\n\n## Theming\n\nOverride `--psi-button-*` custom properties at any scope; interactive states derive automatically (L - 0.04 hover, L - 0.08 active).\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"
|
|
123
130
|
},
|
|
124
131
|
{
|
|
125
132
|
"name": "Card",
|
|
@@ -204,6 +211,13 @@
|
|
|
204
211
|
"name": "Input",
|
|
205
212
|
"description": "Single-line text input with pixel-true heights (24–48) and an error state. Inside a Field, id/aria-describedby/aria-invalid/required are wired automatically (D49).",
|
|
206
213
|
"props": [
|
|
214
|
+
{
|
|
215
|
+
"name": "type",
|
|
216
|
+
"type": "\"number\" | \"text\" | \"search\" | \"email\" | \"tel\" | \"url\" | \"password\" | \"date\"",
|
|
217
|
+
"required": false,
|
|
218
|
+
"default": "text",
|
|
219
|
+
"description": "Input type."
|
|
220
|
+
},
|
|
207
221
|
{
|
|
208
222
|
"name": "size",
|
|
209
223
|
"type": "24 | 32 | 40 | 48",
|
|
@@ -241,7 +255,7 @@
|
|
|
241
255
|
}
|
|
242
256
|
],
|
|
243
257
|
"slots": [],
|
|
244
|
-
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Input\n\nSingle-line text input with pixel-true heights (24–48) and an error state. Inside a Field, id/aria-describedby/aria-invalid/required are wired automatically (D49).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height in px (24 \\| 32 \\| 40 \\| 48). |\n| `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input>` element. |\n| `placeholder` | `string` | — | no | Native placeholder text shown while the field is empty (only on components whose host element supports it). |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focuses the native <input>; focus ring on the field. |\n\nerror sets a red border only. Inside a Field, aria-invalid and aria-describedby are wired automatically (D49); standalone, pair them yourself.\n\n## Theming\n\nOverride `--psi-input-*` 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"
|
|
258
|
+
"doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Input\n\nSingle-line text input with pixel-true heights (24–48) and an error state. Inside a Field, id/aria-describedby/aria-invalid/required are wired automatically (D49).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `type` | `\"number\" \\| \"text\" \\| \"search\" \\| \"email\" \\| \"tel\" \\| \"url\" \\| \"password\" \\| \"date\"` | text | no | Input type. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height in px (24 \\| 32 \\| 40 \\| 48). |\n| `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input>` element. |\n| `placeholder` | `string` | — | no | Native placeholder text shown while the field is empty (only on components whose host element supports it). |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | Focuses the native <input>; focus ring on the field. |\n\nerror sets a red border only. Inside a Field, aria-invalid and aria-describedby are wired automatically (D49); standalone, pair them yourself.\n\n## Theming\n\nOverride `--psi-input-*` 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"
|
|
245
259
|
},
|
|
246
260
|
{
|
|
247
261
|
"name": "Select",
|
|
@@ -2316,6 +2330,216 @@
|
|
|
2316
2330
|
}
|
|
2317
2331
|
],
|
|
2318
2332
|
"patterns": [
|
|
2333
|
+
{
|
|
2334
|
+
"id": "action-feedback",
|
|
2335
|
+
"intent": "Transient confirmation after a completed action, with an optional undo affordance",
|
|
2336
|
+
"match": [
|
|
2337
|
+
"toast",
|
|
2338
|
+
"snackbar",
|
|
2339
|
+
"action feedback",
|
|
2340
|
+
"confirmation message",
|
|
2341
|
+
"undo toast"
|
|
2342
|
+
],
|
|
2343
|
+
"compose": {
|
|
2344
|
+
"component": "Toast",
|
|
2345
|
+
"props": {
|
|
2346
|
+
"variant": "{param:variant}"
|
|
2347
|
+
},
|
|
2348
|
+
"slots": {
|
|
2349
|
+
"body": [
|
|
2350
|
+
"{content:message}"
|
|
2351
|
+
],
|
|
2352
|
+
"action": [
|
|
2353
|
+
{
|
|
2354
|
+
"component": "Button",
|
|
2355
|
+
"props": {
|
|
2356
|
+
"variant": "ghost"
|
|
2357
|
+
},
|
|
2358
|
+
"content": "undo-label"
|
|
2359
|
+
}
|
|
2360
|
+
]
|
|
2361
|
+
}
|
|
2362
|
+
},
|
|
2363
|
+
"parameters": [
|
|
2364
|
+
{
|
|
2365
|
+
"key": "variant",
|
|
2366
|
+
"ask": "What kind of outcome is being confirmed?",
|
|
2367
|
+
"options": [
|
|
2368
|
+
"success",
|
|
2369
|
+
"danger",
|
|
2370
|
+
"neutral"
|
|
2371
|
+
],
|
|
2372
|
+
"default": "success"
|
|
2373
|
+
}
|
|
2374
|
+
],
|
|
2375
|
+
"content": {
|
|
2376
|
+
"message": "[what just happened]",
|
|
2377
|
+
"undo-label": "Undo"
|
|
2378
|
+
},
|
|
2379
|
+
"gaps": [
|
|
2380
|
+
"Toast"
|
|
2381
|
+
],
|
|
2382
|
+
"blocked": true,
|
|
2383
|
+
"preset": null
|
|
2384
|
+
},
|
|
2385
|
+
{
|
|
2386
|
+
"id": "bulk-action-bar",
|
|
2387
|
+
"intent": "Bar that appears when table rows are selected: selection count, bulk actions, and a clear-selection control",
|
|
2388
|
+
"match": [
|
|
2389
|
+
"bulk actions",
|
|
2390
|
+
"selection bar",
|
|
2391
|
+
"batch actions",
|
|
2392
|
+
"selected rows toolbar"
|
|
2393
|
+
],
|
|
2394
|
+
"compose": {
|
|
2395
|
+
"component": "Toolbar",
|
|
2396
|
+
"props": {
|
|
2397
|
+
"gap": 12
|
|
2398
|
+
},
|
|
2399
|
+
"slots": {
|
|
2400
|
+
"body": [
|
|
2401
|
+
{
|
|
2402
|
+
"component": "Tag",
|
|
2403
|
+
"props": {
|
|
2404
|
+
"variant": "accent",
|
|
2405
|
+
"subtle": true
|
|
2406
|
+
},
|
|
2407
|
+
"content": "count-label"
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
"component": "Button",
|
|
2411
|
+
"props": {
|
|
2412
|
+
"variant": "neutral"
|
|
2413
|
+
},
|
|
2414
|
+
"content": "export-label"
|
|
2415
|
+
},
|
|
2416
|
+
{
|
|
2417
|
+
"component": "Button",
|
|
2418
|
+
"props": {
|
|
2419
|
+
"variant": "danger-subtle"
|
|
2420
|
+
},
|
|
2421
|
+
"content": "delete-label"
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
"component": "Button",
|
|
2425
|
+
"props": {
|
|
2426
|
+
"variant": "ghost"
|
|
2427
|
+
},
|
|
2428
|
+
"content": "clear-label"
|
|
2429
|
+
}
|
|
2430
|
+
]
|
|
2431
|
+
}
|
|
2432
|
+
},
|
|
2433
|
+
"parameters": [],
|
|
2434
|
+
"content": {
|
|
2435
|
+
"count-label": "[n] selected",
|
|
2436
|
+
"export-label": "Export",
|
|
2437
|
+
"delete-label": "Delete",
|
|
2438
|
+
"clear-label": "Clear selection"
|
|
2439
|
+
},
|
|
2440
|
+
"gaps": [],
|
|
2441
|
+
"blocked": false,
|
|
2442
|
+
"preset": "<Toolbar\n gap={12}\n>\n <Tag subtle={true} variant=\"accent\">[n] selected</Tag>\n <Button variant=\"neutral\">Export</Button>\n <Button variant=\"danger-subtle\">Delete</Button>\n <Button variant=\"ghost\">Clear selection</Button>\n</Toolbar>\n"
|
|
2443
|
+
},
|
|
2444
|
+
{
|
|
2445
|
+
"id": "data-table",
|
|
2446
|
+
"intent": "Sortable, selectable table of records with numeric columns and a per-row actions menu — compose the row menu from the row-actions pattern",
|
|
2447
|
+
"match": [
|
|
2448
|
+
"data table",
|
|
2449
|
+
"transactions table",
|
|
2450
|
+
"sortable table",
|
|
2451
|
+
"records table",
|
|
2452
|
+
"list of transactions"
|
|
2453
|
+
],
|
|
2454
|
+
"compose": {
|
|
2455
|
+
"component": "Table",
|
|
2456
|
+
"props": {
|
|
2457
|
+
"selectable": true,
|
|
2458
|
+
"sortable": true
|
|
2459
|
+
},
|
|
2460
|
+
"slots": {
|
|
2461
|
+
"head": [
|
|
2462
|
+
"{content:columns-note}"
|
|
2463
|
+
],
|
|
2464
|
+
"body": [
|
|
2465
|
+
"{content:rows-note}"
|
|
2466
|
+
]
|
|
2467
|
+
}
|
|
2468
|
+
},
|
|
2469
|
+
"parameters": [],
|
|
2470
|
+
"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]"
|
|
2473
|
+
},
|
|
2474
|
+
"gaps": [
|
|
2475
|
+
"Table"
|
|
2476
|
+
],
|
|
2477
|
+
"blocked": true,
|
|
2478
|
+
"preset": null
|
|
2479
|
+
},
|
|
2480
|
+
{
|
|
2481
|
+
"id": "date-range-filter",
|
|
2482
|
+
"intent": "From/to date pair for narrowing a dated list — native date inputs, no calendar widget",
|
|
2483
|
+
"match": [
|
|
2484
|
+
"date range filter",
|
|
2485
|
+
"from to date",
|
|
2486
|
+
"date filter",
|
|
2487
|
+
"period filter",
|
|
2488
|
+
"filter by date"
|
|
2489
|
+
],
|
|
2490
|
+
"compose": {
|
|
2491
|
+
"component": "Toolbar",
|
|
2492
|
+
"props": {
|
|
2493
|
+
"gap": 12
|
|
2494
|
+
},
|
|
2495
|
+
"slots": {
|
|
2496
|
+
"body": [
|
|
2497
|
+
{
|
|
2498
|
+
"component": "Field",
|
|
2499
|
+
"slots": {
|
|
2500
|
+
"label": [
|
|
2501
|
+
"{content:from-label}"
|
|
2502
|
+
],
|
|
2503
|
+
"body": [
|
|
2504
|
+
{
|
|
2505
|
+
"component": "Input",
|
|
2506
|
+
"props": {
|
|
2507
|
+
"type": "date",
|
|
2508
|
+
"size": 32
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
]
|
|
2512
|
+
}
|
|
2513
|
+
},
|
|
2514
|
+
{
|
|
2515
|
+
"component": "Field",
|
|
2516
|
+
"slots": {
|
|
2517
|
+
"label": [
|
|
2518
|
+
"{content:to-label}"
|
|
2519
|
+
],
|
|
2520
|
+
"body": [
|
|
2521
|
+
{
|
|
2522
|
+
"component": "Input",
|
|
2523
|
+
"props": {
|
|
2524
|
+
"type": "date",
|
|
2525
|
+
"size": 32
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
]
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
]
|
|
2532
|
+
}
|
|
2533
|
+
},
|
|
2534
|
+
"parameters": [],
|
|
2535
|
+
"content": {
|
|
2536
|
+
"from-label": "From",
|
|
2537
|
+
"to-label": "To"
|
|
2538
|
+
},
|
|
2539
|
+
"gaps": [],
|
|
2540
|
+
"blocked": false,
|
|
2541
|
+
"preset": "<Toolbar\n gap={12}\n>\n <Field\n label=\"From\"\n >\n <Input size={32} type=\"date\" />\n </Field>\n <Field\n label=\"To\"\n >\n <Input size={32} type=\"date\" />\n </Field>\n</Toolbar>\n"
|
|
2542
|
+
},
|
|
2319
2543
|
{
|
|
2320
2544
|
"id": "destructive-confirm",
|
|
2321
2545
|
"intent": "Dialog confirming a destructive action",
|
|
@@ -2375,6 +2599,108 @@
|
|
|
2375
2599
|
"blocked": false,
|
|
2376
2600
|
"preset": "<Dialog\n footer={<>\n <Button size={32} variant=\"ghost\">Cancel</Button>\n <Button size={32} variant=\"danger\">[verb the object]</Button>\n </>}\n title=\"[Delete the object?]\"\n>\n [what is permanently lost]\n</Dialog>\n"
|
|
2377
2601
|
},
|
|
2602
|
+
{
|
|
2603
|
+
"id": "detail-drawer",
|
|
2604
|
+
"intent": "Side sheet showing one record's detail, opened from a table row, with its primary actions in a footer",
|
|
2605
|
+
"match": [
|
|
2606
|
+
"detail drawer",
|
|
2607
|
+
"side sheet",
|
|
2608
|
+
"row detail",
|
|
2609
|
+
"slide-over",
|
|
2610
|
+
"record detail panel"
|
|
2611
|
+
],
|
|
2612
|
+
"compose": {
|
|
2613
|
+
"component": "Drawer",
|
|
2614
|
+
"props": {
|
|
2615
|
+
"side": "{param:side}"
|
|
2616
|
+
},
|
|
2617
|
+
"slots": {
|
|
2618
|
+
"title": [
|
|
2619
|
+
"{content:title}"
|
|
2620
|
+
],
|
|
2621
|
+
"body": [
|
|
2622
|
+
"{content:body-note}"
|
|
2623
|
+
],
|
|
2624
|
+
"footer": [
|
|
2625
|
+
{
|
|
2626
|
+
"component": "Button",
|
|
2627
|
+
"props": {
|
|
2628
|
+
"variant": "accent"
|
|
2629
|
+
},
|
|
2630
|
+
"content": "primary-label"
|
|
2631
|
+
},
|
|
2632
|
+
{
|
|
2633
|
+
"component": "Button",
|
|
2634
|
+
"props": {
|
|
2635
|
+
"variant": "neutral"
|
|
2636
|
+
},
|
|
2637
|
+
"content": "dismiss-label"
|
|
2638
|
+
}
|
|
2639
|
+
]
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2642
|
+
"parameters": [
|
|
2643
|
+
{
|
|
2644
|
+
"key": "side",
|
|
2645
|
+
"ask": "Which edge should the drawer open from?",
|
|
2646
|
+
"options": [
|
|
2647
|
+
"end",
|
|
2648
|
+
"start"
|
|
2649
|
+
],
|
|
2650
|
+
"default": "end"
|
|
2651
|
+
}
|
|
2652
|
+
],
|
|
2653
|
+
"content": {
|
|
2654
|
+
"title": "[record title]",
|
|
2655
|
+
"body-note": "[key-value summary of the selected record]",
|
|
2656
|
+
"primary-label": "Save",
|
|
2657
|
+
"dismiss-label": "Close"
|
|
2658
|
+
},
|
|
2659
|
+
"gaps": [
|
|
2660
|
+
"Drawer"
|
|
2661
|
+
],
|
|
2662
|
+
"blocked": true,
|
|
2663
|
+
"preset": null
|
|
2664
|
+
},
|
|
2665
|
+
{
|
|
2666
|
+
"id": "empty-state",
|
|
2667
|
+
"intent": "Placeholder shown when a list has no results, distinguishing an empty filter from an empty dataset",
|
|
2668
|
+
"match": [
|
|
2669
|
+
"empty state",
|
|
2670
|
+
"no results",
|
|
2671
|
+
"nothing here",
|
|
2672
|
+
"zero state",
|
|
2673
|
+
"no data"
|
|
2674
|
+
],
|
|
2675
|
+
"compose": {
|
|
2676
|
+
"component": "Panel",
|
|
2677
|
+
"props": {
|
|
2678
|
+
"padding": 24
|
|
2679
|
+
},
|
|
2680
|
+
"slots": {
|
|
2681
|
+
"body": [
|
|
2682
|
+
"{content:headline}",
|
|
2683
|
+
"{content:explanation}",
|
|
2684
|
+
{
|
|
2685
|
+
"component": "Button",
|
|
2686
|
+
"props": {
|
|
2687
|
+
"variant": "neutral"
|
|
2688
|
+
},
|
|
2689
|
+
"content": "reset-label"
|
|
2690
|
+
}
|
|
2691
|
+
]
|
|
2692
|
+
}
|
|
2693
|
+
},
|
|
2694
|
+
"parameters": [],
|
|
2695
|
+
"content": {
|
|
2696
|
+
"headline": "[nothing matches these filters]",
|
|
2697
|
+
"explanation": "[say whether the dataset is empty or the filters are too narrow]",
|
|
2698
|
+
"reset-label": "Clear filters"
|
|
2699
|
+
},
|
|
2700
|
+
"gaps": [],
|
|
2701
|
+
"blocked": false,
|
|
2702
|
+
"preset": "<Panel\n padding={24}\n>\n [nothing matches these filters]\n [say whether the dataset is empty or the filters are too narrow]\n <Button variant=\"neutral\">Clear filters</Button>\n</Panel>\n"
|
|
2703
|
+
},
|
|
2378
2704
|
{
|
|
2379
2705
|
"id": "filter-toolbar",
|
|
2380
2706
|
"intent": "Toolbar of filter controls: search, category select, active-filter tags",
|
|
@@ -2411,7 +2737,7 @@
|
|
|
2411
2737
|
},
|
|
2412
2738
|
{
|
|
2413
2739
|
"id": "row-actions",
|
|
2414
|
-
"intent": "List or table row with
|
|
2740
|
+
"intent": "List or table row with an icon-only actions menu, including a destructive item — hand the destructive item's confirmation off to the destructive-confirm pattern",
|
|
2415
2741
|
"match": [
|
|
2416
2742
|
"row actions",
|
|
2417
2743
|
"per-row actions",
|
|
@@ -2428,11 +2754,12 @@
|
|
|
2428
2754
|
"slots": {
|
|
2429
2755
|
"trigger": [
|
|
2430
2756
|
{
|
|
2431
|
-
"component": "
|
|
2757
|
+
"component": "IconButton",
|
|
2432
2758
|
"props": {
|
|
2433
|
-
"variant": "ghost"
|
|
2759
|
+
"variant": "ghost",
|
|
2760
|
+
"aria-label": "{content:trigger-label}"
|
|
2434
2761
|
},
|
|
2435
|
-
"content": "trigger-
|
|
2762
|
+
"content": "trigger-icon"
|
|
2436
2763
|
}
|
|
2437
2764
|
],
|
|
2438
2765
|
"body": [
|
|
@@ -2470,13 +2797,14 @@
|
|
|
2470
2797
|
],
|
|
2471
2798
|
"content": {
|
|
2472
2799
|
"trigger-label": "Actions",
|
|
2800
|
+
"trigger-icon": "[icon]",
|
|
2473
2801
|
"edit-label": "Edit",
|
|
2474
2802
|
"duplicate-label": "Duplicate",
|
|
2475
2803
|
"delete-label": "Delete"
|
|
2476
2804
|
},
|
|
2477
2805
|
"gaps": [],
|
|
2478
2806
|
"blocked": false,
|
|
2479
|
-
"preset": "<Menu\n aria-label=\"Actions\"\n placement=\"bottom-end\"\n trigger={<
|
|
2807
|
+
"preset": "<Menu\n aria-label=\"Actions\"\n placement=\"bottom-end\"\n trigger={<IconButton aria-label=\"Actions\" variant=\"ghost\">[icon]</IconButton>}\n>\n <MenuItem>Edit</MenuItem>\n <MenuItem>Duplicate</MenuItem>\n <MenuSeparator />\n <MenuItem variant=\"danger\">Delete</MenuItem>\n</Menu>\n"
|
|
2480
2808
|
},
|
|
2481
2809
|
{
|
|
2482
2810
|
"id": "settings-form-row",
|
|
@@ -2517,6 +2845,142 @@
|
|
|
2517
2845
|
"gaps": [],
|
|
2518
2846
|
"blocked": false,
|
|
2519
2847
|
"preset": "<Field\n description=\"[one-line consequence of the setting]\"\n group={true}\n label=\"[setting name]\"\n>\n <Switch>[what turning it on does]</Switch>\n</Field>\n"
|
|
2848
|
+
},
|
|
2849
|
+
{
|
|
2850
|
+
"id": "summary-tiles",
|
|
2851
|
+
"intent": "Row of headline figures above a ledger — balances or totals, one Card per figure",
|
|
2852
|
+
"match": [
|
|
2853
|
+
"summary tiles",
|
|
2854
|
+
"stat cards",
|
|
2855
|
+
"kpi row",
|
|
2856
|
+
"balance cards",
|
|
2857
|
+
"totals row"
|
|
2858
|
+
],
|
|
2859
|
+
"compose": {
|
|
2860
|
+
"component": "Toolbar",
|
|
2861
|
+
"props": {
|
|
2862
|
+
"gap": 16
|
|
2863
|
+
},
|
|
2864
|
+
"slots": {
|
|
2865
|
+
"body": [
|
|
2866
|
+
{
|
|
2867
|
+
"component": "Card",
|
|
2868
|
+
"props": {
|
|
2869
|
+
"variant": "stacked"
|
|
2870
|
+
},
|
|
2871
|
+
"content": "tile-note"
|
|
2872
|
+
},
|
|
2873
|
+
{
|
|
2874
|
+
"component": "Card",
|
|
2875
|
+
"props": {
|
|
2876
|
+
"variant": "stacked"
|
|
2877
|
+
},
|
|
2878
|
+
"content": "tile-note"
|
|
2879
|
+
}
|
|
2880
|
+
]
|
|
2881
|
+
}
|
|
2882
|
+
},
|
|
2883
|
+
"parameters": [],
|
|
2884
|
+
"content": {
|
|
2885
|
+
"tile-note": "[label above, figure below]"
|
|
2886
|
+
},
|
|
2887
|
+
"gaps": [],
|
|
2888
|
+
"blocked": false,
|
|
2889
|
+
"preset": "<Toolbar\n gap={16}\n>\n <Card variant=\"stacked\">[label above, figure below]</Card>\n <Card variant=\"stacked\">[label above, figure below]</Card>\n</Toolbar>\n"
|
|
2890
|
+
},
|
|
2891
|
+
{
|
|
2892
|
+
"id": "tabbed-workspace",
|
|
2893
|
+
"intent": "Tab set switching between sibling views of the same workspace, such as accounts or saved filters",
|
|
2894
|
+
"match": [
|
|
2895
|
+
"tabs",
|
|
2896
|
+
"tabbed view",
|
|
2897
|
+
"switch between accounts",
|
|
2898
|
+
"tab bar",
|
|
2899
|
+
"segmented views"
|
|
2900
|
+
],
|
|
2901
|
+
"compose": {
|
|
2902
|
+
"component": "Tabs",
|
|
2903
|
+
"props": {
|
|
2904
|
+
"orientation": "{param:orientation}"
|
|
2905
|
+
},
|
|
2906
|
+
"slots": {
|
|
2907
|
+
"list": [
|
|
2908
|
+
"{content:tabs-note}"
|
|
2909
|
+
],
|
|
2910
|
+
"panels": [
|
|
2911
|
+
"{content:panels-note}"
|
|
2912
|
+
]
|
|
2913
|
+
}
|
|
2914
|
+
},
|
|
2915
|
+
"parameters": [
|
|
2916
|
+
{
|
|
2917
|
+
"key": "orientation",
|
|
2918
|
+
"ask": "How should the tab list be laid out?",
|
|
2919
|
+
"options": [
|
|
2920
|
+
"horizontal",
|
|
2921
|
+
"vertical"
|
|
2922
|
+
],
|
|
2923
|
+
"default": "horizontal"
|
|
2924
|
+
}
|
|
2925
|
+
],
|
|
2926
|
+
"content": {
|
|
2927
|
+
"tabs-note": "[one tab per view; keep labels to one or two words]",
|
|
2928
|
+
"panels-note": "[one panel per tab, in the same order]"
|
|
2929
|
+
},
|
|
2930
|
+
"gaps": [
|
|
2931
|
+
"Tabs"
|
|
2932
|
+
],
|
|
2933
|
+
"blocked": true,
|
|
2934
|
+
"preset": null
|
|
2935
|
+
},
|
|
2936
|
+
{
|
|
2937
|
+
"id": "table-pagination",
|
|
2938
|
+
"intent": "Page-size selector and page range control beneath a data table",
|
|
2939
|
+
"match": [
|
|
2940
|
+
"pagination",
|
|
2941
|
+
"table pagination",
|
|
2942
|
+
"page controls",
|
|
2943
|
+
"rows per page",
|
|
2944
|
+
"paging"
|
|
2945
|
+
],
|
|
2946
|
+
"compose": {
|
|
2947
|
+
"component": "Toolbar",
|
|
2948
|
+
"props": {
|
|
2949
|
+
"gap": 12
|
|
2950
|
+
},
|
|
2951
|
+
"slots": {
|
|
2952
|
+
"body": [
|
|
2953
|
+
{
|
|
2954
|
+
"component": "Field",
|
|
2955
|
+
"slots": {
|
|
2956
|
+
"label": [
|
|
2957
|
+
"{content:page-size-label}"
|
|
2958
|
+
],
|
|
2959
|
+
"body": [
|
|
2960
|
+
{
|
|
2961
|
+
"component": "Select",
|
|
2962
|
+
"props": {
|
|
2963
|
+
"size": 32
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
]
|
|
2967
|
+
}
|
|
2968
|
+
},
|
|
2969
|
+
{
|
|
2970
|
+
"component": "Pagination"
|
|
2971
|
+
}
|
|
2972
|
+
]
|
|
2973
|
+
}
|
|
2974
|
+
},
|
|
2975
|
+
"parameters": [],
|
|
2976
|
+
"content": {
|
|
2977
|
+
"page-size-label": "Rows per page"
|
|
2978
|
+
},
|
|
2979
|
+
"gaps": [
|
|
2980
|
+
"Pagination"
|
|
2981
|
+
],
|
|
2982
|
+
"blocked": true,
|
|
2983
|
+
"preset": null
|
|
2520
2984
|
}
|
|
2521
2985
|
],
|
|
2522
2986
|
"scales": {
|
package/dist/store.js
CHANGED
|
@@ -9,7 +9,11 @@ const RESPONSE_BUDGET = 6000;
|
|
|
9
9
|
* topics — a fixed, slow-growing set of terse briefs (~90 chars each).
|
|
10
10
|
* Carries the rules an agent cannot guess, `getting-started`
|
|
11
11
|
* (the five required CSS imports) above all.
|
|
12
|
-
* patterns —
|
|
12
|
+
* patterns — composition recipes; their stored summaries carry match
|
|
13
|
+
* phrases for keyword ranking and can run long, so the
|
|
14
|
+
* overview projects a trimmed copy (see overviewBrief) capped
|
|
15
|
+
* near topic size — the stored, untrimmed brief is still what
|
|
16
|
+
* score() ranks against.
|
|
13
17
|
* components — verbose (up to 220 chars each) and the only axis that grows
|
|
14
18
|
* with every release. Also the axis a keyword query recovers
|
|
15
19
|
* best: search("menu") finds Menu; no query re-derives a house
|
|
@@ -57,7 +61,52 @@ function patternSummary(p) {
|
|
|
57
61
|
summary += `, blocked (gaps: ${p.gaps.join(", ")})`;
|
|
58
62
|
return summary;
|
|
59
63
|
}
|
|
64
|
+
/** Overview-only cap on the *intro* portion of a pattern summary (intent +
|
|
65
|
+
* match phrases + parameter count), comparable to a topic brief (~100
|
|
66
|
+
* bytes). The stored `Brief.summary` built by patternSummary() above stays
|
|
67
|
+
* full-length — score() reads it for keyword ranking (the match phrases are
|
|
68
|
+
* what let "delete confirmation" find destructive-confirm) — so this only
|
|
69
|
+
* shapes a copy used when search("") projects the overview list.
|
|
70
|
+
*
|
|
71
|
+
* The `, blocked (gaps: …)` tail is never subject to this cap: it is the
|
|
72
|
+
* backlog signal this cycle exists to publish, and patternSummary() puts it
|
|
73
|
+
* at the END of the string — a length-based slice of the assembled summary
|
|
74
|
+
* would silently drop it for every one of the 13 patterns (all longer than
|
|
75
|
+
* 120 chars), which is exactly the bug this trims to avoid. So the overview
|
|
76
|
+
* projects a pattern's summary from the underlying PatternEntry directly:
|
|
77
|
+
* trim the intro, then always append the tail. */
|
|
78
|
+
const OVERVIEW_PATTERN_INTRO_LIMIT = 100;
|
|
79
|
+
/** Trims `s` to at most `limit` chars, backing off to the last word boundary
|
|
80
|
+
* and marking the cut with an ellipsis — never a mid-word fragment that
|
|
81
|
+
* could read as a complete (if coincidental) match phrase. */
|
|
82
|
+
function truncateAtWord(s, limit) {
|
|
83
|
+
if (s.length <= limit)
|
|
84
|
+
return s;
|
|
85
|
+
const cut = s.slice(0, limit);
|
|
86
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
87
|
+
return `${lastSpace > 0 ? cut.slice(0, lastSpace) : cut}…`;
|
|
88
|
+
}
|
|
89
|
+
function overviewPatternSummary(p) {
|
|
90
|
+
const intro = truncateAtWord(`${p.intent} — ${p.match.join(", ")}, ${p.parameters.length} parameters`, OVERVIEW_PATTERN_INTRO_LIMIT);
|
|
91
|
+
return p.blocked ? `${intro}, blocked (gaps: ${p.gaps.join(", ")})` : intro;
|
|
92
|
+
}
|
|
60
93
|
export function createStore(index) {
|
|
94
|
+
const patternsById = new Map(index.patterns.map((p) => [p.id, p]));
|
|
95
|
+
/** Overview-only projection: returns `b` unchanged for every kind except
|
|
96
|
+
* pattern, where it re-derives the summary from the underlying
|
|
97
|
+
* `PatternEntry` (via `overviewPatternSummary`) rather than slicing the
|
|
98
|
+
* already-assembled, full-length `Brief.summary` — so the `blocked (gaps:
|
|
99
|
+
* …)` tail always survives the overview's trim. Always a shallow copy for
|
|
100
|
+
* patterns: the stored brief kept in `briefs` (and used by score()) is
|
|
101
|
+
* never mutated. */
|
|
102
|
+
function overviewBrief(b) {
|
|
103
|
+
if (b.kind !== "pattern")
|
|
104
|
+
return b;
|
|
105
|
+
const pattern = patternsById.get(b.title);
|
|
106
|
+
if (!pattern)
|
|
107
|
+
return b;
|
|
108
|
+
return { ...b, summary: overviewPatternSummary(pattern) };
|
|
109
|
+
}
|
|
61
110
|
const briefs = [
|
|
62
111
|
...index.components.map((c) => ({
|
|
63
112
|
id: `component:${c.name}`,
|
|
@@ -108,7 +157,7 @@ export function createStore(index) {
|
|
|
108
157
|
const candidates = OVERVIEW_KIND_ORDER.flatMap((kind) => briefs.filter((b) => b.kind === kind));
|
|
109
158
|
const out = [];
|
|
110
159
|
for (const brief of candidates) {
|
|
111
|
-
out.push(brief);
|
|
160
|
+
out.push(overviewBrief(brief));
|
|
112
161
|
if (JSON.stringify(out).length > RESPONSE_BUDGET) {
|
|
113
162
|
out.pop();
|
|
114
163
|
break;
|
package/llms.txt
CHANGED
|
@@ -7,4 +7,4 @@ Ids: component:<Name>, token:<name>, topic:<key>, pattern:<id>; bare names resol
|
|
|
7
7
|
|
|
8
8
|
Connect: https://psi.kurkin.de/mcp (Streamable HTTP) or `npx @handamade/psi-mcp` (stdio).
|
|
9
9
|
Agent context: `npx @handamade/psi-mcp init [--claude]` writes a marker-delimited block into AGENTS.md/CLAUDE.md (D44).
|
|
10
|
-
The index bakes packages/react/dist/{manifest.json,patterns.json} (patterns.json:
|
|
10
|
+
The index bakes packages/react/dist/{manifest.json,patterns.json} (patterns.json: 13 composition patterns with clarifying parameters, blocked/gaps flags, preset JSX) + packages/tokens/dist/{guidance.json,resolved/*.json} at publish; lockstep versioning keeps it truthful (D43).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handamade/psi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.9.0",
|
|
37
|
+
"@handamade/psi-react": "0.9.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"
|