@handamade/psi-mcp 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @handamade/psi-mcp
2
2
 
3
- MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the 50 React 19 components, 24 composition patterns (D47) that recipe them together with gap-annotated compose trees, every semantic token with its OKLCH derivation formula resolved across all four themes, and the usage guidance — through two read-only tools. It does not write code.
3
+ MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the 52 React 19 components, 24 composition patterns (D47) that recipe them together with gap-annotated compose trees, every semantic token with its OKLCH derivation formula resolved across all four themes, and the usage guidance — through two read-only tools. It does not write code.
4
4
 
5
5
  ## Connect
6
6
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.26.0",
2
+ "version": "0.27.0",
3
3
  "themes": [
4
4
  "light",
5
5
  "dark",
@@ -242,6 +242,120 @@
242
242
  ],
243
243
  "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Panel\n\nElevated surface panel (D51): secondary background, faint hairline, radius-12 — the shared --psi-surface-* recipe Dialog's panel also binds. Not a Card: no media slot, no hover lift, opaque by design.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Panel contents. |\n| `padding` | `16 \\| 24` | 24 | no | Inner padding in px. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root 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| Tab | Skipped — Panel itself is not focusable; focus moves through its children. |\n\nPlain <div> container with no implicit role. Pass aria-* host props if the panel should announce as a region.\n\n## Theming\n\nOverride `--psi-panel-*` 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- Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination (\"Rows per page\" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as=\"form\" align=\"start\" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
244
244
  },
