@handamade/psi-mcp 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index-builder.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/init.js +1 -0
- package/dist/psi-index.json +470 -19
- package/dist/server.js +8 -4
- package/dist/store.d.ts +5 -3
- package/dist/store.js +18 -1
- package/dist/types.d.ts +43 -0
- package/llms.txt +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @handamade/psi-mcp
|
|
2
2
|
|
|
3
|
-
MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the
|
|
3
|
+
MCP server and agent-context generator for the Psi (Ψ) design system. Documents the system — the 13 React 19 components, 3 composition patterns (D47) that recipe them together with gap-annotated compose trees, every semantic token with its OKLCH derivation formula resolved across all four themes, and the usage guidance — through two read-only tools. It does not write code.
|
|
4
4
|
|
|
5
5
|
## Connect
|
|
6
6
|
|
package/dist/index-builder.js
CHANGED
|
@@ -32,6 +32,9 @@ async function readJson(path) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function buildIndex(inputs) {
|
|
34
34
|
const manifest = (await readJson(join(inputs.reactDist, "manifest.json")));
|
|
35
|
+
// Fail-fast is deliberate: requires @handamade/psi-react built first — same posture
|
|
36
|
+
// as the manifest/guidance reads above.
|
|
37
|
+
const patternsFile = (await readJson(join(inputs.reactDist, "patterns.json")));
|
|
35
38
|
const guidance = (await readJson(join(inputs.tokensDist, "guidance.json")));
|
|
36
39
|
const resolved = {};
|
|
37
40
|
for (const theme of THEMES) {
|
|
@@ -43,6 +46,7 @@ export async function buildIndex(inputs) {
|
|
|
43
46
|
name: c.name,
|
|
44
47
|
description: c.description,
|
|
45
48
|
props: c.props,
|
|
49
|
+
slots: c.slots,
|
|
46
50
|
doc: await readFile(join(inputs.reactDocs, `${c.name}.md`), "utf8"),
|
|
47
51
|
});
|
|
48
52
|
}
|
|
@@ -60,6 +64,7 @@ export async function buildIndex(inputs) {
|
|
|
60
64
|
themes: [...THEMES],
|
|
61
65
|
components,
|
|
62
66
|
tokens,
|
|
67
|
+
patterns: patternsFile.patterns,
|
|
63
68
|
scales: light.scales,
|
|
64
69
|
topics: {
|
|
65
70
|
...guidance,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PsiIndex } from "./types.js";
|
|
2
|
-
export type { PsiIndex, ComponentEntry, TokenEntry, PropDoc } from "./types.js";
|
|
2
|
+
export type { PsiIndex, ComponentEntry, TokenEntry, PropDoc, PatternEntry, PatternNode, PatternParameter, } from "./types.js";
|
|
3
3
|
export { buildIndex, GETTING_STARTED } from "./index-builder.js";
|
|
4
4
|
export { createStore } from "./store.js";
|
|
5
5
|
export type { Store, Brief, Detail } from "./store.js";
|
package/dist/init.js
CHANGED
|
@@ -28,6 +28,7 @@ export function renderAgentsBlock(index) {
|
|
|
28
28
|
"- Never hardcode colors — bind var(--psi-*) custom properties.",
|
|
29
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
|
+
"- Before composing multi-component UI, search for a matching pattern and ask its clarifying parameters; only freehand when no pattern matches.",
|
|
31
32
|
"",
|
|
32
33
|
`## Components (${index.components.length})`,
|
|
33
34
|
...index.components.map((c) => `- ${c.name}${axes(c)}`),
|
package/dist/psi-index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.6.0",
|
|
3
3
|
"themes": [
|
|
4
4
|
"light",
|
|
5
5
|
"dark",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
62
62
|
}
|
|
63
63
|
],
|
|
64
|
-
"
|
|
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",
|
|
@@ -117,7 +118,8 @@
|
|
|
117
118
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
118
119
|
}
|
|
119
120
|
],
|
|
120
|
-
"
|
|
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",
|
|
@@ -159,11 +161,12 @@
|
|
|
159
161
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
160
162
|
}
|
|
161
163
|
],
|
|
162
|
-
"
|
|
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": "Single-line text input with pixel-true heights (24–48) and an error state.",
|
|
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
|
-
"
|
|
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": "Styled native `<select>` with pixel-true heights (24–48) and an error state.",
|
|
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,7 +233,185 @@
|
|
|
229
233
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
230
234
|
}
|
|
231
235
|
],
|
|
232
|
-
"
|
|
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
|
+
{
|
|
302
|
+
"name": "label",
|
|
303
|
+
"accepts": {},
|
|
304
|
+
"cardinality": "0..1",
|
|
305
|
+
"order": 1
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"name": "body",
|
|
309
|
+
"accepts": {},
|
|
310
|
+
"cardinality": "1..*",
|
|
311
|
+
"order": 2
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"name": "description",
|
|
315
|
+
"accepts": {},
|
|
316
|
+
"cardinality": "0..1",
|
|
317
|
+
"order": 3
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
"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"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"name": "Dialog",
|
|
324
|
+
"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).",
|
|
325
|
+
"props": [
|
|
326
|
+
{
|
|
327
|
+
"name": "open",
|
|
328
|
+
"type": "boolean",
|
|
329
|
+
"required": true,
|
|
330
|
+
"default": null,
|
|
331
|
+
"description": "Controlled open state; syncs to showModal()/close()."
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"name": "onClose",
|
|
335
|
+
"type": "(reason: \"esc\" | \"backdrop\" | \"close-button\") => void",
|
|
336
|
+
"required": true,
|
|
337
|
+
"default": null,
|
|
338
|
+
"description": "Called on every dismissal attempt with its source; the consumer flips `open`."
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"name": "title",
|
|
342
|
+
"type": "ReactNode",
|
|
343
|
+
"required": false,
|
|
344
|
+
"default": null,
|
|
345
|
+
"description": "Heading; renders an <h2> wired to aria-labelledby. Without it, pass aria-label."
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"name": "footer",
|
|
349
|
+
"type": "ReactNode",
|
|
350
|
+
"required": false,
|
|
351
|
+
"default": null,
|
|
352
|
+
"description": "Action row (Buttons — one accent per group, danger for destructive)."
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
"name": "width",
|
|
356
|
+
"type": "400 | 560 | 720",
|
|
357
|
+
"required": false,
|
|
358
|
+
"default": 560,
|
|
359
|
+
"description": "Panel width in px (400 | 560 | 720)."
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"name": "dismissible",
|
|
363
|
+
"type": "boolean",
|
|
364
|
+
"required": false,
|
|
365
|
+
"default": true,
|
|
366
|
+
"description": "false = no close button, Esc and backdrop swallowed — footer is the only exit."
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"name": "ref",
|
|
370
|
+
"type": "Ref<HTMLDialogElement>",
|
|
371
|
+
"required": false,
|
|
372
|
+
"default": null,
|
|
373
|
+
"description": "Forwarded ref to the underlying <dialog> element."
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"name": "className",
|
|
377
|
+
"type": "string",
|
|
378
|
+
"required": false,
|
|
379
|
+
"default": null,
|
|
380
|
+
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
381
|
+
}
|
|
382
|
+
],
|
|
383
|
+
"slots": [
|
|
384
|
+
{
|
|
385
|
+
"name": "title",
|
|
386
|
+
"accepts": {
|
|
387
|
+
"contracts": [
|
|
388
|
+
"inline-content"
|
|
389
|
+
]
|
|
390
|
+
},
|
|
391
|
+
"cardinality": "0..1",
|
|
392
|
+
"order": 1
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"name": "body",
|
|
396
|
+
"accepts": {},
|
|
397
|
+
"cardinality": "0..*",
|
|
398
|
+
"order": 2
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"name": "footer",
|
|
402
|
+
"accepts": {
|
|
403
|
+
"components": [
|
|
404
|
+
"Button"
|
|
405
|
+
],
|
|
406
|
+
"contracts": [
|
|
407
|
+
"inline-content"
|
|
408
|
+
]
|
|
409
|
+
},
|
|
410
|
+
"cardinality": "0..*",
|
|
411
|
+
"order": 3
|
|
412
|
+
}
|
|
413
|
+
],
|
|
414
|
+
"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
415
|
},
|
|
234
416
|
{
|
|
235
417
|
"name": "Checkbox",
|
|
@@ -257,7 +439,8 @@
|
|
|
257
439
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
258
440
|
}
|
|
259
441
|
],
|
|
260
|
-
"
|
|
442
|
+
"slots": [],
|
|
443
|
+
"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
444
|
},
|
|
262
445
|
{
|
|
263
446
|
"name": "Switch",
|
|
@@ -285,7 +468,8 @@
|
|
|
285
468
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
286
469
|
}
|
|
287
470
|
],
|
|
288
|
-
"
|
|
471
|
+
"slots": [],
|
|
472
|
+
"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
473
|
},
|
|
290
474
|
{
|
|
291
475
|
"name": "Tag",
|
|
@@ -327,7 +511,8 @@
|
|
|
327
511
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
328
512
|
}
|
|
329
513
|
],
|
|
330
|
-
"
|
|
514
|
+
"slots": [],
|
|
515
|
+
"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
516
|
},
|
|
332
517
|
{
|
|
333
518
|
"name": "Tooltip",
|
|
@@ -355,7 +540,8 @@
|
|
|
355
540
|
"description": "Preferred placement."
|
|
356
541
|
}
|
|
357
542
|
],
|
|
358
|
-
"
|
|
543
|
+
"slots": [],
|
|
544
|
+
"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
545
|
},
|
|
360
546
|
{
|
|
361
547
|
"name": "NavBar",
|
|
@@ -397,7 +583,8 @@
|
|
|
397
583
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
398
584
|
}
|
|
399
585
|
],
|
|
400
|
-
"
|
|
586
|
+
"slots": [],
|
|
587
|
+
"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
588
|
},
|
|
402
589
|
{
|
|
403
590
|
"name": "AspectRatio",
|
|
@@ -425,7 +612,8 @@
|
|
|
425
612
|
"description": "Additional CSS class name(s) merged onto the component's root element."
|
|
426
613
|
}
|
|
427
614
|
],
|
|
428
|
-
"
|
|
615
|
+
"slots": [],
|
|
616
|
+
"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
617
|
}
|
|
430
618
|
],
|
|
431
619
|
"tokens": [
|
|
@@ -813,11 +1001,11 @@
|
|
|
813
1001
|
},
|
|
814
1002
|
"ember": {
|
|
815
1003
|
"oklch": {
|
|
816
|
-
"l": 0.
|
|
1004
|
+
"l": 0.14,
|
|
817
1005
|
"c": 0.011,
|
|
818
1006
|
"h": 72
|
|
819
1007
|
},
|
|
820
|
-
"hex": "#
|
|
1008
|
+
"hex": "#0c0805"
|
|
821
1009
|
}
|
|
822
1010
|
}
|
|
823
1011
|
},
|
|
@@ -1521,6 +1709,44 @@
|
|
|
1521
1709
|
}
|
|
1522
1710
|
}
|
|
1523
1711
|
},
|
|
1712
|
+
{
|
|
1713
|
+
"name": "fillStaticWhite",
|
|
1714
|
+
"formula": "ref(fgStaticWhite)",
|
|
1715
|
+
"values": {
|
|
1716
|
+
"light": {
|
|
1717
|
+
"oklch": {
|
|
1718
|
+
"l": 1,
|
|
1719
|
+
"c": 0,
|
|
1720
|
+
"h": 250
|
|
1721
|
+
},
|
|
1722
|
+
"hex": "#ffffff"
|
|
1723
|
+
},
|
|
1724
|
+
"dark": {
|
|
1725
|
+
"oklch": {
|
|
1726
|
+
"l": 1,
|
|
1727
|
+
"c": 0,
|
|
1728
|
+
"h": 250
|
|
1729
|
+
},
|
|
1730
|
+
"hex": "#ffffff"
|
|
1731
|
+
},
|
|
1732
|
+
"acme": {
|
|
1733
|
+
"oklch": {
|
|
1734
|
+
"l": 1,
|
|
1735
|
+
"c": 0,
|
|
1736
|
+
"h": 80
|
|
1737
|
+
},
|
|
1738
|
+
"hex": "#ffffff"
|
|
1739
|
+
},
|
|
1740
|
+
"ember": {
|
|
1741
|
+
"oklch": {
|
|
1742
|
+
"l": 1,
|
|
1743
|
+
"c": 0,
|
|
1744
|
+
"h": 49
|
|
1745
|
+
},
|
|
1746
|
+
"hex": "#ffffff"
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1524
1750
|
{
|
|
1525
1751
|
"name": "borderFaint",
|
|
1526
1752
|
"formula": "ref(fgPrimary) → alpha(0.08)",
|
|
@@ -1810,6 +2036,219 @@
|
|
|
1810
2036
|
"hex": "#0c0a09"
|
|
1811
2037
|
}
|
|
1812
2038
|
}
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
"name": "bgInverted",
|
|
2042
|
+
"formula": "ref(fgPrimary)",
|
|
2043
|
+
"values": {
|
|
2044
|
+
"light": {
|
|
2045
|
+
"oklch": {
|
|
2046
|
+
"l": 0.3,
|
|
2047
|
+
"c": 0.03,
|
|
2048
|
+
"h": 250
|
|
2049
|
+
},
|
|
2050
|
+
"hex": "#222f3c"
|
|
2051
|
+
},
|
|
2052
|
+
"dark": {
|
|
2053
|
+
"oklch": {
|
|
2054
|
+
"l": 0.93,
|
|
2055
|
+
"c": 0.01,
|
|
2056
|
+
"h": 250
|
|
2057
|
+
},
|
|
2058
|
+
"hex": "#e3e8ee"
|
|
2059
|
+
},
|
|
2060
|
+
"acme": {
|
|
2061
|
+
"oklch": {
|
|
2062
|
+
"l": 0.3,
|
|
2063
|
+
"c": 0.03,
|
|
2064
|
+
"h": 30
|
|
2065
|
+
},
|
|
2066
|
+
"hex": "#3c2825"
|
|
2067
|
+
},
|
|
2068
|
+
"ember": {
|
|
2069
|
+
"oklch": {
|
|
2070
|
+
"l": 0.949,
|
|
2071
|
+
"c": 0.011,
|
|
2072
|
+
"h": 72
|
|
2073
|
+
},
|
|
2074
|
+
"hex": "#f3ede6"
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
},
|
|
2078
|
+
{
|
|
2079
|
+
"name": "fgOnInverted",
|
|
2080
|
+
"formula": "ref(bgPrimary)",
|
|
2081
|
+
"values": {
|
|
2082
|
+
"light": {
|
|
2083
|
+
"oklch": {
|
|
2084
|
+
"l": 0.95,
|
|
2085
|
+
"c": 0.005,
|
|
2086
|
+
"h": 250
|
|
2087
|
+
},
|
|
2088
|
+
"hex": "#eceff2"
|
|
2089
|
+
},
|
|
2090
|
+
"dark": {
|
|
2091
|
+
"oklch": {
|
|
2092
|
+
"l": 0.15,
|
|
2093
|
+
"c": 0.01,
|
|
2094
|
+
"h": 250
|
|
2095
|
+
},
|
|
2096
|
+
"hex": "#080c0f"
|
|
2097
|
+
},
|
|
2098
|
+
"acme": {
|
|
2099
|
+
"oklch": {
|
|
2100
|
+
"l": 0.96,
|
|
2101
|
+
"c": 0.01,
|
|
2102
|
+
"h": 80
|
|
2103
|
+
},
|
|
2104
|
+
"hex": "#f5f1ea"
|
|
2105
|
+
},
|
|
2106
|
+
"ember": {
|
|
2107
|
+
"oklch": {
|
|
2108
|
+
"l": 0.147,
|
|
2109
|
+
"c": 0.004,
|
|
2110
|
+
"h": 49
|
|
2111
|
+
},
|
|
2112
|
+
"hex": "#0c0a09"
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
],
|
|
2117
|
+
"patterns": [
|
|
2118
|
+
{
|
|
2119
|
+
"id": "destructive-confirm",
|
|
2120
|
+
"intent": "Dialog confirming a destructive action",
|
|
2121
|
+
"match": [
|
|
2122
|
+
"delete confirmation",
|
|
2123
|
+
"are you sure",
|
|
2124
|
+
"destructive dialog",
|
|
2125
|
+
"confirm delete"
|
|
2126
|
+
],
|
|
2127
|
+
"compose": {
|
|
2128
|
+
"component": "Dialog",
|
|
2129
|
+
"slots": {
|
|
2130
|
+
"title": [
|
|
2131
|
+
"{content:title}"
|
|
2132
|
+
],
|
|
2133
|
+
"body": [
|
|
2134
|
+
"{content:consequence}"
|
|
2135
|
+
],
|
|
2136
|
+
"footer": [
|
|
2137
|
+
{
|
|
2138
|
+
"component": "Button",
|
|
2139
|
+
"props": {
|
|
2140
|
+
"variant": "ghost",
|
|
2141
|
+
"size": "{param:size}"
|
|
2142
|
+
},
|
|
2143
|
+
"content": "cancel-label"
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
"component": "Button",
|
|
2147
|
+
"props": {
|
|
2148
|
+
"variant": "danger",
|
|
2149
|
+
"size": "{param:size}"
|
|
2150
|
+
},
|
|
2151
|
+
"content": "confirm-label"
|
|
2152
|
+
}
|
|
2153
|
+
]
|
|
2154
|
+
}
|
|
2155
|
+
},
|
|
2156
|
+
"parameters": [
|
|
2157
|
+
{
|
|
2158
|
+
"key": "size",
|
|
2159
|
+
"ask": "Button size?",
|
|
2160
|
+
"options": [
|
|
2161
|
+
32,
|
|
2162
|
+
40
|
|
2163
|
+
],
|
|
2164
|
+
"default": 32
|
|
2165
|
+
}
|
|
2166
|
+
],
|
|
2167
|
+
"content": {
|
|
2168
|
+
"title": "<Delete the object?>",
|
|
2169
|
+
"consequence": "<what is permanently lost>",
|
|
2170
|
+
"cancel-label": "Cancel",
|
|
2171
|
+
"confirm-label": "<verb the object>"
|
|
2172
|
+
},
|
|
2173
|
+
"gaps": [],
|
|
2174
|
+
"blocked": false,
|
|
2175
|
+
"preset": "<Dialog\n footer={<>\n <Button size={32} variant=\"ghost\">Cancel</Button>\n <Button size={32} variant=\"danger\"><verb the object></Button>\n </>}\n title=\"<Delete the object?>\"\n>\n <what is permanently lost>\n</Dialog>\n"
|
|
2176
|
+
},
|
|
2177
|
+
{
|
|
2178
|
+
"id": "filter-toolbar",
|
|
2179
|
+
"intent": "Toolbar of filter controls: search, category select, active-filter tags",
|
|
2180
|
+
"match": [
|
|
2181
|
+
"filter toolbar",
|
|
2182
|
+
"filter bar",
|
|
2183
|
+
"search and filter",
|
|
2184
|
+
"faceted filters"
|
|
2185
|
+
],
|
|
2186
|
+
"compose": {
|
|
2187
|
+
"component": "Toolbar",
|
|
2188
|
+
"slots": {
|
|
2189
|
+
"body": [
|
|
2190
|
+
{
|
|
2191
|
+
"component": "Input"
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
"component": "Select"
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
"component": "Tag",
|
|
2198
|
+
"content": "example-filter"
|
|
2199
|
+
}
|
|
2200
|
+
]
|
|
2201
|
+
}
|
|
2202
|
+
},
|
|
2203
|
+
"parameters": [],
|
|
2204
|
+
"content": {
|
|
2205
|
+
"example-filter": "<active filter>"
|
|
2206
|
+
},
|
|
2207
|
+
"gaps": [
|
|
2208
|
+
"Toolbar"
|
|
2209
|
+
],
|
|
2210
|
+
"blocked": true,
|
|
2211
|
+
"preset": null
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
"id": "settings-form-row",
|
|
2215
|
+
"intent": "Labeled settings row: a self-labeled control with a group label and helper text",
|
|
2216
|
+
"match": [
|
|
2217
|
+
"settings row",
|
|
2218
|
+
"form row",
|
|
2219
|
+
"labeled switch",
|
|
2220
|
+
"preference toggle",
|
|
2221
|
+
"settings form"
|
|
2222
|
+
],
|
|
2223
|
+
"compose": {
|
|
2224
|
+
"component": "Field",
|
|
2225
|
+
"props": {
|
|
2226
|
+
"group": true
|
|
2227
|
+
},
|
|
2228
|
+
"slots": {
|
|
2229
|
+
"label": [
|
|
2230
|
+
"{content:row-label}"
|
|
2231
|
+
],
|
|
2232
|
+
"body": [
|
|
2233
|
+
{
|
|
2234
|
+
"component": "Switch",
|
|
2235
|
+
"content": "control-label"
|
|
2236
|
+
}
|
|
2237
|
+
],
|
|
2238
|
+
"description": [
|
|
2239
|
+
"{content:row-description}"
|
|
2240
|
+
]
|
|
2241
|
+
}
|
|
2242
|
+
},
|
|
2243
|
+
"parameters": [],
|
|
2244
|
+
"content": {
|
|
2245
|
+
"row-label": "<setting name>",
|
|
2246
|
+
"control-label": "<what turning it on does>",
|
|
2247
|
+
"row-description": "<one-line consequence of the setting>"
|
|
2248
|
+
},
|
|
2249
|
+
"gaps": [],
|
|
2250
|
+
"blocked": false,
|
|
2251
|
+
"preset": "<Field\n description=\"<one-line consequence of the setting>\"\n group={true}\n label=\"<setting name>\"\n>\n <Switch><what turning it on does></Switch>\n</Field>\n"
|
|
1813
2252
|
}
|
|
1814
2253
|
],
|
|
1815
2254
|
"scales": {
|
|
@@ -1873,6 +2312,11 @@
|
|
|
1873
2312
|
"overlay": 1000,
|
|
1874
2313
|
"tooltip": 1100
|
|
1875
2314
|
}
|
|
2315
|
+
},
|
|
2316
|
+
"scopes": {
|
|
2317
|
+
"space": [
|
|
2318
|
+
"gap"
|
|
2319
|
+
]
|
|
1876
2320
|
}
|
|
1877
2321
|
},
|
|
1878
2322
|
"topics": {
|
|
@@ -1929,7 +2373,9 @@
|
|
|
1929
2373
|
"Sizes are px numbers (24|32|40|48), never S/M/L.",
|
|
1930
2374
|
"Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.",
|
|
1931
2375
|
"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)."
|
|
2376
|
+
"--psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).",
|
|
2377
|
+
"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.",
|
|
2378
|
+
"Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group."
|
|
1933
2379
|
],
|
|
1934
2380
|
"states": {
|
|
1935
2381
|
"hover": "L - 0.04",
|
|
@@ -2087,6 +2533,11 @@
|
|
|
2087
2533
|
"overlay": 1000,
|
|
2088
2534
|
"tooltip": 1100
|
|
2089
2535
|
}
|
|
2536
|
+
},
|
|
2537
|
+
"scopes": {
|
|
2538
|
+
"space": [
|
|
2539
|
+
"gap"
|
|
2540
|
+
]
|
|
2090
2541
|
}
|
|
2091
2542
|
}
|
|
2092
2543
|
}
|
package/dist/server.js
CHANGED
|
@@ -4,8 +4,9 @@ export function createPsiServer(store, version) {
|
|
|
4
4
|
const server = new McpServer({ name: "psi", version });
|
|
5
5
|
server.registerTool("search", {
|
|
6
6
|
title: "Search the Psi design system",
|
|
7
|
-
description: "Keyword search over Psi components, semantic tokens, and
|
|
8
|
-
"Returns compact briefs with ids for the get tool. Empty query
|
|
7
|
+
description: "Keyword search over Psi components, composition patterns, semantic tokens, and " +
|
|
8
|
+
"guidance topics. Returns compact briefs with ids for the get tool. Empty query " +
|
|
9
|
+
"returns an overview.",
|
|
9
10
|
inputSchema: { query: z.string().describe("Keywords, e.g. 'button variants' or 'bg token'") },
|
|
10
11
|
}, async ({ query }) => ({
|
|
11
12
|
content: [{ type: "text", text: JSON.stringify(store.search(query), null, 1) }],
|
|
@@ -13,8 +14,11 @@ export function createPsiServer(store, version) {
|
|
|
13
14
|
server.registerTool("get", {
|
|
14
15
|
title: "Get full Psi detail by id",
|
|
15
16
|
description: "Full detail for one id from search: component (props, defaults, a11y doc), " +
|
|
16
|
-
"
|
|
17
|
-
"
|
|
17
|
+
"pattern (composition recipe: compose tree, clarifying parameters, content, " +
|
|
18
|
+
"gaps/blocked status, and a copy-paste preset when unblocked), token (formula + " +
|
|
19
|
+
"resolved OKLCH/hex in all four themes), or topic (guidance). Accepts " +
|
|
20
|
+
"'component:Button', 'pattern:destructive-confirm', 'token:bgPrimary', " +
|
|
21
|
+
"'topic:variants', or a bare name.",
|
|
18
22
|
inputSchema: { id: z.string().describe("An id from search results, or a bare name") },
|
|
19
23
|
}, async ({ id }) => {
|
|
20
24
|
const detail = store.get(id);
|
package/dist/store.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ComponentEntry, PsiIndex, TokenEntry } from "./types.js";
|
|
1
|
+
import type { ComponentEntry, PatternEntry, PsiIndex, TokenEntry } from "./types.js";
|
|
2
2
|
export interface Brief {
|
|
3
3
|
id: string;
|
|
4
|
-
kind: "component" | "token" | "topic";
|
|
4
|
+
kind: "component" | "token" | "topic" | "pattern";
|
|
5
5
|
title: string;
|
|
6
6
|
summary: string;
|
|
7
7
|
}
|
|
@@ -9,7 +9,9 @@ export type Detail = ({
|
|
|
9
9
|
kind: "component";
|
|
10
10
|
} & ComponentEntry) | ({
|
|
11
11
|
kind: "token";
|
|
12
|
-
} & TokenEntry) | {
|
|
12
|
+
} & TokenEntry) | ({
|
|
13
|
+
kind: "pattern";
|
|
14
|
+
} & PatternEntry) | {
|
|
13
15
|
kind: "topic";
|
|
14
16
|
name: string;
|
|
15
17
|
content: unknown;
|
package/dist/store.js
CHANGED
|
@@ -29,6 +29,12 @@ function componentSummary(c) {
|
|
|
29
29
|
parts.push(`props: ${c.props.map((p) => p.name).slice(0, 6).join(", ")}`);
|
|
30
30
|
return parts.join(" — ").slice(0, 220);
|
|
31
31
|
}
|
|
32
|
+
function patternSummary(p) {
|
|
33
|
+
let summary = `${p.intent} — ${p.match.join(", ")}, ${p.parameters.length} parameters`;
|
|
34
|
+
if (p.blocked)
|
|
35
|
+
summary += `, blocked (gaps: ${p.gaps.join(", ")})`;
|
|
36
|
+
return summary;
|
|
37
|
+
}
|
|
32
38
|
export function createStore(index) {
|
|
33
39
|
const briefs = [
|
|
34
40
|
...index.components.map((c) => ({
|
|
@@ -37,6 +43,12 @@ export function createStore(index) {
|
|
|
37
43
|
title: c.name,
|
|
38
44
|
summary: componentSummary(c),
|
|
39
45
|
})),
|
|
46
|
+
...index.patterns.map((p) => ({
|
|
47
|
+
id: `pattern:${p.id}`,
|
|
48
|
+
kind: "pattern",
|
|
49
|
+
title: p.id,
|
|
50
|
+
summary: patternSummary(p),
|
|
51
|
+
})),
|
|
40
52
|
...Object.keys(index.topics).map((name) => ({
|
|
41
53
|
id: `topic:${name}`,
|
|
42
54
|
kind: "topic",
|
|
@@ -67,7 +79,7 @@ export function createStore(index) {
|
|
|
67
79
|
function search(query) {
|
|
68
80
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
69
81
|
if (terms.length === 0) {
|
|
70
|
-
// Overview: all components
|
|
82
|
+
// Overview: all components, patterns, and topics (tokens are discoverable via query).
|
|
71
83
|
return briefs.filter((b) => b.kind !== "token").slice(0, MAX_RESULTS + 10);
|
|
72
84
|
}
|
|
73
85
|
return briefs
|
|
@@ -91,6 +103,11 @@ export function createStore(index) {
|
|
|
91
103
|
if (t)
|
|
92
104
|
return { kind: "token", ...t };
|
|
93
105
|
}
|
|
106
|
+
if (want("pattern")) {
|
|
107
|
+
const p = index.patterns.find((p) => p.id.toLowerCase() === name);
|
|
108
|
+
if (p)
|
|
109
|
+
return { kind: "pattern", ...p };
|
|
110
|
+
}
|
|
94
111
|
if (want("topic")) {
|
|
95
112
|
const key = Object.keys(index.topics).find((k) => k.toLowerCase() === name);
|
|
96
113
|
if (key)
|
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;
|
|
@@ -26,11 +37,43 @@ export interface TokenEntry {
|
|
|
26
37
|
hex: string;
|
|
27
38
|
}>;
|
|
28
39
|
}
|
|
40
|
+
/** A single node in a pattern's `compose` tree: a component instance, its
|
|
41
|
+
* props (which may hold `{param:key}` / `{content:key}` placeholders), and
|
|
42
|
+
* its slot fills (nested nodes or string literals / placeholders). Mirrors
|
|
43
|
+
* `packages/react/scripts/patterns.ts`'s `PatternNode` — the shape emitted
|
|
44
|
+
* into `packages/react/dist/patterns.json` (D47). */
|
|
45
|
+
export interface PatternNode {
|
|
46
|
+
component: string;
|
|
47
|
+
props?: Record<string, unknown>;
|
|
48
|
+
slots?: Record<string, Array<PatternNode | string>>;
|
|
49
|
+
content?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface PatternParameter {
|
|
52
|
+
key: string;
|
|
53
|
+
ask: string;
|
|
54
|
+
options: Array<string | number>;
|
|
55
|
+
default?: string | number;
|
|
56
|
+
}
|
|
57
|
+
/** Shape of one entry in `packages/react/dist/patterns.json` (D47/D48):
|
|
58
|
+
* an authored composition recipe plus its build-time validation/render
|
|
59
|
+
* results (`gaps`, `blocked`, `preset`). */
|
|
60
|
+
export interface PatternEntry {
|
|
61
|
+
id: string;
|
|
62
|
+
intent: string;
|
|
63
|
+
match: string[];
|
|
64
|
+
compose: PatternNode;
|
|
65
|
+
parameters: PatternParameter[];
|
|
66
|
+
content: Record<string, string>;
|
|
67
|
+
gaps: string[];
|
|
68
|
+
blocked: boolean;
|
|
69
|
+
preset: string | null;
|
|
70
|
+
}
|
|
29
71
|
export interface PsiIndex {
|
|
30
72
|
version: string;
|
|
31
73
|
themes: string[];
|
|
32
74
|
components: ComponentEntry[];
|
|
33
75
|
tokens: TokenEntry[];
|
|
76
|
+
patterns: PatternEntry[];
|
|
34
77
|
scales: Record<string, unknown>;
|
|
35
78
|
topics: Record<string, unknown>;
|
|
36
79
|
}
|
package/llms.txt
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Two read-only tools over a baked index of the generated artifacts:
|
|
4
4
|
- search(query): briefs {id, kind, title, summary}; empty query = overview
|
|
5
|
-
- get(id): component (props/defaults/a11y doc) | token (formula + OKLCH/hex per theme) | topic (guidance)
|
|
6
|
-
Ids: component:<Name>, token:<name>, topic:<key>; bare names resolve case-insensitively.
|
|
5
|
+
- get(id): component (props/defaults/a11y doc) | token (formula + OKLCH/hex per theme) | topic (guidance) | pattern (compose tree, clarifying parameters, content, gaps/blocked status, preset JSX)
|
|
6
|
+
Ids: component:<Name>, token:<name>, topic:<key>, pattern:<id>; bare names resolve case-insensitively.
|
|
7
7
|
|
|
8
8
|
Connect: https://psi.kurkin.de/mcp (Streamable HTTP) or `npx @handamade/psi-mcp` (stdio).
|
|
9
9
|
Agent context: `npx @handamade/psi-mcp init [--claude]` writes a marker-delimited block into AGENTS.md/CLAUDE.md (D44).
|
|
10
|
-
The index bakes packages/react/dist/manifest.json + packages/tokens/dist/{guidance.json,resolved/*.json} at publish; lockstep versioning keeps it truthful (D43).
|
|
10
|
+
The index bakes packages/react/dist/{manifest.json,patterns.json} (patterns.json: 3 composition patterns with clarifying parameters, blocked/gaps flags, preset JSX) + packages/tokens/dist/{guidance.json,resolved/*.json} at publish; lockstep versioning keeps it truthful (D43).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handamade/psi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.
|
|
36
|
-
"@handamade/psi-tokens": "0.
|
|
35
|
+
"@handamade/psi-react": "0.6.0",
|
|
36
|
+
"@handamade/psi-tokens": "0.6.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "tsx scripts/build-index.ts && tsc -p tsconfig.build.json"
|