@handamade/psi-mcp 0.4.0 → 0.5.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 11 React 19 components, 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 13 React 19 components, 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
 
@@ -12,4 +12,11 @@ export declare const GETTING_STARTED: {
12
12
  themeAttribute: string;
13
13
  rules: string;
14
14
  };
15
+ /** Theme mechanics; travels with the index (spec: topics). */
16
+ export declare const THEMES_TOPIC: {
17
+ themes: string[];
18
+ attribute: string;
19
+ switching: string;
20
+ customerThemes: string;
21
+ };
15
22
  export declare function buildIndex(inputs: BuildInputs): Promise<PsiIndex>;
@@ -9,6 +9,8 @@ export const GETTING_STARTED = {
9
9
  "@handamade/psi-tokens/<theme>.css — one per theme you use (light|dark|acme|ember)",
10
10
  "@handamade/psi-tokens/components.css",
11
11
  "@handamade/psi-tokens/utilities.css — REQUIRED: .psi-container + reduced-motion zeroing",
12
+ "@handamade/psi-react/styles — REQUIRED: component CSS. Separate export; the JS never " +
13
+ "imports it for you. Without it every component renders unstyled (HAN-17 finding).",
12
14
  ],
13
15
  themeAttribute: 'Set data-psi-theme="light|dark|acme|ember" on <html> or a subtree root.',
14
16
  rules: "Sizes are px numbers (24 | 32 | 40 | 48), never S/M/L. Variants are flat " +
@@ -16,6 +18,15 @@ export const GETTING_STARTED = {
16
18
  "one accent per visual group (Tags exempt, D40); danger only for destructive actions. " +
17
19
  "Never hardcode colors — bind var(--psi-*).",
18
20
  };
21
+ /** Theme mechanics; travels with the index (spec: topics). */
22
+ export const THEMES_TOPIC = {
23
+ themes: [...THEMES],
24
+ attribute: 'Set data-psi-theme="light|dark|acme|ember" on <html> or a subtree root.',
25
+ switching: "Theme CSS files are scoped under [data-psi-theme] and co-import safely — " +
26
+ "import every theme you switch between at runtime.",
27
+ customerThemes: "light/dark are the core pair; acme and ember are customer brand themes " +
28
+ "(OKLCH formula overrides, same semantic token names).",
29
+ };
19
30
  async function readJson(path) {
20
31
  return JSON.parse(await readFile(path, "utf8"));
21
32
  }
@@ -32,6 +43,7 @@ export async function buildIndex(inputs) {
32
43
  name: c.name,
33
44
  description: c.description,
34
45
  props: c.props,
46
+ slots: c.slots,
35
47
  doc: await readFile(join(inputs.reactDocs, `${c.name}.md`), "utf8"),
36
48
  });
37
49
  }
@@ -50,6 +62,11 @@ export async function buildIndex(inputs) {
50
62
  components,
51
63
  tokens,
52
64
  scales: light.scales,
53
- topics: { ...guidance, "getting-started": GETTING_STARTED },
65
+ topics: {
66
+ ...guidance,
67
+ "getting-started": GETTING_STARTED,
68
+ themes: THEMES_TOPIC,
69
+ scales: light.scales,
70
+ },
54
71
  };
55
72
  }