245
+ {
246
+ "name": "Stack",
247
+ "description": "A column with a gap from the space scale (D109): Toolbar's vertical counterpart, minus the wrap. A plain <div> with no role. It gives a pattern tree a layout context, which Panel and the psi-gap-* utilities do not.",
248
+ "props": [
249
+ {
250
+ "name": "children",
251
+ "type": "ReactNode",
252
+ "required": false,
253
+ "default": null,
254
+ "description": "Stack contents, laid out top to bottom."
255
+ },
256
+ {
257
+ "name": "gap",
258
+ "type": "4 | 8 | 12 | 16 | 24 | 32",
259
+ "required": false,
260
+ "default": 16,
261
+ "description": "Space between children in px, from the space scale."
262
+ },
263
+ {
264
+ "name": "align",
265
+ "type": "\"stretch\" | \"start\" | \"center\" | \"end\"",
266
+ "required": false,
267
+ "default": "stretch",
268
+ "description": "Cross-axis alignment. `stretch` (the default) makes each child full\nwidth; `start` keeps a Button or a short line at its own width."
269
+ },
270
+ {
271
+ "name": "ref",
272
+ "type": "Ref<HTMLDivElement>",
273
+ "required": false,
274
+ "default": null,
275
+ "description": "Forwarded ref to the root element."
276
+ },
277
+ {
278
+ "name": "className",
279
+ "type": "string",
280
+ "required": false,
281
+ "default": null,
282
+ "description": "Additional CSS class name(s) merged onto the component's root element."
283
+ }
284
+ ],
285
+ "slots": [
286
+ {
287
+ "name": "body",
288
+ "accepts": {},
289
+ "cardinality": "0..*",
290
+ "order": 1
291
+ }
292
+ ],
293
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Stack\n\nA column with a gap from the space scale (D109): Toolbar's vertical counterpart, minus the wrap. A plain <div> with no role. It gives a pattern tree a layout context, which Panel and the psi-gap-* utilities do not.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Stack contents, laid out top to bottom. |\n| `gap` | `4 \\| 8 \\| 12 \\| 16 \\| 24 \\| 32` | 16 | no | Space between children in px, from the space scale. |\n| `align` | `\"stretch\" \\| \"start\" \\| \"center\" \\| \"end\"` | stretch | no | Cross-axis alignment. `stretch` (the default) makes each child full width; `start` keeps a Button or a short line at its own width. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root 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| Tab | Skipped — Stack itself is not focusable; focus moves through its children in source order. |\n\nPlain <div> with no role (D109): a column with a gap. Visual order is source order, so reading and focus order match.\n\n## Theming\n\nThis component has no `--psi-stack-*` 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- Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination (\"Rows per page\" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as=\"form\" align=\"start\" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
294
+ },
295
+ {
296
+ "name": "Text",
297
+ "description": "A line or paragraph of text at one of Psi's text styles (D109). It gives a pattern tree an element per line, so a headline and its explanation do not run together. Margin 0: a Stack's gap does the spacing.",
298
+ "props": [
299
+ {
300
+ "name": "children",
301
+ "type": "ReactNode",
302
+ "required": false,
303
+ "default": null,
304
+ "description": "The text."
305
+ },
306
+ {
307
+ "name": "as",
308
+ "type": "\"p\" | \"span\" | \"div\" | \"h2\" | \"h3\" | \"h4\"",
309
+ "required": false,
310
+ "default": "p",
311
+ "description": "The element. A heading's level follows the document outline, its size\nthe design, so `as` and `size` are separate."
312
+ },
313
+ {
314
+ "name": "size",
315
+ "type": "12 | 14 | 16 | 18",
316
+ "required": false,
317
+ "default": 14,
318
+ "description": "Font size in px; each size carries its own line height."
319
+ },
320
+ {
321
+ "name": "weight",
322
+ "type": "\"regular\" | \"medium\"",
323
+ "required": false,
324
+ "default": "regular",
325
+ "description": ""
326
+ },
327
+ {
328
+ "name": "tone",
329
+ "type": "\"primary\" | \"secondary\"",
330
+ "required": false,
331
+ "default": "primary",
332
+ "description": "`secondary` for supporting text."
333
+ },
334
+ {
335
+ "name": "ref",
336
+ "type": "Ref<HTMLElement>",
337
+ "required": false,
338
+ "default": null,
339
+ "description": "Forwarded ref to the element."
340
+ },
341
+ {
342
+ "name": "className",
343
+ "type": "string",
344
+ "required": false,
345
+ "default": null,
346
+ "description": "Additional CSS class name(s) merged onto the component's root element."
347
+ }
348
+ ],
349
+ "slots": [
350
+ {
351
+ "name": "body",
352
+ "accepts": {},
353
+ "cardinality": "0..*",
354
+ "order": 1
355
+ }
356
+ ],
357
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Text\n\nA line or paragraph of text at one of Psi's text styles (D109). It gives a pattern tree an element per line, so a headline and its explanation do not run together. Margin 0: a Stack's gap does the spacing.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | The text. |\n| `as` | `\"p\" \\| \"span\" \\| \"div\" \\| \"h2\" \\| \"h3\" \\| \"h4\"` | p | no | The element. A heading's level follows the document outline, its size the design, so `as` and `size` are separate. |\n| `size` | `12 \\| 14 \\| 16 \\| 18` | 14 | no | Font size in px; each size carries its own line height. |\n| `weight` | `\"regular\" \\| \"medium\"` | regular | no | |\n| `tone` | `\"primary\" \\| \"secondary\"` | primary | no | `secondary` for supporting text. |\n| `ref` | `Ref<HTMLElement>` | — | no | Forwarded ref to the 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| Tab | Not focusable. |\n\nRenders the element `as` names (p by default). Use h2–h4 for a heading at its outline level, whatever its `size`.\n\n## Theming\n\nOverride `--psi-text-*` 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- Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination (\"Rows per page\" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as=\"form\" align=\"start\" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
358
+ },
245
359
  {
246
360
  "name": "DescriptionList",
247
361
  "description": "Key/value display (D70): a `<dl>` of term/value pairs, the body `detail-drawer` has always described and never had a component for. No `size` prop — this is type, and the type scale is already expressed by --psi-text-*. Consumers restyle via the token family, not a prop.",
@@ -660,7 +774,7 @@
660
774
  },
