@hirobius/design-system 0.20.0 → 0.22.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 +125 -0
- package/CHANGELOG.md +969 -0
- package/CONSUMING.md +27 -11
- package/DESIGN.md +28 -17
- package/MIGRATIONS.md +603 -0
- package/NOTICE.md +1 -1
- package/README.md +39 -5
- package/UPGRADING.md +222 -0
- package/codemods/hds-prefix.mjs +28 -13
- package/codemods/jsx-fold.mjs +28 -13
- package/codemods/lib/atomic-write.mjs +28 -0
- package/codemods/lib/installed-version.mjs +354 -0
- package/codemods/lib/project.mjs +395 -0
- package/codemods/lib/record.mjs +110 -0
- package/codemods/lib/scan.mjs +323 -0
- package/codemods/lib/semver.mjs +57 -0
- package/codemods/not-found-pattern.mjs +3 -3
- package/codemods/patterns-subpath.mjs +28 -12
- package/codemods/registry.mjs +67 -0
- package/codemods/tile-grid.mjs +3 -3
- package/codemods/upgrade.mjs +909 -0
- package/dist/chunks/empty-state-DNoThgfV.js +1227 -0
- package/dist/chunks/form-By82qZcC.js +66 -0
- 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 +876 -877
- package/dist/manifest.js +3 -3
- package/dist/patterns.js +189 -170
- package/dist/styles.css +2 -2
- package/dist/tokens.css +1 -1
- package/dist/tokens.js +136 -57
- package/dist/types/src/app/components/alert-dialog.d.ts +5 -0
- package/dist/types/src/app/components/alert.d.ts +1 -1
- package/dist/types/src/app/components/asset-img.d.ts +1 -0
- package/dist/types/src/app/components/avatar-group.d.ts +1 -0
- package/dist/types/src/app/components/avatar.d.ts +1 -1
- package/dist/types/src/app/components/badge.d.ts +2 -2
- package/dist/types/src/app/components/blockquote.d.ts +1 -0
- package/dist/types/src/app/components/breadcrumb.d.ts +1 -1
- package/dist/types/src/app/components/button.d.ts +3 -1
- package/dist/types/src/app/components/callout.d.ts +1 -1
- package/dist/types/src/app/components/card.d.ts +4 -3
- package/dist/types/src/app/components/checkbox.d.ts +1 -1
- package/dist/types/src/app/components/code-block.d.ts +1 -1
- package/dist/types/src/app/components/combobox.d.ts +1 -1
- package/dist/types/src/app/components/data-table-section.d.ts +1 -0
- package/dist/types/src/app/components/destructive-section.d.ts +1 -0
- package/dist/types/src/app/components/dialog.d.ts +2 -1
- package/dist/types/src/app/components/disclosure.d.ts +1 -1
- package/dist/types/src/app/components/divider.d.ts +1 -1
- package/dist/types/src/app/components/empty-state.d.ts +1 -1
- package/dist/types/src/app/components/error-pattern.d.ts +6 -1
- package/dist/types/src/app/components/field.d.ts +1 -1
- 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/form.d.ts +1 -1
- package/dist/types/src/app/components/grid.d.ts +5 -0
- package/dist/types/src/app/components/hds-tooltip.d.ts +1 -1
- package/dist/types/src/app/components/icon.d.ts +1 -0
- package/dist/types/src/app/components/inline-link.d.ts +1 -1
- package/dist/types/src/app/components/input.d.ts +1 -1
- package/dist/types/src/app/components/kbd.d.ts +1 -0
- package/dist/types/src/app/components/menu.d.ts +1 -1
- package/dist/types/src/app/components/metadata-list.d.ts +1 -0
- package/dist/types/src/app/components/metric-tiles.d.ts +1 -0
- package/dist/types/src/app/components/pagination.d.ts +1 -1
- package/dist/types/src/app/components/popover.d.ts +1 -1
- package/dist/types/src/app/components/progress.d.ts +2 -2
- package/dist/types/src/app/components/radio.d.ts +1 -1
- package/dist/types/src/app/components/select.d.ts +1 -1
- package/dist/types/src/app/components/skeleton.d.ts +2 -2
- package/dist/types/src/app/components/slider.d.ts +1 -1
- package/dist/types/src/app/components/spinner.d.ts +1 -1
- package/dist/types/src/app/components/stat.d.ts +5 -4
- package/dist/types/src/app/components/status-list-item.d.ts +1 -1
- package/dist/types/src/app/components/status-tile.d.ts +4 -3
- package/dist/types/src/app/components/table.d.ts +6 -1
- package/dist/types/src/app/components/tabs.d.ts +1 -1
- package/dist/types/src/app/components/tag.d.ts +1 -1
- package/dist/types/src/app/components/text.d.ts +53 -6
- package/dist/types/src/app/components/textarea.d.ts +1 -1
- package/dist/types/src/app/components/timestamp.d.ts +1 -0
- package/dist/types/src/app/components/toast.d.ts +1 -1
- package/dist/types/src/app/components/toggle.d.ts +1 -1
- package/dist/types/src/app/data/manifest-types.d.ts +4 -0
- package/dist/types/src/app/design-system/generated-token-refs.d.ts +37 -28
- package/dist/types/src/app/design-system/generated-token-values.d.ts +3 -0
- package/dist/types/src/app/design-system/tokens.d.ts +117 -15
- package/dist/types/src/index.d.ts +122 -72
- package/dist/variables.css +41 -33
- package/docs/CONSUMING.md +63 -39
- package/hirobius.tokens.json +163 -112
- package/llms.txt +158 -50
- package/mcp/catalog.mjs +369 -0
- package/mcp/core-set.mjs +66 -0
- package/mcp/guide.mjs +272 -0
- package/mcp/hds-mcp.mjs +49 -0
- package/mcp/server.mjs +94 -0
- package/package.json +53 -11
- package/public/hds-manifest.json +430 -336
- package/public/llms/components.txt +4 -3
- package/public/llms/layout.txt +1 -1
- package/public/llms-full.txt +190 -70
- package/public/llms.txt +158 -50
- 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 +154 -93
- package/upgrade/index.json +208 -0
- package/upgrade/releases/0.17.0.json +962 -0
- package/upgrade/releases/0.18.0.json +8 -0
- package/upgrade/releases/0.19.0.json +233 -0
- package/upgrade/releases/0.19.1.json +8 -0
- package/upgrade/releases/0.20.0.json +5108 -0
- package/upgrade/releases/0.21.0.json +538 -0
- package/upgrade/releases/0.22.0.json +94 -0
- package/upgrade/schema.json +989 -0
- package/dist/chunks/empty-state-BBqhCx0S.js +0 -1217
- package/dist/chunks/form-COP3vK9_.js +0 -66
- package/dist/types/src/app/components/status-dot.d.ts +0 -39
package/mcp/core-set.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `core` rows of the hds#254 right-sizing table in
|
|
3
|
+
* docs/hds-architecture-2026-09-18.html (ratified 2026-09-26, #297), with the
|
|
4
|
+
* hds#315 renames applied: HdsCheckbox → Checkbox, HdsRadio → Radio,
|
|
5
|
+
* HdsSelect → Select, HdsSlider → Slider, HdsToggle → Toggle. The table has 42
|
|
6
|
+
* rows; ButtonGroup, ContextMenu and HoverCard were removed from the package in
|
|
7
|
+
* 0.20.0 (hds#394), which leaves 39. hds#393 adds Menu, Popover, Tooltip and
|
|
8
|
+
* HdsRouterProvider, each with a usage contract (and Menu, Popover and Tooltip
|
|
9
|
+
* a keyboard contract), which makes 43.
|
|
10
|
+
*
|
|
11
|
+
* THE ONLY FILE THAT LISTS THE CORE SET. It lives in mcp/ because mcp/ ships in
|
|
12
|
+
* the package and scripts/ does not; scripts/lib/core-components.mjs re-exports it,
|
|
13
|
+
* mcp/guide.mjs builds the recommended set on it, and the manifest `core` flag,
|
|
14
|
+
* llms.txt, SKILL.md and the README are generated from it
|
|
15
|
+
* (scripts/__tests__/core-set-sources.test.mjs fails on any disagreement).
|
|
16
|
+
*
|
|
17
|
+
* This is not the manifest `tier` field: 5 core components are `tier: pattern`
|
|
18
|
+
* and most `tier: primitive` components are not core. Read by
|
|
19
|
+
* scripts/check-contract-coverage.mjs.
|
|
20
|
+
*/
|
|
21
|
+
export const CORE_COMPONENTS = [
|
|
22
|
+
'Combobox',
|
|
23
|
+
'Checkbox',
|
|
24
|
+
'Radio',
|
|
25
|
+
'Select',
|
|
26
|
+
'Slider',
|
|
27
|
+
'Toggle',
|
|
28
|
+
'Input',
|
|
29
|
+
'SegmentedControl',
|
|
30
|
+
'Tag',
|
|
31
|
+
'Textarea',
|
|
32
|
+
'Box',
|
|
33
|
+
'Container',
|
|
34
|
+
'Disclosure',
|
|
35
|
+
'Divider',
|
|
36
|
+
'Grid',
|
|
37
|
+
'Stack',
|
|
38
|
+
'Surface',
|
|
39
|
+
'Avatar',
|
|
40
|
+
'Card',
|
|
41
|
+
'EmptyState',
|
|
42
|
+
'Field',
|
|
43
|
+
'Icon',
|
|
44
|
+
'Kbd',
|
|
45
|
+
'Table',
|
|
46
|
+
'Breadcrumb',
|
|
47
|
+
'InlineLink',
|
|
48
|
+
'Pagination',
|
|
49
|
+
'Tabs',
|
|
50
|
+
'Alert',
|
|
51
|
+
'Badge',
|
|
52
|
+
'Progress',
|
|
53
|
+
'Skeleton',
|
|
54
|
+
'Spinner',
|
|
55
|
+
'ToastProvider',
|
|
56
|
+
'Button',
|
|
57
|
+
'Dialog',
|
|
58
|
+
'Text',
|
|
59
|
+
'HdsThemeProvider',
|
|
60
|
+
'VisuallyHidden',
|
|
61
|
+
// hds#393 step 7
|
|
62
|
+
'Menu',
|
|
63
|
+
'Popover',
|
|
64
|
+
'Tooltip',
|
|
65
|
+
'HdsRouterProvider',
|
|
66
|
+
];
|
package/mcp/guide.mjs
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
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
|
+
import { CORE_COMPONENTS } from './core-set.mjs';
|
|
17
|
+
|
|
18
|
+
/** Hooks are not components, so component-api.json has no entry for them. */
|
|
19
|
+
export const HOOKS = {
|
|
20
|
+
useToast: {
|
|
21
|
+
import: '@hirobius/design-system',
|
|
22
|
+
note: 'Inside a ToastProvider: `const { toast } = useToast(); toast({ title, description?, tone? })`.',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One row per screen need. `use` is the answer (in order of composition),
|
|
28
|
+
* `avoid` the alternatives agents reached for instead, `how` one line of usage.
|
|
29
|
+
* `keywords` feed search_components ranking. A component named in `avoid` is
|
|
30
|
+
* not wrong in general; it is the wrong answer for that one need, and the
|
|
31
|
+
* server tells an agent so (`insteadFor`). Every need has one answer: the four
|
|
32
|
+
* number components are the case that split the baseline, so a row of numbers
|
|
33
|
+
* is always MetricTiles.
|
|
34
|
+
*/
|
|
35
|
+
export const INTENTS = [
|
|
36
|
+
{
|
|
37
|
+
id: 'page-shell',
|
|
38
|
+
need: 'Outermost wrapper of a screen',
|
|
39
|
+
keywords: ['page', 'screen', 'shell', 'layout', 'wrapper', 'root'],
|
|
40
|
+
use: ['Page', 'Stack'],
|
|
41
|
+
avoid: ['Container', 'Box', 'a bare Stack as the root'],
|
|
42
|
+
how: '`<Page><Stack gap="spacious">…one child per section…</Stack></Page>`. `Container` is for a full-bleed surface that is not a page.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'page-header',
|
|
46
|
+
need: 'Screen title with breadcrumb, status and actions',
|
|
47
|
+
keywords: ['title', 'header', 'heading', 'breadcrumb', 'status', 'name'],
|
|
48
|
+
use: ['PageHeader', 'Breadcrumb', 'Badge'],
|
|
49
|
+
avoid: ['Text as the page title', 'a Stack row of Text and Badge'],
|
|
50
|
+
how: 'Exactly one per screen: `<PageHeader breadcrumb={<Breadcrumb items={[{ label, href }, { label }]} />} title="…" status={<Badge tone="success">Active</Badge>} />`.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'metrics',
|
|
54
|
+
need: 'Row of headline numbers (stats, KPIs, counts, totals)',
|
|
55
|
+
keywords: [
|
|
56
|
+
'metric',
|
|
57
|
+
'metrics',
|
|
58
|
+
'stat',
|
|
59
|
+
'stats',
|
|
60
|
+
'kpi',
|
|
61
|
+
'number',
|
|
62
|
+
'numbers',
|
|
63
|
+
'tile',
|
|
64
|
+
'tiles',
|
|
65
|
+
'headline',
|
|
66
|
+
'count',
|
|
67
|
+
'total',
|
|
68
|
+
],
|
|
69
|
+
use: ['MetricTiles', 'MetricTile'],
|
|
70
|
+
avoid: ['Stat', 'StatusTile', 'CardMetric', 'Card with Text'],
|
|
71
|
+
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).',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'table',
|
|
75
|
+
need: 'Table of records with row actions',
|
|
76
|
+
keywords: ['table', 'rows', 'records', 'list', 'data', 'grid', 'columns'],
|
|
77
|
+
use: ['DataTableSection', 'Badge', 'Button'],
|
|
78
|
+
avoid: ['Table on its own', 'Card around a Table'],
|
|
79
|
+
how: '`<DataTableSection title="Projects" columns={[{ key, label }]} rows={[{ key, cells: [...], actions: <Stack direction="row" gap="tight">…</Stack> }]} />`; leave the actions column out of `columns`.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'tabs',
|
|
83
|
+
need: 'Switch between panels of related content',
|
|
84
|
+
keywords: ['tabs', 'tab', 'panel', 'panels', 'sections', 'switch'],
|
|
85
|
+
use: ['Tabs', 'TabsList', 'TabsTrigger', 'TabsContent'],
|
|
86
|
+
avoid: ['SegmentedControl', 'Button rows'],
|
|
87
|
+
how: '`<Tabs defaultValue="a"><TabsList><TabsTrigger value="a">A</TabsTrigger></TabsList><TabsContent value="a">…</TabsContent></Tabs>`.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'status',
|
|
91
|
+
need: 'Status label in a header or table cell',
|
|
92
|
+
keywords: ['status', 'state', 'label', 'chip', 'pill', 'badge'],
|
|
93
|
+
use: ['Badge'],
|
|
94
|
+
avoid: ['Tag', 'Text'],
|
|
95
|
+
how: '`<Badge tone="success|info|warning|danger|neutral|inProgress">…</Badge>`; tone is the only styling input.',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'form',
|
|
99
|
+
need: 'Form with fields and a submit button',
|
|
100
|
+
keywords: ['form', 'field', 'fields', 'input', 'textarea', 'notes', 'submit', 'save'],
|
|
101
|
+
use: ['Form', 'Textarea', 'Input', 'FormActions', 'Button'],
|
|
102
|
+
avoid: ['a raw `<form>`', 'a Stack of Buttons as the footer'],
|
|
103
|
+
how: '`<Form onSubmit={…}><Textarea label="Notes" … /><FormActions primary={<Button type="submit">Save note</Button>} /></Form>`; every field takes its own `label`.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'confirm',
|
|
107
|
+
need: 'Confirm a destructive or irreversible action (delete, archive, remove)',
|
|
108
|
+
keywords: [
|
|
109
|
+
'confirm',
|
|
110
|
+
'confirmation',
|
|
111
|
+
'destructive',
|
|
112
|
+
'delete',
|
|
113
|
+
'archive',
|
|
114
|
+
'remove',
|
|
115
|
+
'modal',
|
|
116
|
+
'dialog',
|
|
117
|
+
],
|
|
118
|
+
use: ['AlertDialog', 'Button'],
|
|
119
|
+
avoid: ['Dialog', '`window.confirm`'],
|
|
120
|
+
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.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'feedback',
|
|
124
|
+
need: 'Brief confirmation after an action (saved, archived, sent)',
|
|
125
|
+
keywords: [
|
|
126
|
+
'toast',
|
|
127
|
+
'confirmation',
|
|
128
|
+
'saved',
|
|
129
|
+
'success',
|
|
130
|
+
'notify',
|
|
131
|
+
'notification',
|
|
132
|
+
'feedback',
|
|
133
|
+
'message',
|
|
134
|
+
],
|
|
135
|
+
use: ['ToastProvider', 'useToast'],
|
|
136
|
+
avoid: ['Badge', 'Alert', 'Text'],
|
|
137
|
+
how: 'Wrap the app root once in `<ToastProvider>`, then `toast({ title: "Note saved", tone: "success" })` from `useToast()`.',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'message',
|
|
141
|
+
need: 'Message that stays next to its content (error, warning, info)',
|
|
142
|
+
keywords: ['alert', 'error', 'warning', 'info', 'banner', 'callout'],
|
|
143
|
+
use: ['Alert'],
|
|
144
|
+
avoid: ['Text in a coloured Surface'],
|
|
145
|
+
how: '`<Alert tone="warning">…</Alert>`.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'action',
|
|
149
|
+
need: 'Button or row action',
|
|
150
|
+
keywords: ['button', 'action', 'click', 'view', 'edit', 'cta'],
|
|
151
|
+
use: ['Button'],
|
|
152
|
+
avoid: ['a raw `<button>`', 'InlineLink for an action'],
|
|
153
|
+
how: 'Row actions: `<Button variant="tertiary" size="sm" aria-label="View Brand refresh">View</Button>`; the destructive one adds `tone="danger"`.',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'spacing',
|
|
157
|
+
need: 'Space between sections or items',
|
|
158
|
+
keywords: ['spacing', 'gap', 'stack', 'row', 'column', 'layout', 'columns'],
|
|
159
|
+
use: ['Stack', 'Grid'],
|
|
160
|
+
avoid: ['margins on children', 'raw px gaps such as px8'],
|
|
161
|
+
how: 'Stack `gap`: `spacious` between page sections, `normal` inside a section, `tight` between buttons. `Grid` for two-dimensional layout.',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'surface',
|
|
165
|
+
need: 'Padded background or a titled content object',
|
|
166
|
+
keywords: ['card', 'surface', 'panel', 'box', 'background', 'container'],
|
|
167
|
+
use: ['Surface', 'Card'],
|
|
168
|
+
avoid: ['a Box with background and padding'],
|
|
169
|
+
how: '`Surface` for a plain padded background; `Card` only when the content has a title or can be pressed.',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'progress',
|
|
173
|
+
need: 'Progress bar or ring',
|
|
174
|
+
keywords: ['progress', 'bar', 'percent', 'loading', 'completion'],
|
|
175
|
+
use: ['Progress'],
|
|
176
|
+
avoid: ['a hand-built bar'],
|
|
177
|
+
how: '`<Progress value={60} label="Upload" />`; inside an existing `Card`, `CardProgress`.',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'text',
|
|
181
|
+
need: 'Body copy',
|
|
182
|
+
keywords: ['text', 'copy', 'paragraph', 'body', 'typography'],
|
|
183
|
+
use: ['Text'],
|
|
184
|
+
avoid: ['Text as a section heading'],
|
|
185
|
+
how: 'Section headings come from the pattern that owns the section (`PageHeader` `title`, `DataTableSection` `title`), not from extra Text.',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'empty',
|
|
189
|
+
need: 'Empty state for a list or section',
|
|
190
|
+
keywords: ['empty', 'none', 'nothing', 'placeholder', 'zero'],
|
|
191
|
+
use: ['EmptyState'],
|
|
192
|
+
avoid: ['Text saying "No items"'],
|
|
193
|
+
how: '`DataTableSection` shows its own through `emptyTitle`; use `EmptyState` elsewhere.',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'danger-zone',
|
|
197
|
+
need: 'The one irreversible action of a screen (delete the client, close the account)',
|
|
198
|
+
keywords: ['danger', 'irreversible', 'delete', 'account', 'zone', 'destructive'],
|
|
199
|
+
use: ['DestructiveSection'],
|
|
200
|
+
avoid: ['a danger Button that acts without a confirm'],
|
|
201
|
+
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.',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'metadata',
|
|
205
|
+
need: 'Read-only details as label and value pairs',
|
|
206
|
+
keywords: ['metadata', 'details', 'properties', 'key', 'value', 'pairs', 'record'],
|
|
207
|
+
use: ['MetadataList', 'Field'],
|
|
208
|
+
avoid: ['a Table with two columns', 'Text pairs in a Stack'],
|
|
209
|
+
how: '`<MetadataList items={[{ term: "Owner", description: "Ada" }]} />` for a set; `Field` for one pair.',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'people',
|
|
213
|
+
need: 'A person, or a group of people',
|
|
214
|
+
keywords: ['avatar', 'avatars', 'person', 'people', 'user', 'users', 'team', 'members'],
|
|
215
|
+
use: ['Avatar', 'AvatarGroup'],
|
|
216
|
+
avoid: ['an image in a rounded Box'],
|
|
217
|
+
how: '`<Avatar src alt initials />`; several in a row: `<AvatarGroup max={3}>…Avatars…</AvatarGroup>`.',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'date',
|
|
221
|
+
need: 'A date or time',
|
|
222
|
+
keywords: ['date', 'time', 'timestamp', 'when', 'ago', 'due'],
|
|
223
|
+
use: ['Timestamp'],
|
|
224
|
+
avoid: ['a formatted string in Text'],
|
|
225
|
+
how: '`<Timestamp date="2026-10-14" />` renders a `<time dateTime>`; a date in a table cell may stay plain text.',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: 'shortcut',
|
|
229
|
+
need: 'A keyboard key or shortcut hint',
|
|
230
|
+
keywords: ['keyboard', 'shortcut', 'key', 'kbd', 'hotkey'],
|
|
231
|
+
use: ['Kbd'],
|
|
232
|
+
avoid: ['InlineCode', 'Text in a Box'],
|
|
233
|
+
how: '`<Kbd>⌘K</Kbd>`.',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'quote',
|
|
237
|
+
need: 'A quotation with attribution',
|
|
238
|
+
keywords: ['quote', 'quotation', 'testimonial', 'blockquote', 'citation'],
|
|
239
|
+
use: ['Blockquote'],
|
|
240
|
+
avoid: ['Callout for a quote', 'italic Text'],
|
|
241
|
+
how: '`<Blockquote attribution="Ada Lovelace">…</Blockquote>`.',
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'rail',
|
|
245
|
+
need: 'A fixed-width side rail beside fluid content, or a row that stacks when narrow',
|
|
246
|
+
keywords: ['sidebar', 'rail', 'aside', 'responsive', 'switcher', 'stack', 'narrow', 'columns'],
|
|
247
|
+
use: ['Sidebar', 'Switcher'],
|
|
248
|
+
avoid: ['a Grid with media queries'],
|
|
249
|
+
how: '`Sidebar` (`side`, `sideWidth`) for rail + content; `Switcher` (`threshold`) for a row that flips to a column. Neither needs a media query.',
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: 'sticky',
|
|
253
|
+
need: 'Keep an element stuck while its region scrolls',
|
|
254
|
+
keywords: ['sticky', 'pin', 'fixed', 'scroll'],
|
|
255
|
+
use: ['Pin'],
|
|
256
|
+
avoid: ['position: sticky in style'],
|
|
257
|
+
how: '`<Pin top="…">…</Pin>` inside the scrolling region.',
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The recommended set: the ratified core (mcp/core-set.mjs, hds#254) plus every
|
|
263
|
+
* component a need above names, hooks excluded. This is a different set from the
|
|
264
|
+
* ratified core: it adds the patterns and layout parts the guide steers agents to
|
|
265
|
+
* (Page, MetricTiles, ...). list_core and AGENTS.md show this one and say so; the
|
|
266
|
+
* manifest `core` flag, llms.txt and SKILL.md show the ratified core.
|
|
267
|
+
*/
|
|
268
|
+
export function recommendedComponents() {
|
|
269
|
+
const names = new Set(CORE_COMPONENTS);
|
|
270
|
+
for (const intent of INTENTS) for (const name of intent.use) if (!HOOKS[name]) names.add(name);
|
|
271
|
+
return [...names].sort();
|
|
272
|
+
}
|
package/mcp/hds-mcp.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hds-mcp — the `hds` MCP server over stdio, shipped in @hirobius/design-system.
|
|
4
|
+
*
|
|
5
|
+
* Run from an app that has the package installed: npx hds-mcp
|
|
6
|
+
* Without an install: npx -y -p @hirobius/design-system hds-mcp
|
|
7
|
+
*
|
|
8
|
+
* Reads the data the package already ships (public/hds-manifest.json,
|
|
9
|
+
* src/app/data/component-api.json) from its own install directory; it never
|
|
10
|
+
* touches the network or the caller's files. Messages are newline-delimited
|
|
11
|
+
* JSON-RPC 2.0 (the MCP stdio transport); logs go to stderr only.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { createInterface } from 'node:readline';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { loadCatalog } from './catalog.mjs';
|
|
18
|
+
import { createServer } from './server.mjs';
|
|
19
|
+
|
|
20
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const { version } = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
22
|
+
|
|
23
|
+
let catalog;
|
|
24
|
+
try {
|
|
25
|
+
catalog = loadCatalog(PKG_ROOT);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(
|
|
28
|
+
`hds-mcp: could not read the design-system data under ${PKG_ROOT} (${err.message}).\n` +
|
|
29
|
+
' Reinstall @hirobius/design-system; in the hds repo run `node scripts/generate-component-api.mjs` first.',
|
|
30
|
+
);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handle = createServer({ version, instructions: catalog.instructions, tools: catalog.tools });
|
|
35
|
+
|
|
36
|
+
const send = (msg) => process.stdout.write(`${JSON.stringify(msg)}\n`);
|
|
37
|
+
|
|
38
|
+
createInterface({ input: process.stdin }).on('line', (line) => {
|
|
39
|
+
if (!line.trim()) return;
|
|
40
|
+
let message;
|
|
41
|
+
try {
|
|
42
|
+
message = JSON.parse(line);
|
|
43
|
+
} catch {
|
|
44
|
+
send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const response = handle(message);
|
|
48
|
+
if (response) send(response);
|
|
49
|
+
});
|
package/mcp/server.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `hds` MCP server's protocol layer: JSON-RPC 2.0 messages in, responses out.
|
|
3
|
+
*
|
|
4
|
+
* Interface: `createServer({ version, instructions, tools })` returns
|
|
5
|
+
* `handle(message)`, which takes one parsed JSON value and returns the response
|
|
6
|
+
* object, or `null` for a notification. It never throws: a message that is not a
|
|
7
|
+
* request object gets -32600, and anything unexpected inside gets -32603, so one
|
|
8
|
+
* bad line cannot stop the process. Transport (stdio framing) lives in hds-mcp.mjs, the
|
|
9
|
+
* tool behaviour in catalog.mjs; this module only speaks MCP.
|
|
10
|
+
*
|
|
11
|
+
* No dependency on @modelcontextprotocol/sdk: the server needs four methods
|
|
12
|
+
* (initialize, ping, tools/list, tools/call), and a zero-dependency module keeps
|
|
13
|
+
* the published package's install graph unchanged.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const SERVER_NAME = 'hds';
|
|
17
|
+
|
|
18
|
+
/** Protocol versions this server can speak; it echoes the client's when it is one of these. */
|
|
19
|
+
const SUPPORTED_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
20
|
+
|
|
21
|
+
const rpcError = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
22
|
+
const rpcResult = (id, result) => ({ jsonrpc: '2.0', id, result });
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ version: string, instructions: string, tools: { name: string, description: string, inputSchema: object, run: (args: object) => unknown }[] }} options
|
|
26
|
+
*/
|
|
27
|
+
export function createServer({ version, instructions, tools }) {
|
|
28
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
29
|
+
|
|
30
|
+
function dispatch(message) {
|
|
31
|
+
// MCP has no batches (removed in 2025-06-18), so an array is invalid like any non-object.
|
|
32
|
+
if (!message || typeof message !== 'object' || Array.isArray(message)) {
|
|
33
|
+
return rpcError(null, -32600, 'Invalid Request: expected one JSON-RPC request object');
|
|
34
|
+
}
|
|
35
|
+
const hasId = Object.prototype.hasOwnProperty.call(message, 'id');
|
|
36
|
+
const id = hasId ? message.id : null;
|
|
37
|
+
if (typeof message.method !== 'string') {
|
|
38
|
+
return hasId ? rpcError(id, -32600, 'Invalid Request: method must be a string') : null;
|
|
39
|
+
}
|
|
40
|
+
// A message without an `id` member is a notification; `id: null` is still a request.
|
|
41
|
+
if (!hasId) return null;
|
|
42
|
+
const { method } = message;
|
|
43
|
+
const params = message.params && typeof message.params === 'object' ? message.params : {};
|
|
44
|
+
|
|
45
|
+
switch (method) {
|
|
46
|
+
case 'initialize': {
|
|
47
|
+
const asked = params.protocolVersion;
|
|
48
|
+
return rpcResult(id, {
|
|
49
|
+
protocolVersion: SUPPORTED_VERSIONS.includes(asked) ? asked : SUPPORTED_VERSIONS[0],
|
|
50
|
+
capabilities: { tools: { listChanged: false } },
|
|
51
|
+
serverInfo: { name: SERVER_NAME, version },
|
|
52
|
+
instructions,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
case 'ping':
|
|
56
|
+
return rpcResult(id, {});
|
|
57
|
+
case 'tools/list':
|
|
58
|
+
return rpcResult(id, {
|
|
59
|
+
tools: tools.map(({ name, description, inputSchema }) => ({
|
|
60
|
+
name,
|
|
61
|
+
description,
|
|
62
|
+
inputSchema,
|
|
63
|
+
})),
|
|
64
|
+
});
|
|
65
|
+
case 'tools/call': {
|
|
66
|
+
const tool = typeof params.name === 'string' ? byName.get(params.name) : undefined;
|
|
67
|
+
if (!tool) return rpcError(id, -32602, `Unknown tool: ${params.name}`);
|
|
68
|
+
try {
|
|
69
|
+
const args =
|
|
70
|
+
params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
|
|
71
|
+
const data = tool.run(args);
|
|
72
|
+
return rpcResult(id, { content: [{ type: 'text', text: JSON.stringify(data) }] });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// A tool-level failure is a result with isError, so the agent can read and recover.
|
|
75
|
+
return rpcResult(id, {
|
|
76
|
+
content: [{ type: 'text', text: JSON.stringify({ error: String(err.message) }) }],
|
|
77
|
+
isError: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
default:
|
|
82
|
+
return rpcError(id, -32601, `Method not found: ${method}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return function handle(message) {
|
|
87
|
+
try {
|
|
88
|
+
return dispatch(message);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const id = message && typeof message === 'object' && 'id' in message ? message.id : null;
|
|
91
|
+
return rpcError(id ?? null, -32603, `Internal error: ${err?.message ?? err}`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|