package/dist/init.js CHANGED
@@ -26,7 +26,7 @@ export function renderAgentsBlock(index) {
26
26
  "- Sizes are px numbers (24 | 32 | 40 | 48), never S/M/L.",
27
27
  "- Variants are flat: accent | accent-subtle | neutral | neutral-subtle | ghost | danger | danger-subtle | outline. One accent per visual group (Tags exempt, D40); danger only for destructive actions.",
28
28
  "- Never hardcode colors — bind var(--psi-*) custom properties.",
29
- "- Import all four token CSS files: base.css, one theme css, components.css, utilities.css (utilities is REQUIRED — .psi-container + reduced-motion zeroing).",
29
+ "- Import all four token CSS files: base.css, one theme css, components.css, utilities.css (utilities is REQUIRED — .psi-container + reduced-motion zeroing) — PLUS @handamade/psi-react/styles (component CSS; separate export, never auto-imported).",
30
30
  `- Themes: ${index.themes.join(" | ")} — set data-psi-theme on <html> or a subtree root.`,
31
31
  "",
32
32
  `## Components (${index.components.length})`,
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.0",
2
+ "version": "0.5.0",
3
3
  "themes": [
4
4
  "light",
5
5
  "dark",
@@ -9,7 +9,7 @@
9
9
  "components": [
10
10
  {
11
11
  "name": "Button",
12
- "description": "",
12
+ "description": "Action trigger. With `href` renders an anchor with identical styling (D33); `disabled` then maps to `aria-disabled`.",
13
13
  "props": [
14
14
  {
15
15
  "name": "variant",
@@ -61,11 +61,12 @@
61
61
  "description": "Additional CSS class name(s) merged onto the component's root element."
62
62
  }
63
63
  ],
64
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Button\n\n\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"accent\" \\| \"accent-subtle\" \\| \"neutral\" \\| \"neutral-subtle\" \\| \"ghost\" \\| \"danger\" \\| \"danger-subtle\" \\| \"outline\"` | neutral | no | Visual variant. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height 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 the button. |\n| Tab | Focusable; visible focus ring via :focus-visible. |\n\nWith href it renders an <a>; disabled anchors get aria-disabled, lose the href attribute, and suppress activation (D33).\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"
64
+ "slots": [],
65
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Button\n\nAction trigger. With `href` renders an anchor with identical styling (D33); `disabled` then maps to `aria-disabled`.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"accent\" \\| \"accent-subtle\" \\| \"neutral\" \\| \"neutral-subtle\" \\| \"ghost\" \\| \"danger\" \\| \"danger-subtle\" \\| \"outline\"` | neutral | no | Visual variant. |\n| `size` | `24 \\| 32 \\| 40 \\| 48` | 32 | no | Height 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 the button. |\n| Tab | Focusable; visible focus ring via :focus-visible. |\n\nWith href it renders an <a>; disabled anchors get aria-disabled, lose the href attribute, and suppress activation (D33).\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"
65
66
  },
66
67
  {
67
68
  "name": "IconButton",
68
- "description": "",
69
+ "description": "Square icon-only button; same variant/size/href contract as Button. Give it an accessible name (`aria-label`).",
69
70
  "props": [
70
71
  {
71
72
  "name": "variant",
@@ -117,11 +118,12 @@
117
118
  "description": "Additional CSS class name(s) merged onto the component's root element."
118
119
  }
119
120
  ],
120
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# IconButton\n\n\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"
121
+ "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"
121
123
  },
122
124
  {
123
125
  "name": "Card",
124
- "description": "",
126
+ "description": "Content container with an optional edge-to-edge media slot; `stacked` (media above body) or `featured` (media beside body) layout.",
125
127
  "props": [
126
128
  {
127
129
  "name": "variant",
@@ -159,11 +161,12 @@
159
161
  "description": "Additional CSS class name(s) merged onto the component's root element."
160
162
  }
161
163
  ],
162
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Card\n\n\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"stacked\" \\| \"featured\"` | stacked | no | stacked = media above body; featured = media beside body (~1.6fr/1fr), stacks under md. |\n| `media` | `ReactNode` | — | no | Media slot rendered edge-to-edge (img, AspectRatio, …). |\n| `hoverLift` | `boolean` | false | no | Lift on hover: translateY(-6px) over --psi-duration-350. |\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## Theming\n\nOverride `--psi-card-*` 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"
164
+ "slots": [],
165
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Card\n\nContent container with an optional edge-to-edge media slot; `stacked` (media above body) or `featured` (media beside body) layout.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"stacked\" \\| \"featured\"` | stacked | no | stacked = media above body; featured = media beside body (~1.6fr/1fr), stacks under md. |\n| `media` | `ReactNode` | — | no | Media slot rendered edge-to-edge (img, AspectRatio, …). |\n| `hoverLift` | `boolean` | false | no | Lift on hover: translateY(-6px) over --psi-duration-350. |\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## Theming\n\nOverride `--psi-card-*` 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"
163
166
  },
164
167
  {
165
168
  "name": "Input",
166
- "description": "",
169
+ "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).",
167
170
  "props": [
168
171
  {
169
172
  "name": "size",
@@ -177,7 +180,7 @@
177
180
  "type": "boolean",
178
181
  "required": false,
179
182
  "default": false,
180
- "description": "Show error styling."
183
+ "description": "Show error styling. Inside a Field, the Field's error also lights this."
181
184
  },
182
185
  {
183
186
  "name": "ref",
@@ -194,11 +197,12 @@
194
197
  "description": "Additional CSS class name(s) merged onto the component's root element."
195
198
  }
196
199
  ],
197
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Input\n\n\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. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input>` 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 | Focuses the native <input>; focus ring on the field. |\n\nerror sets a red border only — pair with aria-invalid and aria-describedby pointing at your error text.\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"
200
+ "slots": [],
201
+ "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| `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"
198
202
  },
199
203
  {
200
204
  "name": "Select",
201
- "description": "",
205
+ "description": "Styled native `<select>` with pixel-true heights (24–48) and an error state. Inside a Field, id/aria-describedby/aria-invalid/required are wired automatically (D49).",
202
206
  "props": [
203
207
  {
204
208
  "name": "size",
@@ -212,7 +216,7 @@
212
216
  "type": "boolean",
213
217
  "required": false,
214
218
  "default": false,
215
- "description": "Show error styling."
219
+ "description": "Show error styling. Inside a Field, the Field's error also lights this."
216
220
  },
217
221
  {
218
222
  "name": "ref",
@@ -229,11 +233,170 @@
229
233
  "description": "Additional CSS class name(s) merged onto the component's root element."
230
234
  }
231
235
  ],
232
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Select\n\n\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). Replaces the native `size` attribute (which sets the visible option-row count on a listbox-style `<select>`) with the design system's control height in pixels. |\n| `error` | `boolean` | false | no | Show error styling. |\n| `ref` | `Ref<HTMLSelectElement>` | — | no | Forwarded ref to the underlying `<select>` 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 | Focuses the native <select>. |\n| Arrow keys / typeahead | Native option navigation. |\n\nerror: same contract as Input.\n\n## Theming\n\nOverride `--psi-select-*` 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"
236
+ "slots": [],
237
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Select\n\nStyled native `<select>` 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). Replaces the native `size` attribute (which sets the visible option-row count on a listbox-style `<select>`) with the design system's control height in pixels. |\n| `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |\n| `ref` | `Ref<HTMLSelectElement>` | — | no | Forwarded ref to the underlying `<select>` 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 | Focuses the native <select>. |\n| Arrow keys / typeahead | Native option navigation. |\n\nerror: same contract as Input — 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-select-*` 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"
238
+ },
239
+ {
240
+ "name": "Field",
241
+ "description": "Labeled form-row wrapper: label above, control, one message line below — description normally, error when set (aria-live). Auto-wires id/aria-describedby/aria-invalid into Input and Select (D49).",
242
+ "props": [
243
+ {
244
+ "name": "label",
245
+ "type": "ReactNode",
246
+ "required": false,
247
+ "default": null,
248
+ "description": "Label content; renders a <label> (or <legend> in group mode)."
249
+ },
250
+ {
251
+ "name": "description",
252
+ "type": "ReactNode",
253
+ "required": false,
254
+ "default": null,
255
+ "description": "Helper line under the control; replaced by error when error is set."
256
+ },
257
+ {
258
+ "name": "error",
259
+ "type": "ReactNode",
260
+ "required": false,
261
+ "default": null,
262
+ "description": "Error content; when truthy it replaces the description and switches the\nfield (and a wrapped Input/Select) into error state."
263
+ },
264
+ {
265
+ "name": "required",
266
+ "type": "boolean",
267
+ "required": false,
268
+ "default": false,
269
+ "description": "Renders the required marker and flows `required` to the control."
270
+ },
271
+ {
272
+ "name": "group",
273
+ "type": "boolean",
274
+ "required": false,
275
+ "default": false,
276
+ "description": "Group mode: fieldset/legend wrapping several self-labeled controls\n(Checkbox/Switch); the message describes the whole group."
277
+ },
278
+ {
279
+ "name": "htmlFor",
280
+ "type": "string",
281
+ "required": false,
282
+ "default": null,
283
+ "description": "Override the generated control id (pair it with the same id on the control)."
284
+ },
285
+ {
286
+ "name": "ref",
287
+ "type": "Ref<HTMLDivElement | HTMLFieldSetElement>",
288
+ "required": false,
289
+ "default": null,
290
+ "description": "Forwarded ref to the root element."
291
+ },
292
+ {
293
+ "name": "className",
294
+ "type": "string",
295
+ "required": false,
296
+ "default": null,
297
+ "description": "Additional CSS class name(s) merged onto the component's root element."
298
+ }
299
+ ],
300
+ "slots": [],
301
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Field\n\nLabeled form-row wrapper: label above, control, one message line below — description normally, error when set (aria-live). Auto-wires id/aria-describedby/aria-invalid into Input and Select (D49).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `label` | `ReactNode` | — | no | Label content; renders a <label> (or <legend> in group mode). |\n| `description` | `ReactNode` | — | no | Helper line under the control; replaced by error when error is set. |\n| `error` | `ReactNode` | — | no | Error content; when truthy it replaces the description and switches the field (and a wrapped Input/Select) into error state. |\n| `required` | `boolean` | false | no | Renders the required marker and flows `required` to the control. |\n| `group` | `boolean` | false | no | Group mode: fieldset/legend wrapping several self-labeled controls (Checkbox/Switch); the message describes the whole group. |\n| `htmlFor` | `string` | — | no | Override the generated control id (pair it with the same id on the control). |\n| `ref` | `Ref<HTMLDivElement \\| HTMLFieldSetElement>` | — | 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 | Focus moves to the wrapped control; the label is announced with it. |\n\nWires label association, aria-describedby and aria-invalid into a wrapped Input/Select automatically; the message line is aria-live=polite. Group mode renders fieldset/legend.\n\n## Theming\n\nOverride `--psi-field-*` 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"
302
+ },
303
+ {
304
+ "name": "Dialog",
305
+ "description": "Modal dialog on the native <dialog> top layer: focus trap, aria-modal and focus restore come from the platform; title/footer slots, Esc/backdrop/ close-button dismissal via onClose(reason) (D50).",
306
+ "props": [
307
+ {
308
+ "name": "open",
309
+ "type": "boolean",
310
+ "required": true,
311
+ "default": null,
312
+ "description": "Controlled open state; syncs to showModal()/close()."
313
+ },
314
+ {
315
+ "name": "onClose",
316
+ "type": "(reason: \"esc\" | \"backdrop\" | \"close-button\") => void",
317
+ "required": true,
318
+ "default": null,
319
+ "description": "Called on every dismissal attempt with its source; the consumer flips `open`."
320
+ },
321
+ {
322
+ "name": "title",
323
+ "type": "ReactNode",
324
+ "required": false,
325
+ "default": null,
326
+ "description": "Heading; renders an <h2> wired to aria-labelledby. Without it, pass aria-label."
327
+ },
328
+ {
329
+ "name": "footer",
330
+ "type": "ReactNode",
331
+ "required": false,
332
+ "default": null,
333
+ "description": "Action row (Buttons — one accent per group, danger for destructive)."
334
+ },
335
+ {
336
+ "name": "width",
337
+ "type": "400 | 560 | 720",
338
+ "required": false,
339
+ "default": 560,
340
+ "description": "Panel width in px (400 | 560 | 720)."
341
+ },
342
+ {
343
+ "name": "dismissible",
344
+ "type": "boolean",
345
+ "required": false,
346
+ "default": true,
347
+ "description": "false = no close button, Esc and backdrop swallowed — footer is the only exit."
348
+ },
349
+ {
350
+ "name": "ref",
351
+ "type": "Ref<HTMLDialogElement>",
352
+ "required": false,
353
+ "default": null,
354
+ "description": "Forwarded ref to the underlying <dialog> element."
355
+ },
356
+ {
357
+ "name": "className",
358
+ "type": "string",
359
+ "required": false,
360
+ "default": null,
361
+ "description": "Additional CSS class name(s) merged onto the component's root element."
362
+ }
363
+ ],
364
+ "slots": [
365
+ {
366
+ "name": "title",
367
+ "accepts": {
368
+ "contracts": [
369
+ "inline-content"
370
+ ]
371
+ },
372
+ "cardinality": "0..1",
373
+ "order": 1
374
+ },
375
+ {
376
+ "name": "body",
377
+ "accepts": {},
378
+ "cardinality": "0..*",
379
+ "order": 2
380
+ },
381
+ {
382
+ "name": "footer",
383
+ "accepts": {
384
+ "components": [
385
+ "Button"
386
+ ],
387
+ "contracts": [
388
+ "inline-content"
389
+ ]
390
+ },
391
+ "cardinality": "0..*",
392
+ "order": 3
393
+ }
394
+ ],
395
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Dialog\n\nModal dialog on the native <dialog> top layer: focus trap, aria-modal and focus restore come from the platform; title/footer slots, Esc/backdrop/ close-button dismissal via onClose(reason) (D50).\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `open` | `boolean` | — | yes | Controlled open state; syncs to showModal()/close(). |\n| `onClose` | `(reason: \"esc\" \\| \"backdrop\" \\| \"close-button\") => void` | — | yes | Called on every dismissal attempt with its source; the consumer flips `open`. |\n| `title` | `ReactNode` | — | no | Heading; renders an <h2> wired to aria-labelledby. Without it, pass aria-label. |\n| `footer` | `ReactNode` | — | no | Action row (Buttons — one accent per group, danger for destructive). |\n| `width` | `400 \\| 560 \\| 720` | 560 | no | Panel width in px (400 \\| 560 \\| 720). |\n| `dismissible` | `boolean` | true | no | false = no close button, Esc and backdrop swallowed — footer is the only exit. |\n| `ref` | `Ref<HTMLDialogElement>` | — | no | Forwarded ref to the underlying <dialog> element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Keyboard & assistive tech\n\n| Keys | Behavior |\n|---|---|\n| Esc | Dismisses via onClose('esc') when dismissible; swallowed otherwise. |\n| Tab | Focus is trapped inside by the native <dialog> top layer; restored on close. |\n\nRendered with showModal(): aria-modal, inert background and focus restore come from the platform. title wires aria-labelledby; without title, pass aria-label. Backdrop click dismisses only when dismissible.\n\n## Theming\n\nOverride `--psi-dialog-*` custom properties at any scope; 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"
233
396
  },
234
397
  {
235
398
  "name": "Checkbox",
236
- "description": "",
399
+ "description": "Native checkbox with a built-in label; for independent binary choices.",
237
400
  "props": [
238
401
  {
239
402
  "name": "children",
@@ -257,11 +420,12 @@
257
420
  "description": "Additional CSS class name(s) merged onto the component's root element."
258
421
  }
259
422
  ],
260
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Checkbox\n\n\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label text. |\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; 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"
423
+ "slots": [],
424
+ "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| `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; 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"
261
425
  },
262
426
  {
263
427
  "name": "Switch",
264
- "description": "",
428
+ "description": "On/off toggle (`role=\"switch\"`) with a built-in label; for settings that take effect immediately.",
265
429
  "props": [
266
430
  {
267
431
  "name": "children",
@@ -285,11 +449,12 @@
285
449
  "description": "Additional CSS class name(s) merged onto the component's root element."
286
450
  }
287
451
  ],
288
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Switch\n\n\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label text. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type=\"checkbox\" role=\"switch\">` 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 checkbox input with role=\"switch\"; announced as a switch, not a checkbox. |\n\n\n## Theming\n\nOverride `--psi-switch-*` 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"
452
+ "slots": [],
453
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Switch\n\nOn/off toggle (`role=\"switch\"`) with a built-in label; for settings that take effect immediately.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `children` | `ReactNode` | — | no | Label text. |\n| `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input type=\"checkbox\" role=\"switch\">` 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 checkbox input with role=\"switch\"; announced as a switch, not a checkbox. |\n\n\n## Theming\n\nOverride `--psi-switch-*` 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"
289
454
  },
290
455
  {
291
456
  "name": "Tag",
292
- "description": "",
457
+ "description": "Compact label for status or category; semantic variants, optional subtle tint and dismiss button.",
293
458
  "props": [
294
459
  {
295
460
  "name": "variant",
@@ -327,11 +492,12 @@
327
492
  "description": "Additional CSS class name(s) merged onto the component's root element."
328
493
  }
329
494
  ],
330
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tag\n\n\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"neutral\" \\| \"accent\" \\| \"success\" \\| \"warning\" \\| \"danger\"` | neutral | no | Semantic color variant. |\n| `subtle` | `boolean` | false | no | Use tinted background with semantic foreground. |\n| `onDismiss` | `(() => void)` | — | no | When provided, renders a dismiss button that calls this handler. |\n| `ref` | `Ref<HTMLSpanElement>` | — | no | Forwarded ref to the underlying `<span>` 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 (dismiss button) | onDismiss renders a real <button>; keyboard-dismissible. |\n\nPassive label otherwise; not in the tab order without onDismiss.\n\n## Theming\n\nOverride `--psi-tag-*` 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"
495
+ "slots": [],
496
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tag\n\nCompact label for status or category; semantic variants, optional subtle tint and dismiss button.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `variant` | `\"neutral\" \\| \"accent\" \\| \"success\" \\| \"warning\" \\| \"danger\"` | neutral | no | Semantic color variant. |\n| `subtle` | `boolean` | false | no | Use tinted background with semantic foreground. |\n| `onDismiss` | `(() => void)` | — | no | When provided, renders a dismiss button that calls this handler. |\n| `ref` | `Ref<HTMLSpanElement>` | — | no | Forwarded ref to the underlying `<span>` 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 (dismiss button) | onDismiss renders a real <button>; keyboard-dismissible. |\n\nPassive label otherwise; not in the tab order without onDismiss.\n\n## Theming\n\nOverride `--psi-tag-*` 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"
331
497
  },
332
498
  {
333
499
  "name": "Tooltip",
334
- "description": "",
500
+ "description": "Short hover/focus hint attached to a single trigger element.",
335
501
  "props": [
336
502
  {
337
503
  "name": "content",
@@ -355,11 +521,12 @@
355
521
  "description": "Preferred placement."
356
522
  }
357
523
  ],
358
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# Tooltip\n\n\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; 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"
524
+ "slots": [],
525
+ "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; 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"
359
526
  },
360
527
  {
361
528
  "name": "NavBar",
362
- "description": "",
529
+ "description": "Top navigation bar with brand, nav-link, and trailing-action slots.",
363
530
  "props": [
364
531
  {
365
532
  "name": "brand",
@@ -397,11 +564,12 @@
397
564
  "description": "Additional CSS class name(s) merged onto the component's root element."
398
565
  }
399
566
  ],
400
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# NavBar\n\n\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. |\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"
567
+ "slots": [],
568
+ "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. |\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- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.\n"
401
569
  },
402
570
  {
403
571
  "name": "AspectRatio",
404
- "description": "",
572
+ "description": "Fixed-aspect-ratio box that sizes its child media.",
405
573
  "props": [
406
574
  {
407
575
  "name": "ratio",
@@ -425,7 +593,8 @@
425
593
  "description": "Additional CSS class name(s) merged onto the component's root element."
426
594
  }
427
595
  ],
428
- "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# AspectRatio\n\n\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `ratio` | `number` | — | yes | Width/height ratio, e.g. 16/10 or 4/5. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the frame element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nOverride `--psi-aspect-ratio-*` 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"
596
+ "slots": [],
597
+ "doc": "<!-- Generated by emit-docs.ts — do not edit. -->\n# AspectRatio\n\nFixed-aspect-ratio box that sizes its child media.\n\n## Props\n\n| Prop | Type | Default | Required | Description |\n|---|---|---|---|---|\n| `ratio` | `number` | — | yes | Width/height ratio, e.g. 16/10 or 4/5. |\n| `ref` | `Ref<HTMLDivElement>` | — | no | Forwarded ref to the frame element. |\n| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |\n\n## Theming\n\nOverride `--psi-aspect-ratio-*` 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"
429
598
  }
430
599
  ],
