@hirobius/design-system 0.19.1 → 0.21.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/AGENTS.md +123 -0
- package/CONSUMING.md +11 -6
- package/DESIGN.md +34 -21
- package/NOTICE.md +1 -1
- package/README.md +48 -13
- package/codemods/hds-prefix.mjs +231 -0
- package/codemods/jsx-fold.mjs +515 -0
- package/codemods/not-found-pattern.mjs +71 -0
- package/codemods/patterns-subpath.mjs +242 -81
- package/codemods/patterns-subpath.names.json +41 -15
- package/codemods/removed-0.20.json +194 -0
- package/codemods/tile-grid.mjs +283 -0
- package/codemods/unrewritable.mjs +291 -0
- package/dist/chunks/DemoStateContext-Cz5TmrYu.js +32 -0
- package/dist/chunks/empty-state-DNoThgfV.js +1227 -0
- package/dist/chunks/form-By82qZcC.js +66 -0
- package/dist/contexts.js +157 -78
- package/dist/fonts/geist-mono-400.woff2 +0 -0
- package/dist/fonts/satoshi-400.woff2 +0 -0
- package/dist/fonts/satoshi-500.woff2 +0 -0
- package/dist/fonts/satoshi-700.woff2 +0 -0
- package/dist/fonts.css +53 -0
- package/dist/form.js +1 -1
- package/dist/hirobius-ui.js +1600 -5354
- package/dist/manifest.js +12 -13
- package/dist/patterns.js +796 -510
- package/dist/styles.css +2 -2
- package/dist/tokens.css +1 -1
- package/dist/tokens.js +165 -65
- package/dist/types/src/app/components/alert-dialog.d.ts +5 -0
- package/dist/types/src/app/components/asset-img.d.ts +1 -0
- package/dist/types/src/app/components/avatar-group.d.ts +2 -2
- package/dist/types/src/app/components/badge.d.ts +8 -3
- package/dist/types/src/app/components/blockquote.d.ts +2 -2
- package/dist/types/src/app/components/box-sx.d.ts +110 -13
- package/dist/types/src/app/components/box.d.ts +6 -3
- package/dist/types/src/app/components/breadcrumb.d.ts +0 -5
- package/dist/types/src/app/components/button.d.ts +35 -4
- package/dist/types/src/app/components/card.d.ts +10 -10
- package/dist/types/src/app/components/combobox.d.ts +47 -11
- package/dist/types/src/app/components/container.d.ts +2 -2
- package/dist/types/src/app/components/data-table-section.d.ts +2 -1
- package/dist/types/src/app/components/destructive-section.d.ts +1 -0
- package/dist/types/src/app/components/dialog.d.ts +8 -1
- package/dist/types/src/app/components/disclosure.d.ts +1 -5
- package/dist/types/src/app/components/error-pattern.d.ts +6 -1
- package/dist/types/src/app/components/field.d.ts +1 -2
- package/dist/types/src/app/components/form-actions.d.ts +1 -0
- package/dist/types/src/app/components/form-control.d.ts +10 -0
- package/dist/types/src/app/components/grid.d.ts +15 -6
- package/dist/types/src/app/components/hds-tooltip.d.ts +12 -2
- package/dist/types/src/app/components/icon.d.ts +1 -0
- package/dist/types/src/app/components/inline-code.d.ts +1 -2
- package/dist/types/src/app/components/input.d.ts +8 -5
- package/dist/types/src/app/components/kbd.d.ts +2 -2
- package/dist/types/src/app/components/metadata-list.d.ts +2 -2
- package/dist/types/src/app/components/metric-tiles.d.ts +1 -0
- package/dist/types/src/app/components/page-header.d.ts +0 -2
- package/dist/types/src/app/components/popover.d.ts +8 -0
- package/dist/types/src/app/components/progress.d.ts +33 -6
- package/dist/types/src/app/components/segmented-control.d.ts +2 -26
- package/dist/types/src/app/components/select.d.ts +10 -1
- package/dist/types/src/app/components/skeleton.d.ts +3 -4
- package/dist/types/src/app/components/spinner.d.ts +1 -2
- package/dist/types/src/app/components/stack.d.ts +4 -1
- package/dist/types/src/app/components/stat.d.ts +5 -5
- package/dist/types/src/app/components/status-list-item.d.ts +1 -2
- package/dist/types/src/app/components/status-tile.d.ts +3 -2
- package/dist/types/src/app/components/surface.d.ts +2 -3
- package/dist/types/src/app/components/switcher.d.ts +1 -1
- package/dist/types/src/app/components/table.d.ts +6 -15
- package/dist/types/src/app/components/tag.d.ts +0 -6
- package/dist/types/src/app/components/text.d.ts +54 -8
- package/dist/types/src/app/components/textarea.d.ts +1 -2
- package/dist/types/src/app/components/timestamp.d.ts +1 -0
- package/dist/types/src/app/components/toast.d.ts +1 -2
- package/dist/types/src/app/components/tooltip.d.ts +1 -1
- package/dist/types/src/app/context/DemoStateContext.d.ts +1 -1
- package/dist/types/src/app/context/RouterContext.d.ts +5 -5
- package/dist/types/src/app/data/manifest-types.d.ts +10 -5
- package/dist/types/src/app/design-system/generated-token-refs.d.ts +45 -28
- package/dist/types/src/app/design-system/generated-token-values.d.ts +4 -0
- package/dist/types/src/app/design-system/tokens.d.ts +139 -18
- package/dist/types/src/icons.d.ts +1 -1
- package/dist/types/src/index.d.ts +76 -164
- package/dist/types/src/patterns.d.ts +20 -25
- package/dist/variables.css +47 -33
- package/docs/CONSUMING.md +80 -48
- package/hirobius.tokens.json +125 -88
- package/llms.txt +166 -59
- package/mcp/catalog.mjs +366 -0
- package/mcp/guide.mjs +315 -0
- package/mcp/hds-mcp.mjs +49 -0
- package/mcp/server.mjs +94 -0
- package/package.json +48 -27
- package/public/hds-manifest.json +2667 -6705
- package/public/llms/components.txt +45 -426
- package/public/llms/layout.txt +5 -5
- package/public/llms-full.txt +245 -506
- package/public/llms.txt +166 -59
- package/scripts/eslint-plugin-hds/index.d.mts +29 -0
- package/scripts/eslint-plugin-hds/index.mjs +55 -0
- package/scripts/eslint-plugin-hds/package.json +30 -0
- package/scripts/eslint-plugin-hds/rules/no-raw-controls.mjs +47 -0
- package/scripts/eslint-plugin-hds/rules/no-raw-hex.mjs +70 -0
- package/scripts/eslint-plugin-hds/rules/no-raw-px-spacing.mjs +125 -0
- package/scripts/eslint-plugin-hds/rules/prefer-hds-layout-primitive.mjs +54 -0
- package/scripts/eslint-plugin-hds/rules/sx-token-first.mjs +111 -0
- package/scripts/eslint-plugin-hds/rules/utils.mjs +66 -0
- package/src/app/data/component-api.json +1082 -2913
- package/dist/chunks/LanguageContext-BxdCwiFg.js +0 -95
- package/dist/chunks/TokenDisplayContext-DKYJMVWk.js +0 -26
- package/dist/chunks/activity-feed-BGQ6v3wc.js +0 -22040
- package/dist/chunks/form-COP3vK9_.js +0 -66
- package/dist/types/src/app/components/activity-feed.d.ts +0 -73
- package/dist/types/src/app/components/app-shell.d.ts +0 -40
- package/dist/types/src/app/components/aspect-ratio.d.ts +0 -25
- package/dist/types/src/app/components/bleed.d.ts +0 -39
- package/dist/types/src/app/components/button-group.d.ts +0 -22
- package/dist/types/src/app/components/calendar.d.ts +0 -22
- package/dist/types/src/app/components/carousel.d.ts +0 -26
- package/dist/types/src/app/components/center.d.ts +0 -36
- package/dist/types/src/app/components/cinematic-link.d.ts +0 -18
- package/dist/types/src/app/components/circular-progress.d.ts +0 -28
- package/dist/types/src/app/components/cluster.d.ts +0 -39
- package/dist/types/src/app/components/command-palette.d.ts +0 -46
- package/dist/types/src/app/components/component-instance-matrix.d.ts +0 -27
- package/dist/types/src/app/components/context-menu.d.ts +0 -78
- package/dist/types/src/app/components/cover.d.ts +0 -43
- package/dist/types/src/app/components/date-input.d.ts +0 -31
- package/dist/types/src/app/components/date-range-input.d.ts +0 -34
- package/dist/types/src/app/components/date-time-input.d.ts +0 -30
- package/dist/types/src/app/components/doc-link-card.d.ts +0 -32
- package/dist/types/src/app/components/error-boundary.d.ts +0 -25
- package/dist/types/src/app/components/file-input.d.ts +0 -30
- package/dist/types/src/app/components/foundation-swatch.d.ts +0 -45
- package/dist/types/src/app/components/frame.d.ts +0 -38
- package/dist/types/src/app/components/heading-stack.d.ts +0 -52
- package/dist/types/src/app/components/history-card.d.ts +0 -12
- package/dist/types/src/app/components/hover-card.d.ts +0 -46
- package/dist/types/src/app/components/icon-button.d.ts +0 -22
- package/dist/types/src/app/components/image-lightbox.d.ts +0 -14
- package/dist/types/src/app/components/input-group.d.ts +0 -23
- package/dist/types/src/app/components/multi-selector.d.ts +0 -40
- package/dist/types/src/app/components/nav-group.d.ts +0 -29
- package/dist/types/src/app/components/nav-item.d.ts +0 -33
- package/dist/types/src/app/components/not-found-pattern.d.ts +0 -1
- package/dist/types/src/app/components/overflow-list.d.ts +0 -21
- package/dist/types/src/app/components/selectable-card.d.ts +0 -29
- package/dist/types/src/app/components/side-nav.d.ts +0 -23
- package/dist/types/src/app/components/sketch.d.ts +0 -19
- package/dist/types/src/app/components/stacked-card-rail.d.ts +0 -32
- package/dist/types/src/app/components/status-dot.d.ts +0 -25
- package/dist/types/src/app/components/stepper-field.d.ts +0 -12
- package/dist/types/src/app/components/stepper.d.ts +0 -33
- package/dist/types/src/app/components/text-lockup.d.ts +0 -38
- package/dist/types/src/app/components/tile-grid.d.ts +0 -17
- package/dist/types/src/app/components/time-input.d.ts +0 -24
- package/dist/types/src/app/components/toggle-button.d.ts +0 -32
- package/dist/types/src/app/components/token.d.ts +0 -90
- package/dist/types/src/app/components/tokenUtils.d.ts +0 -38
- package/dist/types/src/app/components/tokenizer.d.ts +0 -24
- package/dist/types/src/app/components/toolbar.d.ts +0 -36
- package/dist/types/src/app/components/top-nav.d.ts +0 -23
- package/dist/types/src/app/components/tree-list.d.ts +0 -30
- package/dist/types/src/app/data/nav-model.d.ts +0 -39
- package/dist/types/src/app/hooks/useFocusVisible.d.ts +0 -9
- package/dist/types/src/app/layouts/CaseStudyLayout.d.ts +0 -20
- package/dist/types/src/app/layouts/DocLayout.d.ts +0 -14
- package/dist/types/src/app/layouts/EmbeddedDocLayoutContext.d.ts +0 -6
- package/dist/types/src/app/layouts/HdsDocsShell.d.ts +0 -72
- package/dist/types/src/app/layouts/HdsSystemDocLayout.d.ts +0 -14
- package/dist/types/src/app/lib/hds-search.d.ts +0 -45
- package/dist/types/src/app/lib/nav-active.d.ts +0 -16
- package/dist/types/src/app/lib/navLevels.d.ts +0 -4
package/mcp/catalog.mjs
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `hds` MCP server's tools, built from the data the package already ships.
|
|
3
|
+
*
|
|
4
|
+
* Interface: `loadCatalog(packageRoot)` returns `{ instructions, tools }`, where
|
|
5
|
+
* each tool is `{ name, description, inputSchema, run(args) }` and `run` returns
|
|
6
|
+
* a plain object the server serialises. Every result is kept under RESULT_BUDGET
|
|
7
|
+
* bytes as it travels (the JSON text, escaped again inside the JSON-RPC
|
|
8
|
+
* response line): an agent asks a narrow question and gets a narrow answer, never
|
|
9
|
+
* the 400 KB manifest.
|
|
10
|
+
*
|
|
11
|
+
* Sources, all inside the installed package:
|
|
12
|
+
* src/app/data/component-api.json props, descriptions, usage contracts
|
|
13
|
+
* public/hds-manifest.json compound parts, keyboard, tokens (cssVar, light/dark values)
|
|
14
|
+
* codemods/patterns-subpath.names.json which names import from /patterns
|
|
15
|
+
* mcp/guide.mjs which component answers which need
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { HOOKS, INTENTS, coreComponents } from './guide.mjs';
|
|
20
|
+
|
|
21
|
+
export const RESULT_BUDGET = 1900;
|
|
22
|
+
|
|
23
|
+
/** Bytes a result costs inside a tools/call response: its JSON, escaped as a string. */
|
|
24
|
+
const bytes = (value) => Buffer.byteLength(JSON.stringify(JSON.stringify(value)));
|
|
25
|
+
|
|
26
|
+
const ROOT_IMPORT = '@hirobius/design-system';
|
|
27
|
+
|
|
28
|
+
const STOPWORDS = new Set(
|
|
29
|
+
'a an and as at by for from in into is it of on or the to with without show use'.split(' '),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const readJson = (root, rel) => JSON.parse(readFileSync(join(root, rel), 'utf8'));
|
|
33
|
+
|
|
34
|
+
const words = (text) =>
|
|
35
|
+
String(text ?? '')
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.split(/[^a-z0-9.]+/)
|
|
38
|
+
.filter((w) => w && !STOPWORDS.has(w));
|
|
39
|
+
|
|
40
|
+
function firstSentence(text = '', max = 160) {
|
|
41
|
+
const flat = String(text).replace(/\s+/g, ' ').trim();
|
|
42
|
+
const m = /^(.+?[.!?])(?=\s|$)/.exec(flat);
|
|
43
|
+
const s = m ? m[1] : flat;
|
|
44
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Levenshtein distance, for "did you mean" on a misspelt component name. */
|
|
48
|
+
function editDistance(a, b) {
|
|
49
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
50
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
51
|
+
const cur = [i];
|
|
52
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
53
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
54
|
+
}
|
|
55
|
+
prev = cur;
|
|
56
|
+
}
|
|
57
|
+
return prev[b.length];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Drops the entries of `list` from the end until `make(list)` fits the budget. */
|
|
61
|
+
function fitList(list, make) {
|
|
62
|
+
let n = list.length;
|
|
63
|
+
let out = make(list, 0);
|
|
64
|
+
while (n > 0 && bytes(out) > RESULT_BUDGET) {
|
|
65
|
+
n -= 1;
|
|
66
|
+
out = make(list.slice(0, n), list.length - n);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function loadCatalog(root) {
|
|
72
|
+
const api = readJson(root, 'src/app/data/component-api.json').components;
|
|
73
|
+
const manifest = readJson(root, 'public/hds-manifest.json');
|
|
74
|
+
const patternNames = new Set(readJson(root, 'codemods/patterns-subpath.names.json').names);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The flags both search_components and get_component put on a component:
|
|
78
|
+
* `core` when the guide prefers it, `insteadFor` for each need it is the
|
|
79
|
+
* wrong answer to (the guide names `use` instead).
|
|
80
|
+
*/
|
|
81
|
+
const steer = (name) => {
|
|
82
|
+
const insteadFor = INTENTS.filter((i) => i.avoid.includes(name)).map((i) => ({
|
|
83
|
+
need: i.need,
|
|
84
|
+
use: i.use[0],
|
|
85
|
+
}));
|
|
86
|
+
return {
|
|
87
|
+
...(core.has(name) ? { core: true } : {}),
|
|
88
|
+
...(insteadFor.length ? { insteadFor } : {}),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
const core = new Set(coreComponents());
|
|
92
|
+
const components = Object.keys(api)
|
|
93
|
+
.filter((name) => !api[name].hidden)
|
|
94
|
+
.sort();
|
|
95
|
+
const byLower = new Map(components.map((n) => [n.toLowerCase(), n]));
|
|
96
|
+
const importOf = (name) => (patternNames.has(name) ? `${ROOT_IMPORT}/patterns` : ROOT_IMPORT);
|
|
97
|
+
|
|
98
|
+
const tokens = Object.values(manifest.tokens ?? {}).flat();
|
|
99
|
+
|
|
100
|
+
// Flat aliases of compound parts (DialogContent for Dialog.Content): searchable by
|
|
101
|
+
// exact name only, so a purpose search returns the root component, not its parts.
|
|
102
|
+
const partAliases = new Set();
|
|
103
|
+
for (const [name, spec] of Object.entries(manifest.componentSpecs ?? {})) {
|
|
104
|
+
for (const m of spec.compoundMembers ?? [])
|
|
105
|
+
partAliases.add(`${name}${typeof m === 'string' ? m : m.name}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── search_components ───────────────────────────────────────────────────
|
|
109
|
+
function rankComponents(query) {
|
|
110
|
+
const q = String(query ?? '').trim();
|
|
111
|
+
const qWords = words(q);
|
|
112
|
+
const compact = q.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
113
|
+
const scores = new Map(components.map((n) => [n, 0]));
|
|
114
|
+
const add = (name, n) => scores.has(name) && scores.set(name, scores.get(name) + n);
|
|
115
|
+
|
|
116
|
+
const matchedIntents = [];
|
|
117
|
+
for (const intent of INTENTS) {
|
|
118
|
+
const hits =
|
|
119
|
+
q.toLowerCase() === intent.id
|
|
120
|
+
? 3
|
|
121
|
+
: qWords.filter((w) => intent.keywords.includes(w)).length;
|
|
122
|
+
if (!hits) continue;
|
|
123
|
+
matchedIntents.push({ intent, hits });
|
|
124
|
+
intent.use.forEach((name, i) => add(name, 10 * hits + (i === 0 ? 5 : 0)));
|
|
125
|
+
}
|
|
126
|
+
for (const name of components) {
|
|
127
|
+
const lower = name.toLowerCase();
|
|
128
|
+
if (lower === compact) add(name, 100);
|
|
129
|
+
else if (partAliases.has(name) && !core.has(name)) continue;
|
|
130
|
+
else if (qWords.some((w) => w.length > 2 && (lower.includes(w) || w.includes(lower))))
|
|
131
|
+
add(name, 20);
|
|
132
|
+
const entry = api[name];
|
|
133
|
+
const text = words(`${entry.description} ${entry.usage?.when ?? ''}`);
|
|
134
|
+
add(name, 2 * qWords.filter((w) => text.includes(w)).length);
|
|
135
|
+
if (scores.get(name) > 0) {
|
|
136
|
+
if (core.has(name)) add(name, 1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const ranked = components
|
|
140
|
+
.filter((n) => scores.get(n) > 0)
|
|
141
|
+
.sort((a, b) => scores.get(b) - scores.get(a) || (a < b ? -1 : 1));
|
|
142
|
+
matchedIntents.sort((a, b) => b.hits - a.hits);
|
|
143
|
+
return { ranked, intents: matchedIntents.map((m) => m.intent) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function searchComponents({ query, limit = 8 }) {
|
|
147
|
+
if (!String(query ?? '').trim()) throw new Error('query is required, e.g. "confirm delete"');
|
|
148
|
+
const { ranked, intents } = rankComponents(query);
|
|
149
|
+
const top = ranked.slice(0, Math.min(Math.max(1, limit), 8)).map((name) => ({
|
|
150
|
+
name,
|
|
151
|
+
import: importOf(name),
|
|
152
|
+
summary: firstSentence(api[name].usage?.when || api[name].description, 120),
|
|
153
|
+
...steer(name),
|
|
154
|
+
}));
|
|
155
|
+
return fitList(top, (results) => ({
|
|
156
|
+
results,
|
|
157
|
+
needs: intents.slice(0, 2).map((i) => ({ need: i.need, use: i.use })),
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── get_component ───────────────────────────────────────────────────────
|
|
162
|
+
function getComponent({ name }) {
|
|
163
|
+
const base = String(name ?? '')
|
|
164
|
+
.trim()
|
|
165
|
+
.split('.')[0];
|
|
166
|
+
const canonical = byLower.get(base.toLowerCase()) ?? (HOOKS[base] ? base : null);
|
|
167
|
+
if (!canonical) {
|
|
168
|
+
const target = base.toLowerCase();
|
|
169
|
+
const close = components
|
|
170
|
+
.map((n) => ({ n, d: editDistance(n.toLowerCase(), target) }))
|
|
171
|
+
.filter((c) => c.d <= Math.max(2, Math.floor(target.length / 3)))
|
|
172
|
+
.sort((a, b) => a.d - b.d || (a.n < b.n ? -1 : 1))
|
|
173
|
+
.slice(0, 3)
|
|
174
|
+
.map((c) => c.n);
|
|
175
|
+
throw new Error(
|
|
176
|
+
`No component named "${name}".${close.length ? ` Did you mean: ${close.join(', ')}?` : ''} Use search_components to find one.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (HOOKS[canonical]) return { name: canonical, kind: 'hook', ...HOOKS[canonical] };
|
|
180
|
+
|
|
181
|
+
const entry = api[canonical];
|
|
182
|
+
const spec = manifest.componentSpecs?.[canonical] ?? {};
|
|
183
|
+
const guide = INTENTS.filter((i) => i.use.includes(canonical)).map((i) => ({
|
|
184
|
+
need: i.need,
|
|
185
|
+
how: i.how,
|
|
186
|
+
}));
|
|
187
|
+
const propLine = (p, withDesc, typeMax) => {
|
|
188
|
+
const type = p.type.length > typeMax ? `${p.type.slice(0, typeMax - 1)}…` : p.type;
|
|
189
|
+
const def = p.default !== undefined ? ` = ${p.default}` : '';
|
|
190
|
+
const desc = withDesc && p.description ? ` — ${firstSentence(p.description, 90)}` : '';
|
|
191
|
+
return `${p.name}${p.required ? '' : '?'}: ${type}${def}${desc}`;
|
|
192
|
+
};
|
|
193
|
+
const build = (withDesc, typeMax, withHow) => ({
|
|
194
|
+
name: canonical,
|
|
195
|
+
import: importOf(canonical),
|
|
196
|
+
category: entry.category,
|
|
197
|
+
summary: firstSentence(entry.description, 200),
|
|
198
|
+
...steer(canonical),
|
|
199
|
+
...(entry.usage?.when ? { when: entry.usage.when } : {}),
|
|
200
|
+
...(entry.usage?.whenNot ? { whenNot: entry.usage.whenNot } : {}),
|
|
201
|
+
...(entry.usage?.useInstead?.length
|
|
202
|
+
? { useInstead: entry.usage.useInstead.map((u) => `${u.component}: ${u.reason}`) }
|
|
203
|
+
: {}),
|
|
204
|
+
...(spec.compoundMembers?.length
|
|
205
|
+
? {
|
|
206
|
+
parts: spec.compoundMembers.map(
|
|
207
|
+
(m) => `${canonical}.${typeof m === 'string' ? m : m.name}`,
|
|
208
|
+
),
|
|
209
|
+
}
|
|
210
|
+
: {}),
|
|
211
|
+
props: (entry.props ?? []).map((p) => propLine(p, withDesc, typeMax)),
|
|
212
|
+
...(guide.length ? { guide: withHow ? guide : guide.map((g) => ({ need: g.need })) } : {}),
|
|
213
|
+
});
|
|
214
|
+
for (const [withDesc, typeMax, withHow] of [
|
|
215
|
+
[true, 80, true],
|
|
216
|
+
[false, 80, true],
|
|
217
|
+
[false, 48, true],
|
|
218
|
+
[false, 48, false],
|
|
219
|
+
]) {
|
|
220
|
+
const out = build(withDesc, typeMax, withHow);
|
|
221
|
+
if (bytes(out) <= RESULT_BUDGET) return out;
|
|
222
|
+
}
|
|
223
|
+
const out = build(false, 32, false);
|
|
224
|
+
return fitList(out.props, (props, dropped) => ({
|
|
225
|
+
...out,
|
|
226
|
+
props,
|
|
227
|
+
...(dropped ? { propsOmitted: dropped, propsSource: 'src/app/data/component-api.json' } : {}),
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── search_tokens ───────────────────────────────────────────────────────
|
|
232
|
+
const tokenRow = (t) => ({
|
|
233
|
+
path: t.path,
|
|
234
|
+
cssVar: t.cssVar,
|
|
235
|
+
value: t.resolvedValue ?? t.value ?? t.composite ?? null,
|
|
236
|
+
...(t.dark ? { dark: t.dark.resolvedValue ?? t.dark.value } : {}),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
function searchTokens({ query, limit = 25 }) {
|
|
240
|
+
const q = String(query ?? '')
|
|
241
|
+
.trim()
|
|
242
|
+
.toLowerCase()
|
|
243
|
+
.replace(/^--/, '')
|
|
244
|
+
.replace(/^var\(--|\)$/g, '');
|
|
245
|
+
if (!q)
|
|
246
|
+
throw new Error(
|
|
247
|
+
'query is required: a path, a prefix ("semantic.space") or a word ("surface")',
|
|
248
|
+
);
|
|
249
|
+
const exact = tokens.find(
|
|
250
|
+
(t) => t.path.toLowerCase() === q || t.cssVar?.toLowerCase() === `--${q}`,
|
|
251
|
+
);
|
|
252
|
+
if (exact) {
|
|
253
|
+
return {
|
|
254
|
+
token: {
|
|
255
|
+
...tokenRow(exact),
|
|
256
|
+
type: exact.type,
|
|
257
|
+
...(exact.alias ? { alias: exact.alias } : {}),
|
|
258
|
+
description: firstSentence(exact.description, 300),
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const tierRank = { semantic: 0, role: 1, component: 2, primitive: 3 };
|
|
263
|
+
const qWords = q.split(/[\s,]+/).filter(Boolean);
|
|
264
|
+
const matches = tokens
|
|
265
|
+
.map((t) => {
|
|
266
|
+
const path = t.path.toLowerCase();
|
|
267
|
+
let score = 0;
|
|
268
|
+
if (path.startsWith(`${q}.`) || path.startsWith(q)) score = 3;
|
|
269
|
+
else if (qWords.every((w) => path.includes(w))) score = 2;
|
|
270
|
+
else if (qWords.every((w) => `${path} ${t.description ?? ''}`.toLowerCase().includes(w)))
|
|
271
|
+
score = 1;
|
|
272
|
+
return { t, score };
|
|
273
|
+
})
|
|
274
|
+
.filter((m) => m.score > 0)
|
|
275
|
+
.sort(
|
|
276
|
+
(a, b) =>
|
|
277
|
+
b.score - a.score ||
|
|
278
|
+
(tierRank[a.t.path.split('.')[0]] ?? 9) - (tierRank[b.t.path.split('.')[0]] ?? 9) ||
|
|
279
|
+
(a.t.path < b.t.path ? -1 : 1),
|
|
280
|
+
);
|
|
281
|
+
const cap = Math.min(Math.max(1, limit), 40);
|
|
282
|
+
const rows = matches.slice(0, cap).map((m) => tokenRow(m.t));
|
|
283
|
+
return fitList(rows, (results) => ({
|
|
284
|
+
total: matches.length,
|
|
285
|
+
results,
|
|
286
|
+
...(matches.length > results.length ? { more: matches.length - results.length } : {}),
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── list_core ───────────────────────────────────────────────────────────
|
|
291
|
+
function listCore() {
|
|
292
|
+
const names = [...core].filter((n) => api[n]);
|
|
293
|
+
// `id → use`: short enough that every need fits; search_components takes the id.
|
|
294
|
+
const needs = INTENTS.map((i) => `${i.id} → ${i.use.join(', ')}`);
|
|
295
|
+
// Needs are ordered most common first; what does not fit is one search_components away.
|
|
296
|
+
return fitList(needs, (shown, dropped) => ({
|
|
297
|
+
root: names.filter((n) => !patternNames.has(n)),
|
|
298
|
+
patterns: names.filter((n) => patternNames.has(n)),
|
|
299
|
+
hooks: Object.keys(HOOKS),
|
|
300
|
+
needs: shown,
|
|
301
|
+
...(dropped ? { moreNeeds: dropped, forMore: 'search_components with the need' } : {}),
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const tools = [
|
|
306
|
+
{
|
|
307
|
+
name: 'search_components',
|
|
308
|
+
description:
|
|
309
|
+
'Find HDS components by name or by what you need ("row of headline numbers", "confirm delete"). Returns up to 8 matches with their import path, and the guide entry for the need.',
|
|
310
|
+
inputSchema: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
properties: {
|
|
313
|
+
query: { type: 'string', description: 'A component name or a plain-language need.' },
|
|
314
|
+
limit: { type: 'integer', minimum: 1, maximum: 8 },
|
|
315
|
+
},
|
|
316
|
+
required: ['query'],
|
|
317
|
+
},
|
|
318
|
+
run: searchComponents,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: 'get_component',
|
|
322
|
+
description:
|
|
323
|
+
"One component's import path, usage contract (when, when not, use instead), props and compound parts, plus how the guide says to use it. Call before using a component.",
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: {
|
|
327
|
+
name: {
|
|
328
|
+
type: 'string',
|
|
329
|
+
description: 'Component name, e.g. "MetricTiles" or "AlertDialog".',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
required: ['name'],
|
|
333
|
+
},
|
|
334
|
+
run: getComponent,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'search_tokens',
|
|
338
|
+
description:
|
|
339
|
+
'Look up design tokens. An exact path or CSS variable returns that token with its description; a prefix ("semantic.space") or words ("surface page") return matching tokens, semantic tier first, with CSS variable and light/dark values.',
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: {
|
|
343
|
+
query: { type: 'string', description: 'Token path, prefix, CSS variable or words.' },
|
|
344
|
+
limit: { type: 'integer', minimum: 1, maximum: 40 },
|
|
345
|
+
},
|
|
346
|
+
required: ['query'],
|
|
347
|
+
},
|
|
348
|
+
run: searchTokens,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: 'list_core',
|
|
352
|
+
description:
|
|
353
|
+
'The core components to prefer, split by import path, the hooks, and one line per screen need (`id → components`; pass an id to search_components for its usage line). Call this first.',
|
|
354
|
+
inputSchema: { type: 'object', properties: {} },
|
|
355
|
+
run: listCore,
|
|
356
|
+
},
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
const instructions = [
|
|
360
|
+
'Lookups for @hirobius/design-system (HDS). Call list_core first, then get_component for each component before you use it.',
|
|
361
|
+
'Import pattern-tier components (Page, PageHeader, MetricTiles, DataTableSection, Form, FormActions) from @hirobius/design-system/patterns, everything else from @hirobius/design-system.',
|
|
362
|
+
'Style with component props and semantic tokens only (search_tokens); no raw hex, px or Tailwind classes. The package AGENTS.md has the full rules.',
|
|
363
|
+
].join(' ');
|
|
364
|
+
|
|
365
|
+
return { instructions, tools };
|
|
366
|
+
}
|
package/mcp/guide.mjs
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consumer guide: which HDS component answers which screen need.
|
|
3
|
+
*
|
|
4
|
+
* One source for both agent surfaces: the `hds` MCP server (mcp/catalog.mjs)
|
|
5
|
+
* serves it, and scripts/generate-agents-md.mjs projects it into the packaged
|
|
6
|
+
* AGENTS.md. Component facts (props, descriptions, import paths) are NOT here;
|
|
7
|
+
* they come from the shipped manifest and component-api.json. This file holds
|
|
8
|
+
* only the choices the data leaves open, each one a place where agents built
|
|
9
|
+
* the same screen differently in the 2026-10-05 consistency baseline
|
|
10
|
+
* (eval/consistency/DIAGNOSIS-2026-10-05.md).
|
|
11
|
+
*
|
|
12
|
+
* Every name in `use` must exist in component-api.json or be a listed hook;
|
|
13
|
+
* scripts/__tests__/generate-agents-md.test.mjs enforces it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Hooks are not components, so component-api.json has no entry for them. */
|
|
17
|
+
export const HOOKS = {
|
|
18
|
+
useToast: {
|
|
19
|
+
import: '@hirobius/design-system',
|
|
20
|
+
note: 'Inside a ToastProvider: `const { toast } = useToast(); toast({ title, description?, tone? })`.',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One row per screen need. `use` is the answer (in order of composition),
|
|
26
|
+
* `avoid` the alternatives agents reached for instead, `how` one line of usage.
|
|
27
|
+
* `keywords` feed search_components ranking. A component named in `avoid` is
|
|
28
|
+
* not wrong in general; it is the wrong answer for that one need, and the
|
|
29
|
+
* server tells an agent so (`insteadFor`). Every need has one answer: the four
|
|
30
|
+
* number components are the case that split the baseline, so a row of numbers
|
|
31
|
+
* is always MetricTiles.
|
|
32
|
+
*/
|
|
33
|
+
export const INTENTS = [
|
|
34
|
+
{
|
|
35
|
+
id: 'page-shell',
|
|
36
|
+
need: 'Outermost wrapper of a screen',
|
|
37
|
+
keywords: ['page', 'screen', 'shell', 'layout', 'wrapper', 'root'],
|
|
38
|
+
use: ['Page', 'Stack'],
|
|
39
|
+
avoid: ['Container', 'Box', 'a bare Stack as the root'],
|
|
40
|
+
how: '`<Page><Stack gap="spacious">…one child per section…</Stack></Page>`. `Container` is for a full-bleed surface that is not a page.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'page-header',
|
|
44
|
+
need: 'Screen title with breadcrumb, status and actions',
|
|
45
|
+
keywords: ['title', 'header', 'heading', 'breadcrumb', 'status', 'name'],
|
|
46
|
+
use: ['PageHeader', 'Breadcrumb', 'Badge'],
|
|
47
|
+
avoid: ['Text as the page title', 'a Stack row of Text and Badge'],
|
|
48
|
+
how: 'Exactly one per screen: `<PageHeader breadcrumb={<Breadcrumb items={[{ label, href }, { label }]} />} title="…" status={<Badge tone="success">Active</Badge>} />`.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'metrics',
|
|
52
|
+
need: 'Row of headline numbers (stats, KPIs, counts, totals)',
|
|
53
|
+
keywords: [
|
|
54
|
+
'metric',
|
|
55
|
+
'metrics',
|
|
56
|
+
'stat',
|
|
57
|
+
'stats',
|
|
58
|
+
'kpi',
|
|
59
|
+
'number',
|
|
60
|
+
'numbers',
|
|
61
|
+
'tile',
|
|
62
|
+
'tiles',
|
|
63
|
+
'headline',
|
|
64
|
+
'count',
|
|
65
|
+
'total',
|
|
66
|
+
],
|
|
67
|
+
use: ['MetricTiles', 'MetricTile'],
|
|
68
|
+
avoid: ['Stat', 'StatusTile', 'CardMetric', 'Card with Text'],
|
|
69
|
+
how: '`<MetricTiles><MetricTile label="Open projects" value="5" />…</MetricTiles>`; one MetricTile per number. `Stat` is for a number inside prose or a dense list, `Card.Metric` for one inside an existing `Card`, `StatusTile` for a state with notes (never a number).',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'table',
|
|
73
|
+
need: 'Table of records with row actions',
|
|
74
|
+
keywords: ['table', 'rows', 'records', 'list', 'data', 'grid', 'columns'],
|
|
75
|
+
use: ['DataTableSection', 'Badge', 'Button'],
|
|
76
|
+
avoid: ['Table on its own', 'Card around a Table'],
|
|
77
|
+
how: '`<DataTableSection title="Projects" columns={[{ key, label }]} rows={[{ key, cells: [...], actions: <Stack direction="row" gap="tight">…</Stack> }]} />`; leave the actions column out of `columns`.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'tabs',
|
|
81
|
+
need: 'Switch between panels of related content',
|
|
82
|
+
keywords: ['tabs', 'tab', 'panel', 'panels', 'sections', 'switch'],
|
|
83
|
+
use: ['Tabs', 'TabsList', 'TabsTrigger', 'TabsContent'],
|
|
84
|
+
avoid: ['SegmentedControl', 'Button rows'],
|
|
85
|
+
how: '`<Tabs defaultValue="a"><TabsList><TabsTrigger value="a">A</TabsTrigger></TabsList><TabsContent value="a">…</TabsContent></Tabs>`.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'status',
|
|
89
|
+
need: 'Status label in a header or table cell',
|
|
90
|
+
keywords: ['status', 'state', 'label', 'chip', 'pill', 'badge'],
|
|
91
|
+
use: ['Badge'],
|
|
92
|
+
avoid: ['Tag', 'Text'],
|
|
93
|
+
how: '`<Badge tone="success|info|warning|danger|neutral|inProgress">…</Badge>`; tone is the only styling input.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'form',
|
|
97
|
+
need: 'Form with fields and a submit button',
|
|
98
|
+
keywords: ['form', 'field', 'fields', 'input', 'textarea', 'notes', 'submit', 'save'],
|
|
99
|
+
use: ['Form', 'Textarea', 'Input', 'FormActions', 'Button'],
|
|
100
|
+
avoid: ['a raw `<form>`', 'a Stack of Buttons as the footer'],
|
|
101
|
+
how: '`<Form onSubmit={…}><Textarea label="Notes" … /><FormActions primary={<Button type="submit">Save note</Button>} /></Form>`; every field takes its own `label`.',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'confirm',
|
|
105
|
+
need: 'Confirm a destructive or irreversible action (delete, archive, remove)',
|
|
106
|
+
keywords: [
|
|
107
|
+
'confirm',
|
|
108
|
+
'confirmation',
|
|
109
|
+
'destructive',
|
|
110
|
+
'delete',
|
|
111
|
+
'archive',
|
|
112
|
+
'remove',
|
|
113
|
+
'modal',
|
|
114
|
+
'dialog',
|
|
115
|
+
],
|
|
116
|
+
use: ['AlertDialog', 'Button'],
|
|
117
|
+
avoid: ['Dialog', '`window.confirm`'],
|
|
118
|
+
how: 'Trigger, title, description, Cancel and Action parts: `<AlertDialog><AlertDialog.Trigger asChild><Button …>Archive</Button></AlertDialog.Trigger><AlertDialog.Content><AlertDialog.Header><AlertDialog.Title>…</AlertDialog.Title><AlertDialog.Description>…</AlertDialog.Description></AlertDialog.Header><AlertDialog.Footer><AlertDialog.Cancel asChild><Button variant="secondary">Cancel</Button></AlertDialog.Cancel><AlertDialog.Action asChild><Button tone="danger" onClick={…}>Archive</Button></AlertDialog.Action></AlertDialog.Footer></AlertDialog.Content></AlertDialog>`. Focus moves in and returns to the trigger for you.',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'feedback',
|
|
122
|
+
need: 'Brief confirmation after an action (saved, archived, sent)',
|
|
123
|
+
keywords: [
|
|
124
|
+
'toast',
|
|
125
|
+
'confirmation',
|
|
126
|
+
'saved',
|
|
127
|
+
'success',
|
|
128
|
+
'notify',
|
|
129
|
+
'notification',
|
|
130
|
+
'feedback',
|
|
131
|
+
'message',
|
|
132
|
+
],
|
|
133
|
+
use: ['ToastProvider', 'useToast'],
|
|
134
|
+
avoid: ['Badge', 'Alert', 'Text'],
|
|
135
|
+
how: 'Wrap the app root once in `<ToastProvider>`, then `toast({ title: "Note saved", tone: "success" })` from `useToast()`.',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'message',
|
|
139
|
+
need: 'Message that stays next to its content (error, warning, info)',
|
|
140
|
+
keywords: ['alert', 'error', 'warning', 'info', 'banner', 'callout'],
|
|
141
|
+
use: ['Alert'],
|
|
142
|
+
avoid: ['Text in a coloured Surface'],
|
|
143
|
+
how: '`<Alert tone="warning">…</Alert>`.',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'action',
|
|
147
|
+
need: 'Button or row action',
|
|
148
|
+
keywords: ['button', 'action', 'click', 'view', 'edit', 'cta'],
|
|
149
|
+
use: ['Button'],
|
|
150
|
+
avoid: ['a raw `<button>`', 'InlineLink for an action'],
|
|
151
|
+
how: 'Row actions: `<Button variant="tertiary" size="sm" aria-label="View Brand refresh">View</Button>`; the destructive one adds `tone="danger"`.',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'spacing',
|
|
155
|
+
need: 'Space between sections or items',
|
|
156
|
+
keywords: ['spacing', 'gap', 'stack', 'row', 'column', 'layout', 'columns'],
|
|
157
|
+
use: ['Stack', 'Grid'],
|
|
158
|
+
avoid: ['margins on children', 'raw px gaps such as px8'],
|
|
159
|
+
how: 'Stack `gap`: `spacious` between page sections, `normal` inside a section, `tight` between buttons. `Grid` for two-dimensional layout.',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'surface',
|
|
163
|
+
need: 'Padded background or a titled content object',
|
|
164
|
+
keywords: ['card', 'surface', 'panel', 'box', 'background', 'container'],
|
|
165
|
+
use: ['Surface', 'Card'],
|
|
166
|
+
avoid: ['a Box with background and padding'],
|
|
167
|
+
how: '`Surface` for a plain padded background; `Card` only when the content has a title or can be pressed.',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'progress',
|
|
171
|
+
need: 'Progress bar or ring',
|
|
172
|
+
keywords: ['progress', 'bar', 'percent', 'loading', 'completion'],
|
|
173
|
+
use: ['Progress'],
|
|
174
|
+
avoid: ['a hand-built bar'],
|
|
175
|
+
how: '`<Progress value={60} label="Upload" />`; inside an existing `Card`, `CardProgress`.',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'text',
|
|
179
|
+
need: 'Body copy',
|
|
180
|
+
keywords: ['text', 'copy', 'paragraph', 'body', 'typography'],
|
|
181
|
+
use: ['Text'],
|
|
182
|
+
avoid: ['Text as a section heading'],
|
|
183
|
+
how: 'Section headings come from the pattern that owns the section (`PageHeader` `title`, `DataTableSection` `title`), not from extra Text.',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'empty',
|
|
187
|
+
need: 'Empty state for a list or section',
|
|
188
|
+
keywords: ['empty', 'none', 'nothing', 'placeholder', 'zero'],
|
|
189
|
+
use: ['EmptyState'],
|
|
190
|
+
avoid: ['Text saying "No items"'],
|
|
191
|
+
how: '`DataTableSection` shows its own through `emptyTitle`; use `EmptyState` elsewhere.',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'danger-zone',
|
|
195
|
+
need: 'The one irreversible action of a screen (delete the client, close the account)',
|
|
196
|
+
keywords: ['danger', 'irreversible', 'delete', 'account', 'zone', 'destructive'],
|
|
197
|
+
use: ['DestructiveSection'],
|
|
198
|
+
avoid: ['a danger Button that acts without a confirm'],
|
|
199
|
+
how: 'Last on the screen: `<DestructiveSection title="Delete client" description="…" actionLabel="Delete" onConfirm={…} />`; it opens its own AlertDialog. A per-row destructive action uses AlertDialog directly.',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'metadata',
|
|
203
|
+
need: 'Read-only details as label and value pairs',
|
|
204
|
+
keywords: ['metadata', 'details', 'properties', 'key', 'value', 'pairs', 'record'],
|
|
205
|
+
use: ['MetadataList', 'Field'],
|
|
206
|
+
avoid: ['a Table with two columns', 'Text pairs in a Stack'],
|
|
207
|
+
how: '`<MetadataList items={[{ term: "Owner", description: "Ada" }]} />` for a set; `Field` for one pair.',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'people',
|
|
211
|
+
need: 'A person, or a group of people',
|
|
212
|
+
keywords: ['avatar', 'avatars', 'person', 'people', 'user', 'users', 'team', 'members'],
|
|
213
|
+
use: ['Avatar', 'AvatarGroup'],
|
|
214
|
+
avoid: ['an image in a rounded Box'],
|
|
215
|
+
how: '`<Avatar src alt initials />`; several in a row: `<AvatarGroup max={3}>…Avatars…</AvatarGroup>`.',
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'date',
|
|
219
|
+
need: 'A date or time',
|
|
220
|
+
keywords: ['date', 'time', 'timestamp', 'when', 'ago', 'due'],
|
|
221
|
+
use: ['Timestamp'],
|
|
222
|
+
avoid: ['a formatted string in Text'],
|
|
223
|
+
how: '`<Timestamp date="2026-10-14" />` renders a `<time dateTime>`; a date in a table cell may stay plain text.',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'shortcut',
|
|
227
|
+
need: 'A keyboard key or shortcut hint',
|
|
228
|
+
keywords: ['keyboard', 'shortcut', 'key', 'kbd', 'hotkey'],
|
|
229
|
+
use: ['Kbd'],
|
|
230
|
+
avoid: ['InlineCode', 'Text in a Box'],
|
|
231
|
+
how: '`<Kbd>⌘K</Kbd>`.',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'quote',
|
|
235
|
+
need: 'A quotation with attribution',
|
|
236
|
+
keywords: ['quote', 'quotation', 'testimonial', 'blockquote', 'citation'],
|
|
237
|
+
use: ['Blockquote'],
|
|
238
|
+
avoid: ['Callout for a quote', 'italic Text'],
|
|
239
|
+
how: '`<Blockquote attribution="Ada Lovelace">…</Blockquote>`.',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: 'rail',
|
|
243
|
+
need: 'A fixed-width side rail beside fluid content, or a row that stacks when narrow',
|
|
244
|
+
keywords: ['sidebar', 'rail', 'aside', 'responsive', 'switcher', 'stack', 'narrow', 'columns'],
|
|
245
|
+
use: ['Sidebar', 'Switcher'],
|
|
246
|
+
avoid: ['a Grid with media queries'],
|
|
247
|
+
how: '`Sidebar` (`side`, `sideWidth`) for rail + content; `Switcher` (`threshold`) for a row that flips to a column. Neither needs a media query.',
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'sticky',
|
|
251
|
+
need: 'Keep an element stuck while its region scrolls',
|
|
252
|
+
keywords: ['sticky', 'pin', 'fixed', 'scroll'],
|
|
253
|
+
use: ['Pin'],
|
|
254
|
+
avoid: ['position: sticky in style'],
|
|
255
|
+
how: '`<Pin top="…">…</Pin>` inside the scrolling region.',
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The ratified core set (scripts/lib/core-components.mjs, hds#254). Copied, not
|
|
261
|
+
* imported, because scripts/ does not ship; scripts/__tests__/generate-agents-md.test.mjs
|
|
262
|
+
* pins the two together.
|
|
263
|
+
*/
|
|
264
|
+
export const RATIFIED_CORE = [
|
|
265
|
+
'Alert',
|
|
266
|
+
'Avatar',
|
|
267
|
+
'Badge',
|
|
268
|
+
'Box',
|
|
269
|
+
'Breadcrumb',
|
|
270
|
+
'Button',
|
|
271
|
+
'Card',
|
|
272
|
+
'Checkbox',
|
|
273
|
+
'Combobox',
|
|
274
|
+
'Container',
|
|
275
|
+
'Dialog',
|
|
276
|
+
'Disclosure',
|
|
277
|
+
'Divider',
|
|
278
|
+
'EmptyState',
|
|
279
|
+
'Field',
|
|
280
|
+
'Grid',
|
|
281
|
+
'HdsRouterProvider',
|
|
282
|
+
'HdsThemeProvider',
|
|
283
|
+
'Icon',
|
|
284
|
+
'InlineLink',
|
|
285
|
+
'Input',
|
|
286
|
+
'Kbd',
|
|
287
|
+
'Menu',
|
|
288
|
+
'Pagination',
|
|
289
|
+
'Popover',
|
|
290
|
+
'Progress',
|
|
291
|
+
'Radio',
|
|
292
|
+
'SegmentedControl',
|
|
293
|
+
'Select',
|
|
294
|
+
'Skeleton',
|
|
295
|
+
'Slider',
|
|
296
|
+
'Spinner',
|
|
297
|
+
'Stack',
|
|
298
|
+
'Surface',
|
|
299
|
+
'Table',
|
|
300
|
+
'Tabs',
|
|
301
|
+
'Tag',
|
|
302
|
+
'Text',
|
|
303
|
+
'Textarea',
|
|
304
|
+
'ToastProvider',
|
|
305
|
+
'Toggle',
|
|
306
|
+
'Tooltip',
|
|
307
|
+
'VisuallyHidden',
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
/** The core set agents should prefer: the ratified core plus every `use` above, hooks excluded. */
|
|
311
|
+
export function coreComponents() {
|
|
312
|
+
const names = new Set(RATIFIED_CORE);
|
|
313
|
+
for (const intent of INTENTS) for (const name of intent.use) if (!HOOKS[name]) names.add(name);
|
|
314
|
+
return [...names].sort();
|
|
315
|
+
}
|