661
775
  {
662
776
  "name": "Checkbox",
663
- "description": "Native checkbox with a built-in label; for independent binary choices.",
777
+ "description": "Native checkbox with a built-in label; for independent binary choices. In an invalid group Field it is aria-invalid, and an unchecked box marked aria-invalid outlines in the error colour (D108).",
664
778
  "props": [
665
779
  {
666
780
  "name": "children",
@@ -699,7 +813,7 @@
699
813
  }
700
814
  ],
701
815
  "slots": [],
702
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Checkbox\n\nNative checkbox with a built-in label; for independent binary choices.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label text. |\n| `aria-label` | `string` | — | no | Accessible name when there is no visible label — a table's row-selection checkbox, say. Optional, unlike IconButton's required form (D60): a labelled checkbox is still the common case. |\n| `indeterminate` | `boolean` | — | no | The mixed state, e.g. a \"select all\" over a partial selection (D103). Sets the input's DOM property, which the platform exposes as aria-checked=\"mixed\", and draws a dash. Re-applied on every render, because a click clears the property before the consumer re-renders. When absent, the property is left alone, so a state set through the ref survives. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type=\"checkbox\">` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Space | Toggles. Native <input type=checkbox> underneath (visually hidden). |\n\n\n## Theming\n\nOverride `--psi-checkbox-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination (\"Rows per page\" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as=\"form\" align=\"start\" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
816
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Checkbox\n\nNative checkbox with a built-in label; for independent binary choices. In an invalid group Field it is aria-invalid, and an unchecked box marked aria-invalid outlines in the error colour (D108).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label text. |\n| `aria-label` | `string` | — | no | Accessible name when there is no visible label — a table's row-selection checkbox, say. Optional, unlike IconButton's required form (D60): a labelled checkbox is still the common case. |\n| `indeterminate` | `boolean` | — | no | The mixed state, e.g. a \"select all\" over a partial selection (D103). Sets the input's DOM property, which the platform exposes as aria-checked=\"mixed\", and draws a dash. Re-applied on every render, because a click clears the property before the consumer re-renders. When absent, the property is left alone, so a state set through the ref survives. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type=\"checkbox\">` element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Space | Toggles. Native <input type=checkbox> underneath (visually hidden). |\n\n\n## Theming\n\nOverride `--psi-checkbox-*` custom properties at any scope.\n\n\n\n## Rules\n\n- One accent per visual group; everything else neutral or ghost.\n- danger only for actions with real consequences.\n- Sizes are px numbers (24|32|40|48), never S/M/L.\n- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.\n- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.\n- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).\n- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.\n- Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination (\"Rows per page\" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as=\"form\" align=\"start\" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
703
817
  },
704
818
  {
705
819
  "name": "Switch",
@@ -1049,7 +1163,7 @@
1049
1163
  "type": "ReactElement<Record<string, unknown>, string | JSXElementConstructor<any>>",
1050
1164
  "required": true,
1051
1165
  "default": null,
1052
- "description": "The trigger element (must accept ref, onMouseEnter/Leave, onFocus/Blur)."
1166
+ "description": "The trigger element (must accept onFocus/Blur)."
1053
1167
  },
1054
1168
  {
1055
1169
  "name": "placement",
@@ -1060,7 +1174,7 @@
1060
1174
  }
1061
1175
  ],
1062
1176
  "slots": [],
1063
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tooltip\n\nShort hover/focus hint attached to a single trigger element.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `content` | `ReactNode` | — | yes | Tooltip content. |\n| `children` | `ReactElement<Record<string, unknown>, string \\| JSXElementConstructor<any>>` | — | yes | The trigger element (must accept ref, onMouseEnter/Leave, onFocus/Blur). |\n| `placement` | `\"top\" \\| \"bottom\" \\| \"left\" \\| \"right\"` | top | no | Preferred placement. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab (focus trigger) | Shows immediately on focus (no delay). |\n| Escape | Dismisses while visible (WCAG 1.4.13). |\n\nHover opens after a short delay; content is linked via aria-describedby while visible. Trigger must accept onMouseEnter/Leave and onFocus/Blur props (cloned in automatically) — no ref forwarding required.\n\n## Theming\n\nOverride `--psi-tooltip-*` 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- Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination (\"Rows per page\" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as=\"form\" align=\"start\" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
1177
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tooltip\n\nShort hover/focus hint attached to a single trigger element.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `content` | `ReactNode` | — | yes | Tooltip content. |\n| `children` | `ReactElement<Record<string, unknown>, string \\| JSXElementConstructor<any>>` | — | yes | The trigger element (must accept onFocus/Blur). |\n| `placement` | `\"top\" \\| \"bottom\" \\| \"left\" \\| \"right\"` | top | no | Preferred placement. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab (focus trigger) | Shows immediately on focus (no delay). |\n| Escape | Dismisses while visible (WCAG 1.4.13). |\n\nHover opens after a short delay; content is linked via aria-describedby while visible. Trigger must accept onMouseEnter/Leave and onFocus/Blur props (cloned in automatically) — no ref forwarding required.\n\n## Theming\n\nOverride `--psi-tooltip-*` 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- Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination (\"Rows per page\" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as=\"form\" align=\"start\" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).\n- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
1064
1178
  },
1065
1179
  {
1066
1180
  "name": "NavBar",
@@ -3988,6 +4102,48 @@
3988
4102
  }
3989
4103
  }
3990
4104
  },
