@handamade/psi-mcp 0.26.0 → 0.28.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.28.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,11 +1174,11 @@
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",
1067
- "description": "Top navigation bar with brand, nav-link, and trailing-action slots.",
1181
+ "description": "Top navigation bar with brand, nav-link, and trailing-action slots. Plain text in a slot takes `--psi-navbar-fg`, the foreground of the theme on the bar, so `data-psi-theme` on the `NavBar` reaches it (D111).",
1068
1182
  "props": [
1069
1183
  {
1070
1184
  "name": "brand",
@@ -1136,7 +1250,7 @@
1136
1250
  "order": 3
1137
1251
  }
1138
1252
  ],
1139
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# NavBar\n\nTop navigation bar with brand, nav-link, and trailing-action slots.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `brand` | `ReactNode` | — | no | Brand slot (wordmark / logo link), leading edge. |\n| `actions` | `ReactNode` | — | no | Trailing actions slot (theme switch, CTA). |\n| `children` | `ReactNode` | — | no | Nav links. The `<nav>` renders only when there are children, so a bar with a brand and actions and no links has no empty landmark (D88). |\n| `navLabel` | `string` | — | no | Accessible name of the links' `<nav>` (its `aria-label`). Rest props land on the `<header>`, so this is the only way to label the `<nav>` (D88). |\n| `fluid` | `boolean` | false | no | Lay the row out at full width: it drops `psi-container` (the centred max-width column) and keeps the `--psi-gutter` side padding. For an application header over a full-width frame (D88). |\n| `ref` | `Ref<HTMLElement>` | — | no | Forwarded ref to the header element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nOverride `--psi-navbar-*` 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- 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"
1253
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# NavBar\n\nTop navigation bar with brand, nav-link, and trailing-action slots. Plain text in a slot takes `--psi-navbar-fg`, the foreground of the theme on the bar, so `data-psi-theme` on the `NavBar` reaches it (D111).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `brand` | `ReactNode` | — | no | Brand slot (wordmark / logo link), leading edge. |\n| `actions` | `ReactNode` | — | no | Trailing actions slot (theme switch, CTA). |\n| `children` | `ReactNode` | — | no | Nav links. The `<nav>` renders only when there are children, so a bar with a brand and actions and no links has no empty landmark (D88). |\n| `navLabel` | `string` | — | no | Accessible name of the links' `<nav>` (its `aria-label`). Rest props land on the `<header>`, so this is the only way to label the `<nav>` (D88). |\n| `fluid` | `boolean` | false | no | Lay the row out at full width: it drops `psi-container` (the centred max-width column) and keeps the `--psi-gutter` side padding. For an application header over a full-width frame (D88). |\n| `ref` | `Ref<HTMLElement>` | — | no | Forwarded ref to the header element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nOverride `--psi-navbar-*` 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- 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"
1140
1254
  },
1141
1255
  {
1142
1256
  "name": "SkipLink",
@@ -1310,7 +1424,7 @@
1310
1424
  },