431
600
  "tokens": [
@@ -1929,7 +2098,9 @@
1929
2098
  "Sizes are px numbers (24|32|40|48), never S/M/L.",
1930
2099
  "Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.",
1931
2100
  "Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.",
1932
- "--psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono)."
2101
+ "--psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).",
2102
+ "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.",
2103
+ "Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group."
1933
2104
  ],
1934
2105
  "states": {
1935
2106
  "hover": "L - 0.04",
@@ -2009,10 +2180,85 @@
2009
2180
  "@handamade/psi-tokens/base.css",
2010
2181
  "@handamade/psi-tokens/<theme>.css — one per theme you use (light|dark|acme|ember)",
2011
2182
  "@handamade/psi-tokens/components.css",
2012
- "@handamade/psi-tokens/utilities.css — REQUIRED: .psi-container + reduced-motion zeroing"
2183
+ "@handamade/psi-tokens/utilities.css — REQUIRED: .psi-container + reduced-motion zeroing",
2184
+ "@handamade/psi-react/styles — REQUIRED: component CSS. Separate export; the JS never imports it for you. Without it every component renders unstyled (HAN-17 finding)."
2013
2185
  ],
2014
2186
  "themeAttribute": "Set data-psi-theme=\"light|dark|acme|ember\" on <html> or a subtree root.",