4105
+ {
4106
+ "name": "borderControl",
4107
+ "formula": "ref(fgPrimary) → alpha(0.55)",
4108
+ "values": {
4109
+ "light": {
4110
+ "oklch": {
4111
+ "l": 0.3,
4112
+ "c": 0.03,
4113
+ "h": 250,
4114
+ "alpha": 0.55
4115
+ },
4116
+ "hex": "#222f3c"
4117
+ },
4118
+ "dark": {
4119
+ "oklch": {
4120
+ "l": 0.93,
4121
+ "c": 0.01,
4122
+ "h": 250,
4123
+ "alpha": 0.4
4124
+ },
4125
+ "hex": "#e3e8ee"
4126
+ },
4127
+ "acme": {
4128
+ "oklch": {
4129
+ "l": 0.3,
4130
+ "c": 0.03,
4131
+ "h": 30,
4132
+ "alpha": 0.55
4133
+ },
4134
+ "hex": "#3c2825"
4135
+ },
4136
+ "ember": {
4137
+ "oklch": {
4138
+ "l": 0.949,
4139
+ "c": 0.011,
4140
+ "h": 72,
4141
+ "alpha": 0.4
4142
+ },
4143
+ "hex": "#f3ede6"
4144
+ }
4145
+ }
4146
+ },
3991
4147
  {
3992
4148
  "name": "borderFocus",
3993
4149
  "formula": "ref(fgAccent)",
@@ -5076,14 +5232,39 @@
5076
5232
  },
5077
5233
  "slots": {
5078
5234
  "body": [
5079
- "{content:headline}",
5080
- "{content:explanation}",
5081
5235
  {
5082
- "component": "Button",
5236
+ "component": "Stack",
5083
5237
  "props": {
5084
- "variant": "neutral"
5238
+ "gap": 8,
5239
+ "align": "start"
5085
5240
  },
5086
- "content": "reset-label"
5241
+ "slots": {
5242
+ "body": [
5243
+ {
5244
+ "component": "Text",
5245
+ "props": {
5246
+ "as": "h3",
5247
+ "size": 16,
5248
+ "weight": "medium"
5249
+ },
5250
+ "content": "headline"
5251
+ },
5252
+ {
5253
+ "component": "Text",
5254
+ "props": {
5255
+ "tone": "secondary"
5256
+ },
5257
+ "content": "explanation"
5258
+ },
5259
+ {
5260
+ "component": "Button",
5261
+ "props": {
5262
+ "variant": "neutral"
5263
+ },
5264
+ "content": "reset-label"
5265
+ }
5266
+ ]
5267
+ }
5087
5268
  }
5088
5269
  ]
5089
5270
  }
@@ -5096,7 +5277,7 @@
5096
5277
  },
5097
5278
  "gaps": [],
5098
5279
  "blocked": false,
5099
- "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"
5280
+ "preset": "<Panel\n padding={24}\n>\n <Stack\n align=\"start\"\n gap={8}\n >\n <Text as=\"h3\" size={16} weight=\"medium\">[nothing matches these filters]</Text>\n <Text tone=\"secondary\">[say whether the dataset is empty or the filters are too narrow]</Text>\n <Button variant=\"neutral\">Clear filters</Button>\n </Stack>\n</Panel>\n"
5100
5281
  },