1311
1425
  {
1312
1426
  "name": "AppShell",
1313
- "description": "The frame of an application (D88): a header row over a sidebar and a `<main>` that each scroll on their own, filling the viewport (`100dvh`). Because the header has a row of its own and `main` is its own scroller, nothing scrolls under the header, so a focused element is never obscured by it (WCAG 2.2 *Focus Not Obscured*) without `scroll-padding`. `main` carries `id={mainId}` and `tabIndex={-1}`: the target of a `SkipLink`, not a tab stop; focused by the skip link it draws the shared focus ring inside its box. A closed sidebar is `hidden`, not narrowed to icons; there is no responsive drawer and no stored open state, so this is a desktop frame. A page whose document must scroll as a whole does not use `AppShell`. Mount it at the root of the page with no margin or padding around it: it is `100dvh` tall, so anything around it makes the document scroll and slides the frame. Do not place a `ToastRegion` at `top-start` in an app with a skip link: the native top layer paints over the focused link.",
1427
+ "description": "The frame of an application (D88): a header row over a sidebar and a `<main>` that each scroll on their own, filling the viewport (`100dvh`). Because the header has a row of its own and `main` is its own scroller, nothing scrolls under the header, so a focused element is never obscured by it (WCAG 2.2 *Focus Not Obscured*) without `scroll-padding`. `main` carries `id={mainId}` and `tabIndex={-1}`: the target of a `SkipLink`, not a tab stop; focused by the skip link it draws the shared focus ring inside its box. A closed sidebar is `hidden`, not narrowed to icons; there is no responsive drawer and no stored open state, so this is a desktop frame. A page whose document must scroll as a whole does not use `AppShell`. Mount it at the root of the page with no margin or padding around it: it is `100dvh` tall, so anything around it makes the document scroll and slides the frame. Do not place a `ToastRegion` at `top-start` in an app with a skip link: the native top layer paints over the focused link. A theme can make `main` a surface beside a one-colour header and sidebar, with no CSS of the consumer's (D110): `--psi-app-shell-bg`, which must equal the canvas of the theme on the header and the sidebar, `--psi-app-shell-main-bg` and `--psi-app-shell-main-radius`. All three paint nothing by default. The corner is square unless a header and a sidebar both meet `main`. Put the header under that theme with `data-psi-theme` on the `NavBar`, whose rest props land on its `<header>`, and remove its line with `--psi-navbar-border-width: 0`.",
1314
1428
  "props": [
1315
1429
  {
1316
1430
  "name": "skipLink",
@@ -1421,7 +1535,7 @@
1421
1535
  "order": 4
1422
1536
  }
1423
1537
  ],
1424
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# AppShell\n\nThe frame of an application (D88): a header row over a sidebar and a `<main>` that each scroll on their own, filling the viewport (`100dvh`). Because the header has a row of its own and `main` is its own scroller, nothing scrolls under the header, so a focused element is never obscured by it (WCAG 2.2 *Focus Not Obscured*) without `scroll-padding`. `main` carries `id={mainId}` and `tabIndex={-1}`: the target of a `SkipLink`, not a tab stop; focused by the skip link it draws the shared focus ring inside its box. A closed sidebar is `hidden`, not narrowed to icons; there is no responsive drawer and no stored open state, so this is a desktop frame. A page whose document must scroll as a whole does not use `AppShell`. Mount it at the root of the page with no margin or padding around it: it is `100dvh` tall, so anything around it makes the document scroll and slides the frame. Do not place a `ToastRegion` at `top-start` in an app with a skip link: the native top layer paints over the focused link.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `skipLink` | `ReactNode` | — | no | A `SkipLink` (D88), rendered first so it is the first tab stop. Point its `href` at `#` plus `mainId`. |\n| `header` | `ReactNode` | — | no | The header row, usually a `NavBar` (D88). It sits in a row of its own above the sidebar and `main`, so nothing scrolls under it. |\n| `sidebar` | `ReactNode` | — | no | The sidebar's content, usually a `NavTree` (D88). The shell wraps it in a `<div>`, not an `<aside>`: a `NavTree` is already the landmark. |\n| `children` | `ReactNode` | — | no | The page, rendered inside `<main>` (D88). |\n| `sidebarOpen` | `boolean` | true | no | Whether the sidebar is shown. Controlled: wire your Menu toggle's `aria-expanded` and this to one piece of state. Closed, the sidebar is `hidden`, so it leaves the layout and the accessibility tree, and stays in the DOM so the toggle's `aria-controls` keeps naming an element (D88). |\n| `sidebarTheme` | `string` | — | no | A theme name for the sidebar alone, such as `\"dark\"`: sets `data-psi-theme` on the sidebar element, so every token inside re-resolves under that theme while the rest of the page keeps its own (D88). The sidebar paints `--psi-bg-primary` itself; a theme paints no background. |\n| `sidebarId` | `string` | sidebar | no | `id` of the sidebar element, which a toggle names with `aria-controls` (D88). |\n| `mainId` | `string` | main | no | `id` of `<main>`, which a `SkipLink`'s `href` names (D88). |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | The first stop is the skip link (when given), then the header, the sidebar's links and buttons, and the links of main, in document order. The shell adds no stop of its own: main has tabIndex={-1}. |\n| Enter (on the skip link) | Follows the fragment href: focus moves to main, drawing the shared focus ring inside its box, and the next Tab continues from main's first link. |\n\nA grid filling the viewport: the header sits in a row of its own and main is its own scroller, so nothing scrolls under the header and a focused element is never obscured by it (WCAG 2.2 Focus Not Obscured) without scroll-padding, which does nothing on a main that is not the document's scroller (D88). main carries id={mainId} and tabIndex={-1}, the skip link's target. A closed sidebar is `hidden`, so it leaves the layout and the accessibility tree while its id stays in the DOM for a toggle's aria-controls; the toggle's aria-expanded and sidebarOpen are wired by the consumer, since the shell is controlled and stores nothing. The sidebar is a div, not an aside: the NavTree inside is already the navigation landmark. sidebarTheme sets data-psi-theme on the sidebar alone, so its tokens re-resolve under that theme in a page that keeps its own; the sidebar paints --psi-bg-primary itself. Desktop frame only: no responsive drawer. Do not place a ToastRegion at top-start in an app with a skip link.\n\n## Theming\n\nOverride `--psi-app-shell-*` 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"
1538
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# AppShell\n\nThe frame of an application (D88): a header row over a sidebar and a `<main>` that each scroll on their own, filling the viewport (`100dvh`). Because the header has a row of its own and `main` is its own scroller, nothing scrolls under the header, so a focused element is never obscured by it (WCAG 2.2 *Focus Not Obscured*) without `scroll-padding`. `main` carries `id={mainId}` and `tabIndex={-1}`: the target of a `SkipLink`, not a tab stop; focused by the skip link it draws the shared focus ring inside its box. A closed sidebar is `hidden`, not narrowed to icons; there is no responsive drawer and no stored open state, so this is a desktop frame. A page whose document must scroll as a whole does not use `AppShell`. Mount it at the root of the page with no margin or padding around it: it is `100dvh` tall, so anything around it makes the document scroll and slides the frame. Do not place a `ToastRegion` at `top-start` in an app with a skip link: the native top layer paints over the focused link. A theme can make `main` a surface beside a one-colour header and sidebar, with no CSS of the consumer's (D110): `--psi-app-shell-bg`, which must equal the canvas of the theme on the header and the sidebar, `--psi-app-shell-main-bg` and `--psi-app-shell-main-radius`. All three paint nothing by default. The corner is square unless a header and a sidebar both meet `main`. Put the header under that theme with `data-psi-theme` on the `NavBar`, whose rest props land on its `<header>`, and remove its line with `--psi-navbar-border-width: 0`.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `skipLink` | `ReactNode` | — | no | A `SkipLink` (D88), rendered first so it is the first tab stop. Point its `href` at `#` plus `mainId`. |\n| `header` | `ReactNode` | — | no | The header row, usually a `NavBar` (D88). It sits in a row of its own above the sidebar and `main`, so nothing scrolls under it. |\n| `sidebar` | `ReactNode` | — | no | The sidebar's content, usually a `NavTree` (D88). The shell wraps it in a `<div>`, not an `<aside>`: a `NavTree` is already the landmark. |\n| `children` | `ReactNode` | — | no | The page, rendered inside `<main>` (D88). |\n| `sidebarOpen` | `boolean` | true | no | Whether the sidebar is shown. Controlled: wire your Menu toggle's `aria-expanded` and this to one piece of state. Closed, the sidebar is `hidden`, so it leaves the layout and the accessibility tree, and stays in the DOM so the toggle's `aria-controls` keeps naming an element (D88). |\n| `sidebarTheme` | `string` | — | no | A theme name for the sidebar alone, such as `\"dark\"`: sets `data-psi-theme` on the sidebar element, so every token inside re-resolves under that theme while the rest of the page keeps its own (D88). The sidebar paints `--psi-bg-primary` itself; a theme paints no background. |\n| `sidebarId` | `string` | sidebar | no | `id` of the sidebar element, which a toggle names with `aria-controls` (D88). |\n| `mainId` | `string` | main | no | `id` of `<main>`, which a `SkipLink`'s `href` names (D88). |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the root `<div>`. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Tab | The first stop is the skip link (when given), then the header, the sidebar's links and buttons, and the links of main, in document order. The shell adds no stop of its own: main has tabIndex={-1}. |\n| Enter (on the skip link) | Follows the fragment href: focus moves to main, drawing the shared focus ring inside its box, and the next Tab continues from main's first link. |\n\nA grid filling the viewport: the header sits in a row of its own and main is its own scroller, so nothing scrolls under the header and a focused element is never obscured by it (WCAG 2.2 Focus Not Obscured) without scroll-padding, which does nothing on a main that is not the document's scroller (D88). main carries id={mainId} and tabIndex={-1}, the skip link's target. A closed sidebar is `hidden`, so it leaves the layout and the accessibility tree while its id stays in the DOM for a toggle's aria-controls; the toggle's aria-expanded and sidebarOpen are wired by the consumer, since the shell is controlled and stores nothing. The sidebar is a div, not an aside: the NavTree inside is already the navigation landmark. sidebarTheme sets data-psi-theme on the sidebar alone, so its tokens re-resolve under that theme in a page that keeps its own; the sidebar paints --psi-bg-primary itself. Desktop frame only: no responsive drawer. Do not place a ToastRegion at top-start in an app with a skip link. main's rounded corner (--psi-app-shell-main-radius, D110) keeps the skip link's ring: the inset outline follows the curve, measured in Chromium in LTR and RTL.\n\n## Theming\n\nOverride `--psi-app-shell-*` 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"
1425
1539
  },
1426
1540
  {
1427
1541
  "name": "AspectRatio",
@@ -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.28.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-tokens": "0.28.0",
37
+ "@handamade/psi-react": "0.28.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"