2015
2187
  "rules": "Sizes are px numbers (24 | 32 | 40 | 48), never S/M/L. Variants are flat (accent, accent-subtle, neutral, neutral-subtle, ghost, danger, danger-subtle, outline); one accent per visual group (Tags exempt, D40); danger only for destructive actions. Never hardcode colors — bind var(--psi-*)."
2188
+ },
2189
+ "themes": {
2190
+ "themes": [
2191
+ "light",
2192
+ "dark",
2193
+ "acme",
2194
+ "ember"
2195
+ ],
2196
+ "attribute": "Set data-psi-theme=\"light|dark|acme|ember\" on <html> or a subtree root.",
2197
+ "switching": "Theme CSS files are scoped under [data-psi-theme] and co-import safely — import every theme you switch between at runtime.",
2198
+ "customerThemes": "light/dark are the core pair; acme and ember are customer brand themes (OKLCH formula overrides, same semantic token names)."
2199
+ },
2200
+ "scales": {
2201
+ "space": [
2202
+ 0,
2203
+ 2,
2204
+ 4,
2205
+ 6,
2206
+ 8,
2207
+ 12,
2208
+ 16,
2209
+ 20,
2210
+ 24,
2211
+ 32,
2212
+ 40,
2213
+ 48,
2214
+ 64,
2215
+ 80,
2216
+ 96,
2217
+ 120,
2218
+ 144
2219
+ ],
2220
+ "size": [
2221
+ 24,
2222
+ 32,
2223
+ 40,
2224
+ 48
2225
+ ],
2226
+ "radius": [
2227
+ 4,
2228
+ 6,
2229
+ 8,
2230
+ 12
2231
+ ],
2232
+ "motion": {
2233
+ "durations": [
2234
+ 150,
2235
+ 200,
2236
+ 350,
2237
+ 450,
2238
+ 600
2239
+ ],
2240
+ "easings": {
2241
+ "standard": "ease",
2242
+ "in-out": "ease-in-out",
2243
+ "soft": "cubic-bezier(0.2, 0.6, 0.2, 1)"
2244
+ }
2245
+ },
2246
+ "layout": {
2247
+ "breakpoints": {
2248
+ "sm": 560,
2249
+ "md": 960
2250
+ },
2251
+ "container": {
2252
+ "max": 1312,
2253
+ "gutter": 40,
2254
+ "gutterNarrow": 24
2255
+ },
2256
+ "zIndex": {
2257
+ "nav": 100,
2258
+ "overlay": 1000,
2259
+ "tooltip": 1100
2260
+ }
2261
+ }
2016
2262
  }