5101
5282
  {
5102
5283
  "id": "feedback-form",
@@ -5116,75 +5297,92 @@
5116
5297
  "slots": {
5117
5298
  "body": [
5118
5299
  {
5119
- "component": "Field",
5300
+ "component": "Stack",
5120
5301
  "props": {
5121
- "group": true,
5122
- "required": true,
5123
- "requiredText": "(Required)"
5302
+ "gap": 24
5124
5303
  },
5125
5304
  "slots": {
5126
- "label": [
5127
- "{content:category-label}"
5128
- ],
5129
5305
  "body": [
5130
5306
  {
5131
- "component": "RadioGroup",
5307
+ "component": "Field",
5308
+ "props": {
5309
+ "group": true,
5310
+ "required": true,
5311
+ "requiredText": "(Required)"
5312
+ },
5132
5313
  "slots": {
5314
+ "label": [
5315
+ "{content:category-label}"
5316
+ ],
5133
5317
  "body": [
5134
5318
  {
5135
- "component": "Radio",
5136
- "props": {
5137
- "value": "problem"
5138
- },
5139
- "content": "first-option"
5140
- },
5319
+ "component": "RadioGroup",
5320
+ "slots": {
5321
+ "body": [
5322
+ {
5323
+ "component": "Radio",
5324
+ "props": {
5325
+ "value": "problem"
5326
+ },
5327
+ "content": "first-option"
5328
+ },
5329
+ {
5330
+ "component": "Radio",
5331
+ "props": {
5332
+ "value": "idea"
5333
+ },
5334
+ "content": "second-option"
5335
+ },
5336
+ {
5337
+ "component": "Radio",
5338
+ "props": {
5339
+ "value": "other"
5340
+ },
5341
+ "content": "third-option"
5342
+ }
5343
+ ]
5344
+ }
5345
+ }
5346
+ ]
5347
+ }
5348
+ },
5349
+ {
5350
+ "component": "Field",
5351
+ "slots": {
5352
+ "label": [
5353
+ "{content:description-label}"
5354
+ ],
5355
+ "body": [
5141
5356
  {
5142
- "component": "Radio",
5357
+ "component": "Textarea",
5143
5358
  "props": {
5144
- "value": "idea"
5145
- },
5146
- "content": "second-option"
5147
- },
5359
+ "maxLength": 500,
5360
+ "rows": 4
5361
+ }
5362
+ }
5363
+ ],
5364
+ "description": [
5365
+ "{content:description-help}"
5366
+ ]
5367
+ }
5368
+ },
5369
+ {
5370
+ "component": "Toolbar",
5371
+ "slots": {
5372
+ "body": [
5148
5373
  {
5149
- "component": "Radio",
5374
+ "component": "Button",
5150
5375
  "props": {
5151
- "value": "other"
5376
+ "type": "submit",
5377
+ "variant": "accent"
5152
5378
  },
5153
- "content": "third-option"
5379
+ "content": "submit-label"
5154
5380
  }
5155
5381
  ]
5156
5382
  }
5157
5383
  }
5158
5384
  ]
5159
5385
  }
5160
- },
5161
- {
5162
- "component": "Field",
5163
- "slots": {
5164
- "label": [
5165
- "{content:description-label}"
5166
- ],
5167
- "body": [
5168
- {
5169
- "component": "Textarea",
5170
- "props": {
5171
- "maxLength": 500,
5172
- "rows": 4
5173
- }
5174
- }
5175
- ],
5176
- "description": [
5177
- "{content:description-help}"
5178
- ]
5179
- }
5180
- },
5181
- {
5182
- "component": "Button",
5183
- "props": {
5184
- "type": "submit",
5185
- "variant": "accent"
5186
- },
5187
- "content": "submit-label"
5188
5386
  }
5189
5387
  ]
5190
5388
  }
@@ -5201,7 +5399,7 @@
5201
5399
  },
5202
5400
  "gaps": [],
5203
5401
  "blocked": false,
5204
- "preset": "<Panel\n padding={24}\n>\n <Field\n group={true}\n label=\"Category\"\n required={true}\n requiredText=\"(Required)\"\n >\n <RadioGroup>\n <Radio value=\"problem\">Something is broken</Radio>\n <Radio value=\"idea\">An idea</Radio>\n <Radio value=\"other\">Something else</Radio>\n </RadioGroup>\n </Field>\n <Field\n description=\"[what happened, and what you expected]\"\n label=\"Description\"\n >\n <Textarea maxLength={500} rows={4} />\n </Field>\n <Button type=\"submit\" variant=\"accent\">Send</Button>\n</Panel>\n"
5402
+ "preset": "<Panel\n padding={24}\n>\n <Stack\n gap={24}\n >\n <Field\n group={true}\n label=\"Category\"\n required={true}\n requiredText=\"(Required)\"\n >\n <RadioGroup>\n <Radio value=\"problem\">Something is broken</Radio>\n <Radio value=\"idea\">An idea</Radio>\n <Radio value=\"other\">Something else</Radio>\n </RadioGroup>\n </Field>\n <Field\n description=\"[what happened, and what you expected]\"\n label=\"Description\"\n >\n <Textarea maxLength={500} rows={4} />\n </Field>\n <Toolbar>\n <Button type=\"submit\" variant=\"accent\">Send</Button>\n </Toolbar>\n </Stack>\n</Panel>\n"
5205
5403
  },
5206
5404
  {
5207
5405
  "id": "filter-form",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handamade/psi-mcp",
3
- "version": "0.26.0",
3
+ "version": "0.27.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.26.0",
37
- "@handamade/psi-react": "0.26.0"
36
+ "@handamade/psi-react": "0.27.0",
37
+ "@handamade/psi-tokens": "0.27.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"