@handamade/psi-mcp 0.8.1 → 0.9.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 +1 -1
- package/dist/psi-index.json +472 -8
- package/dist/store.js +84 -19
- 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.1",
|
|
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
|
@@ -3,24 +3,32 @@ const MAX_RESULTS = 20;
|
|
|
3
3
|
* (~1.5k tokens)" (docs/superpowers/plans/2026-07-18-agent-access-plan.md),
|
|
4
4
|
* asserted by __tests__/store.test.ts's "stays within the response budget". */
|
|
5
5
|
const RESPONSE_BUDGET = 6000;
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
6
|
+
/** How the empty-query overview divides its budget across kinds. Topics,
|
|
7
|
+
* patterns, and components grow and cost very differently:
|
|
8
8
|
*
|
|
9
9
|
* topics — a fixed, slow-growing set of terse briefs (~90 chars each).
|
|
10
10
|
* Carries the rules an agent cannot guess, `getting-started`
|
|
11
|
-
* (the five required CSS imports) above all.
|
|
12
|
-
*
|
|
11
|
+
* (the five required CSS imports) above all. Takes its full
|
|
12
|
+
* cost, unconditionally.
|
|
13
13
|
* components — verbose (up to 220 chars each) and the only axis that grows
|
|
14
14
|
* with every release. Also the axis a keyword query recovers
|
|
15
15
|
* best: search("menu") finds Menu; no query re-derives a house
|
|
16
|
-
* rule an agent doesn't know exists.
|
|
16
|
+
* rule an agent doesn't know exists. Reserved a floor of
|
|
17
|
+
* `COMPONENT_FLOOR` items before patterns spend the remainder,
|
|
18
|
+
* so catalog growth cannot starve them below the floor.
|
|
19
|
+
* patterns — composition recipes; their stored summaries carry match
|
|
20
|
+
* phrases for keyword ranking and can run long, so the overview
|
|
21
|
+
* projects a trimmed copy (see overviewBrief) at a *derived*
|
|
22
|
+
* per-call cap — the largest that lets every pattern fit
|
|
23
|
+
* alongside topics and the component floor. The stored,
|
|
24
|
+
* untrimmed brief is still what score() ranks against.
|
|
17
25
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
const
|
|
26
|
+
* Previously the overview filled topics, then patterns, then components
|
|
27
|
+
* until the budget ran out, so whichever kind was filled last absorbed all
|
|
28
|
+
* catalog growth — three more patterns was enough to starve components from
|
|
29
|
+
* six down to four (D61). Allocating by kind first means catalog growth
|
|
30
|
+
* shortens pattern summaries instead of evicting items off the tail. */
|
|
31
|
+
const COMPONENT_FLOOR = 8;
|
|
24
32
|
const TOPIC_SUMMARIES = {
|
|
25
33
|
variants: "Variant intent and typical use for all 8 flat variants",
|
|
26
34
|
rules: "House rules: accent budget, sizing, color binding",
|
|
@@ -57,7 +65,53 @@ function patternSummary(p) {
|
|
|
57
65
|
summary += `, blocked (gaps: ${p.gaps.join(", ")})`;
|
|
58
66
|
return summary;
|
|
59
67
|
}
|
|
68
|
+
/** Most an overview pattern intro (intent + match phrases + parameter count)
|
|
69
|
+
* may cost. The effective limit is derived per-call (see the empty-query
|
|
70
|
+
* branch): patterns take whatever the budget leaves after topics and the
|
|
71
|
+
* component floor, so a growing catalog shortens summaries instead of
|
|
72
|
+
* evicting items (D61).
|
|
73
|
+
*
|
|
74
|
+
* The `, blocked (gaps: …)` tail is never subject to this cap: it is the
|
|
75
|
+
* backlog signal this cycle exists to publish, and patternSummary() puts it
|
|
76
|
+
* at the END of the string — a length-based slice of the assembled summary
|
|
77
|
+
* would silently drop it for every one of the 13 patterns (all longer than
|
|
78
|
+
* 120 chars), which is exactly the bug this trims to avoid. So the overview
|
|
79
|
+
* projects a pattern's summary from the underlying PatternEntry directly:
|
|
80
|
+
* trim the intro, then always append the tail. */
|
|
81
|
+
const OVERVIEW_PATTERN_INTRO_MAX = 120;
|
|
82
|
+
/** Trims `s` to at most `limit` chars, backing off to the last word boundary
|
|
83
|
+
* and marking the cut with an ellipsis — never a mid-word fragment that
|
|
84
|
+
* could read as a complete (if coincidental) match phrase. */
|
|
85
|
+
function truncateAtWord(s, limit) {
|
|
86
|
+
if (s.length <= limit)
|
|
87
|
+
return s;
|
|
88
|
+
const cut = s.slice(0, limit);
|
|
89
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
90
|
+
return `${lastSpace > 0 ? cut.slice(0, lastSpace) : cut}…`;
|
|
91
|
+
}
|
|
92
|
+
function overviewPatternSummary(p, limit) {
|
|
93
|
+
const intro = truncateAtWord(`${p.intent} — ${p.match.join(", ")}, ${p.parameters.length} parameters`, limit);
|
|
94
|
+
return p.blocked ? `${intro}, blocked (gaps: ${p.gaps.join(", ")})` : intro;
|
|
95
|
+
}
|
|
60
96
|
export function createStore(index) {
|
|
97
|
+
const patternsById = new Map(index.patterns.map((p) => [p.id, p]));
|
|
98
|
+
/** Overview-only projection: returns `b` unchanged for every kind except
|
|
99
|
+
* pattern, where it re-derives the summary from the underlying
|
|
100
|
+
* `PatternEntry` (via `overviewPatternSummary`) rather than slicing the
|
|
101
|
+
* already-assembled, full-length `Brief.summary` — so the `blocked (gaps:
|
|
102
|
+
* …)` tail always survives the overview's trim. Always a shallow copy for
|
|
103
|
+
* patterns: the stored brief kept in `briefs` (and used by score()) is
|
|
104
|
+
* never mutated. `limit` is derived per-call by the empty-query branch
|
|
105
|
+
* (D61), not a module constant, so a growing catalog shortens summaries
|
|
106
|
+
* instead of evicting items. */
|
|
107
|
+
function overviewBrief(b, limit) {
|
|
108
|
+
if (b.kind !== "pattern")
|
|
109
|
+
return b;
|
|
110
|
+
const pattern = patternsById.get(b.title);
|
|
111
|
+
if (!pattern)
|
|
112
|
+
return b;
|
|
113
|
+
return { ...b, summary: overviewPatternSummary(pattern, limit) };
|
|
114
|
+
}
|
|
61
115
|
const briefs = [
|
|
62
116
|
...index.components.map((c) => ({
|
|
63
117
|
id: `component:${c.name}`,
|
|
@@ -101,14 +155,25 @@ export function createStore(index) {
|
|
|
101
155
|
function search(query) {
|
|
102
156
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
103
157
|
if (terms.length === 0) {
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
158
|
+
// D61: allocate before filling. Topics take their full cost, components
|
|
159
|
+
// are reserved a floor, and patterns take the remainder via a derived
|
|
160
|
+
// cap — so catalog growth shortens summaries instead of evicting the
|
|
161
|
+
// tail. Whichever kind was filled last used to absorb all growth;
|
|
162
|
+
// that was the bug. (Tokens are omitted; they are discoverable via
|
|
163
|
+
// query.)
|
|
164
|
+
const topics = briefs.filter((b) => b.kind === "topic");
|
|
165
|
+
const patterns = briefs.filter((b) => b.kind === "pattern");
|
|
166
|
+
const components = briefs.filter((b) => b.kind === "component");
|
|
167
|
+
const reserve = JSON.stringify(components.slice(0, COMPONENT_FLOOR)).length;
|
|
168
|
+
const fits = (limit) => JSON.stringify([...topics, ...patterns.map((b) => overviewBrief(b, limit))]).length +
|
|
169
|
+
reserve <=
|
|
170
|
+
RESPONSE_BUDGET;
|
|
171
|
+
let limit = OVERVIEW_PATTERN_INTRO_MAX;
|
|
172
|
+
while (limit > 0 && !fits(limit))
|
|
173
|
+
limit -= 10;
|
|
174
|
+
const out = [...topics, ...patterns.map((b) => overviewBrief(b, limit))];
|
|
175
|
+
for (const component of components) {
|
|
176
|
+
out.push(component);
|
|
112
177
|
if (JSON.stringify(out).length > RESPONSE_BUDGET) {
|
|
113
178
|
out.pop();
|
|
114
179
|
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.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.
|
|
37
|
-
"@handamade/psi-react": "0.
|
|
36
|
+
"@handamade/psi-tokens": "0.9.1",
|
|
37
|
+
"@handamade/psi-react": "0.9.1"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"
|