2017
2263
  }
2018
2264
  }
package/dist/store.js CHANGED
@@ -9,7 +9,10 @@ const TOPIC_SUMMARIES = {
9
9
  layout: "Breakpoints, container, z-index",
10
10
  recipes: "Composition recipes (mediaTint, sectionHeader)",
11
11
  tags: "Tag/badge rules incl. the accent exemption (D40)",
12
- "getting-started": "Install, the four required CSS imports, theme attribute, core rules",
12
+ "getting-started": "Install, the five required CSS imports, theme attribute, core rules",
13
+ themes: "Theme list (light|dark|acme|ember), data-psi-theme mechanics, customer themes",
14
+ scales: "Pixel scales — space, size, radius, motion, layout — the values behind " +
15
+ "--psi-space-* / --psi-radius-* / --psi-duration-*",
13
16
  };
14
17
  function componentSummary(c) {
15
18
  const axis = (name) => c.props.find((p) => p.name === name)?.type;
package/dist/types.d.ts CHANGED
@@ -11,6 +11,17 @@ export interface ComponentEntry {
11
11
  props: PropDoc[];
12
12
  /** Full generated markdown doc (includes the a11y/keyboard table). */
13
13
  doc: string;
14
+ /** Authored slot contracts (D45): what each named slot accepts, how
15
+ * many, in what order. Empty for components with no slots. */
16
+ slots: Array<{
17
+ name: string;
18
+ accepts: {
19
+ components?: string[];
20
+ contracts?: string[];
21
+ };
22
+ cardinality: string;
23
+ order?: number;
24
+ }>;
14
25
  }
15
26
  export interface Oklch {
16
27
  l: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handamade/psi-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server and agent-context generator for the Psi (Ψ) design system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,8 +32,8 @@
32
32
  "tsx": "^4.22.4",
33
33
  "typescript": "^5.7.0",
34
34
  "vitest": "^3.2.6",
35
- "@handamade/psi-react": "0.4.0",
36
- "@handamade/psi-tokens": "0.4.0"
35
+ "@handamade/psi-react": "0.5.0",
36
+ "@handamade/psi-tokens": "0.5.0"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"