@hirobius/design-system 0.18.0 → 0.19.1

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.
Files changed (88) hide show
  1. package/CONSUMING.md +150 -0
  2. package/DESIGN.md +220 -0
  3. package/README.md +14 -2
  4. package/dist/chunks/{LanguageContext-CH_bwzCq.js → LanguageContext-BxdCwiFg.js} +20 -8
  5. package/dist/chunks/{TokenDisplayContext-evZi_214.js → TokenDisplayContext-DKYJMVWk.js} +10 -2
  6. package/dist/chunks/{activity-feed-C8_e-Btq.js → activity-feed-BGQ6v3wc.js} +5248 -3163
  7. package/dist/chunks/{form-DnGRy27o.js → form-COP3vK9_.js} +31 -29
  8. package/dist/contexts.js +4 -4
  9. package/dist/form.js +5 -5
  10. package/dist/hirobius-ui.js +2790 -3257
  11. package/dist/icons.js +53 -0
  12. package/dist/manifest.js +15 -14
  13. package/dist/patterns.js +284 -101
  14. package/dist/static.css +3 -3
  15. package/dist/styles.css +1 -1
  16. package/dist/tokens.css +1 -1
  17. package/dist/types/src/app/components/alert-dialog.d.ts +9 -1
  18. package/dist/types/src/app/components/alert.d.ts +4 -0
  19. package/dist/types/src/app/components/avatar.d.ts +4 -0
  20. package/dist/types/src/app/components/badge.d.ts +4 -0
  21. package/dist/types/src/app/components/box.d.ts +5 -0
  22. package/dist/types/src/app/components/breadcrumb.d.ts +4 -0
  23. package/dist/types/src/app/components/button-group.d.ts +4 -0
  24. package/dist/types/src/app/components/button.d.ts +3 -0
  25. package/dist/types/src/app/components/card.d.ts +9 -0
  26. package/dist/types/src/app/components/checkbox.d.ts +4 -0
  27. package/dist/types/src/app/components/combobox.d.ts +10 -0
  28. package/dist/types/src/app/components/command-palette.d.ts +3 -1
  29. package/dist/types/src/app/components/container.d.ts +5 -0
  30. package/dist/types/src/app/components/context-menu.d.ts +25 -2
  31. package/dist/types/src/app/components/data-table-section.d.ts +45 -0
  32. package/dist/types/src/app/components/destructive-section.d.ts +32 -0
  33. package/dist/types/src/app/components/dialog.d.ts +20 -2
  34. package/dist/types/src/app/components/disclosure.d.ts +3 -0
  35. package/dist/types/src/app/components/divider.d.ts +4 -0
  36. package/dist/types/src/app/components/empty-state.d.ts +4 -0
  37. package/dist/types/src/app/components/field.d.ts +4 -0
  38. package/dist/types/src/app/components/form-actions.d.ts +26 -0
  39. package/dist/types/src/app/components/grid.d.ts +4 -0
  40. package/dist/types/src/app/components/hds-tooltip.d.ts +14 -2
  41. package/dist/types/src/app/components/hover-card.d.ts +16 -1
  42. package/dist/types/src/app/components/icon.d.ts +4 -0
  43. package/dist/types/src/app/components/inline-link.d.ts +4 -0
  44. package/dist/types/src/app/components/input.d.ts +5 -0
  45. package/dist/types/src/app/components/kbd.d.ts +3 -0
  46. package/dist/types/src/app/components/menu.d.ts +26 -2
  47. package/dist/types/src/app/components/metric-tiles.d.ts +40 -0
  48. package/dist/types/src/app/components/page-header.d.ts +34 -0
  49. package/dist/types/src/app/components/pagination.d.ts +3 -0
  50. package/dist/types/src/app/components/popover.d.ts +14 -2
  51. package/dist/types/src/app/components/progress.d.ts +4 -0
  52. package/dist/types/src/app/components/radio.d.ts +5 -0
  53. package/dist/types/src/app/components/select.d.ts +16 -0
  54. package/dist/types/src/app/components/skeleton.d.ts +4 -0
  55. package/dist/types/src/app/components/slider.d.ts +3 -0
  56. package/dist/types/src/app/components/spinner.d.ts +4 -0
  57. package/dist/types/src/app/components/stack.d.ts +4 -0
  58. package/dist/types/src/app/components/stat.d.ts +4 -0
  59. package/dist/types/src/app/components/status-tile.d.ts +4 -0
  60. package/dist/types/src/app/components/table.d.ts +36 -4
  61. package/dist/types/src/app/components/tabs.d.ts +4 -0
  62. package/dist/types/src/app/components/tag.d.ts +3 -0
  63. package/dist/types/src/app/components/text.d.ts +4 -0
  64. package/dist/types/src/app/components/textarea.d.ts +3 -0
  65. package/dist/types/src/app/components/toast.d.ts +4 -0
  66. package/dist/types/src/app/components/toggle.d.ts +4 -0
  67. package/dist/types/src/app/components/tooltip.d.ts +1 -1
  68. package/dist/types/src/app/components/visually-hidden.d.ts +3 -0
  69. package/dist/types/src/app/context/hds-portal.d.ts +52 -0
  70. package/dist/types/src/app/context/hds-theme.d.ts +10 -0
  71. package/dist/types/src/app/data/manifest-types.d.ts +27 -0
  72. package/dist/types/src/app/design-system/generated-token-refs.d.ts +2 -0
  73. package/dist/types/src/icons.d.ts +13 -0
  74. package/dist/types/src/index.d.ts +20 -1
  75. package/dist/types/src/patterns.d.ts +5 -0
  76. package/dist/variables.css +4 -0
  77. package/docs/CONSUMING.md +758 -0
  78. package/hirobius.tokens.json +17 -0
  79. package/llms.txt +261 -0
  80. package/package.json +22 -4
  81. package/public/hds-manifest.json +1605 -105
  82. package/public/llms/components.txt +1090 -0
  83. package/public/llms/layout.txt +54 -0
  84. package/public/llms/scroll.txt +22 -0
  85. package/public/llms/tokens.txt +32 -0
  86. package/public/llms-full.txt +1561 -0
  87. package/public/llms.txt +261 -0
  88. package/src/app/data/component-api.json +6156 -0
@@ -0,0 +1,758 @@
1
+ # Consuming `@hirobius/design-system`
2
+
3
+ How to install and use the Hirobius Design System (HDS) in another project. This
4
+ is the canonical consumption guide — point other teams here.
5
+
6
+ | | |
7
+ | ------------------------------- | -------------------------------------------------------------------------- |
8
+ | **Package** | `@hirobius/design-system` |
9
+ | **Min. version for this guide** | `0.7.0` (router optional, fonts bundled, scoped base styles) |
10
+ | **Module format** | ESM only (`"type": "module"`) |
11
+ | **Registry** | **Public npm** (`https://registry.npmjs.org`) — no auth, no `.npmrc` |
12
+ | **Peers** | `react` ^18.3 ‖ ^19 · `react-dom` (match) · `react-router` ^7 _(optional)_ |
13
+
14
+ > **What changed in 0.7.0** — three things make HDS drop-in for a plain app:
15
+ >
16
+ > 1. **`react-router` is now optional** — components route through an adapter and
17
+ > fall back to plain anchors when no router is provided (§5).
18
+ > 2. **Fonts are bundled** — `tokens.css` embeds the typefaces; no font files to copy (§3).
19
+ > 3. **Base styles are scoped to `[data-hds]`** — add that attribute to your root
20
+ > or section so HDS doesn't fight a host app's resets/fonts (§4, §6).
21
+
22
+ ---
23
+
24
+ ## 1–2. Install
25
+
26
+ HDS publishes to the **public npm registry**. There is no `.npmrc`, no token, and
27
+ no registry configuration — install it like any other package:
28
+
29
+ ```bash
30
+ npm install @hirobius/design-system
31
+ npm install react react-dom # required peers
32
+ ```
33
+
34
+ (`pnpm add` / `yarn add` work identically.)
35
+
36
+ `react-router` is an **optional** peer. You only install it if you want HDS's
37
+ nav/link components to drive client-side navigation through your router — see
38
+ §5. With no router, links render as plain `<a>` and work everywhere.
39
+
40
+ ## 3. Import a stylesheet (once, at the app root)
41
+
42
+ HDS ships **three** stylesheets. Pick one by how much you want HDS to own:
43
+
44
+ ```ts
45
+ // RECOMMENDED for embedding in an existing app (e.g. MUI): tokens + component
46
+ // styles + utilities + embedded fonts, with NO global reset. Styles every HDS
47
+ // component and changes ZERO host-element styles.
48
+ import '@hirobius/design-system/styles.css';
49
+
50
+ // OR batteries-included: everything in styles.css PLUS a global reset
51
+ // (Tailwind preflight). Best for an HDS-first app you fully own.
52
+ import '@hirobius/design-system/tokens.css';
53
+
54
+ // OR vars-only: just the design-token custom properties, no components,
55
+ // no utilities, no reset. For theming another system from HDS tokens.
56
+ import '@hirobius/design-system/variables.css';
57
+ ```
58
+
59
+ | Stylesheet | Tokens | Components + utilities | Embedded fonts | Global reset |
60
+ | ------------------- | :----: | :--------------------: | :------------: | :-------------------: |
61
+ | **`styles.css`** | ✅ | ✅ | ✅ | ❌ none |
62
+ | **`tokens.css`** | ✅ | ✅ | ✅ | ⚠️ Tailwind preflight |
63
+ | **`variables.css`** | ✅ | ❌ | ❌ | ❌ none |
64
+
65
+ All three:
66
+
67
+ - **Need no Tailwind config** in the consumer — utilities ship compiled.
68
+ - **Need no font files** — the woff2 are inlined (the file is correspondingly
69
+ larger; woff2 is already compressed so gzip recovers most of it).
70
+
71
+ HDS's own base styles (type baseline, resets) are scoped to the `[data-hds]`
72
+ subtree (§4) in **all** bundles, so they never touch host elements. The only
73
+ difference is `tokens.css`'s **global** Tailwind preflight, which `styles.css`
74
+ omits — so `styles.css` is the safe default when HDS shares a page with another
75
+ framework's CSS (§6). Importing any component also pulls a stylesheet in as a
76
+ side-effect, but importing one explicitly at the entry point is the canonical,
77
+ order-stable setup.
78
+
79
+ ## 4. Mark your HDS scope with `data-hds`
80
+
81
+ As of 0.7.0 the base styles (element resets, the body/heading type baseline, the
82
+ theme-change transition) are scoped to a **`[data-hds]`** subtree so they don't
83
+ collide with a host app's own resets or fonts. Add the attribute to the element
84
+ that should host HDS:
85
+
86
+ ```html
87
+ <!-- whole app uses HDS -->
88
+ <html data-hds>
89
+ …
90
+ </html>
91
+ ```
92
+
93
+ ```tsx
94
+ // or just a section of an otherwise non-HDS app
95
+ <div data-hds>
96
+ <Button>Inside HDS scope</Button>
97
+ </div>
98
+ ```
99
+
100
+ Without `data-hds`, components still get their own token-driven styling, but the
101
+ global type baseline and resets won't apply (text falls back to the host font).
102
+ Put `data-hds` as high as makes sense — on `<html>`/`<body>` for an
103
+ HDS-first app, or on a wrapper for a section. Overlays (Dialog, AlertDialog,
104
+ Menu, ContextMenu, Popover, Select, HoverCard, Tooltip) inherit the nearest
105
+ `data-hds` scope, so a `<div data-hds data-theme="dark">` themes them too; pass
106
+ `container` on the Content part to portal elsewhere. If the scope element
107
+ creates a containing block or stacking context (`transform`, `filter`, `z-index`,
108
+ `isolation`), pass `container={null}` (portal to `document.body`) or a dedicated
109
+ element so `position: fixed` overlays still cover the viewport. Scoping at
110
+ `<html>` still works. See [ADR-016](adr/016-scoped-base-styles.md).
111
+
112
+ ## 5. Routing — optional, via the adapter seam
113
+
114
+ HDS components never import a router. Navigation comes from an injectable
115
+ adapter:
116
+
117
+ - **No router (default).** Do nothing. Links are real `<a href>`; in-app
118
+ navigation falls back to `window.location`. Perfect for a plain Vite/React
119
+ app or any page that doesn't need SPA nav.
120
+ - **react-router / Next.js / any router.** Wrap your app once and bridge your
121
+ router into the adapter:
122
+
123
+ ```tsx
124
+ import { HdsRouterProvider, type HdsRouterAdapter } from '@hirobius/design-system';
125
+ import { useNavigate, useLocation, Link } from 'react-router';
126
+
127
+ function HdsRouting({ children }: { children: React.ReactNode }) {
128
+ const navigate = useNavigate();
129
+ const { pathname } = useLocation();
130
+ const adapter: HdsRouterAdapter = {
131
+ navigate: (href, opts) => navigate(href, opts),
132
+ currentPath: pathname,
133
+ LinkComponent: ({ to, children, ...rest }) => (
134
+ <Link to={to} {...rest}>
135
+ {children}
136
+ </Link>
137
+ ),
138
+ };
139
+ return <HdsRouterProvider adapter={adapter}>{children}</HdsRouterProvider>;
140
+ }
141
+ ```
142
+
143
+ `useHdsRouter()` is also exported if you build your own components on the seam.
144
+
145
+ ## 6. Coexisting with MUI / Emotion / another global CSS
146
+
147
+ If your app runs MUI `<CssBaseline>` + Emotion (or any opinionated global CSS),
148
+ **don't** apply HDS globally:
149
+
150
+ - Scope HDS to a section with `data-hds` (§4) rather than `<html>`, so HDS's
151
+ base styles only affect that subtree.
152
+ - Don't expect HDS and `<CssBaseline>` to share `<body>` cleanly — pick one type
153
+ system per surface. HDS's namespaced custom properties (`--semantic-*`,
154
+ `--primitive-*`, `--hds-*`) won't collide with MUI's, but the two opinionated
155
+ resets will fight if both target `body`.
156
+ - **Styled path (recommended for MUI/host embedding):** import
157
+ `@hirobius/design-system/styles.css` instead of `tokens.css`. It ships the full
158
+ component styling (tokens + utilities + fonts) but **no global reset** — HDS's
159
+ own base is scoped to `[data-hds]`, so it styles every HDS component and
160
+ **cannot** restyle your host's `*`, `body`, headings, `button`, `a`, or form
161
+ controls. This is the clean way to run HDS next to `<CssBaseline>`: no reset
162
+ fight, full component fidelity.
163
+ - **Vars-only path:** import `@hirobius/design-system/variables.css` for just the
164
+ design-token custom properties — no components, no utilities, no reset. Use
165
+ when you only want to theme another system (MUI palette, etc.) from HDS tokens
166
+ and aren't rendering HDS components on that surface.
167
+ - **MUI theme preset:** `@hirobius/design-system/mui` maps HDS tokens to an MUI
168
+ palette so MUI and HDS share one source of truth (status colors line up with
169
+ `<Button tone>`). It imports no MUI code — the return type is structural:
170
+
171
+ ```tsx
172
+ import { createTheme, ThemeProvider } from '@mui/material';
173
+ import { hdsMuiThemeOptions } from '@hirobius/design-system/mui';
174
+
175
+ const theme = createTheme({ cssVariables: true, ...hdsMuiThemeOptions() });
176
+ // render inside a `data-hds` scope (+ variables.css/styles.css) so the vars resolve
177
+ ```
178
+
179
+ Palette values are HDS token `var(--…)` references, so light/dark follow
180
+ `[data-theme]` automatically. Requires MUI v6+ (`cssVariables: true`).
181
+
182
+ - **Leaf imports stay light:** importing a primitive (e.g. `Button`) does **not**
183
+ pull `react-router`, `react-hook-form`, `zod`, or `@hookform/resolvers` into
184
+ your bundle — those are optional peers used only by the router seam / the
185
+ `/form` subpath, and aren't in the main bundle graph. A consumer with only
186
+ `react`/`react-dom` installed builds cleanly (verified in `smoke:consumer`).
187
+ - **Only `tokens.css` carries a global reset.** If you specifically want HDS's
188
+ batteries-included reset (Tailwind preflight, resets `margin`/`padding`/`border`
189
+ globally), import `tokens.css`. Otherwise prefer `styles.css` — same components,
190
+ no host reset.
191
+
192
+ ## 7. Use it
193
+
194
+ ```tsx
195
+ import { Button, Card, Badge } from '@hirobius/design-system';
196
+ import { cn } from '@hirobius/design-system/cn';
197
+ import { tokens } from '@hirobius/design-system/tokens';
198
+ import manifest from '@hirobius/design-system/manifest';
199
+
200
+ export function Example() {
201
+ return (
202
+ <div data-hds>
203
+ <Card>
204
+ <Badge tone="accent">New</Badge>
205
+ <Button className={cn('mt-4')}>Get started</Button>
206
+ </Card>
207
+ </div>
208
+ );
209
+ }
210
+ ```
211
+
212
+ ### Subpath exports
213
+
214
+ | Import | What you get |
215
+ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
216
+ | `@hirobius/design-system` | All public components + the router seam (`HdsRouterProvider`, `useHdsRouter`) |
217
+ | `@hirobius/design-system/styles.css` | Components + utilities + tokens + fonts, NO global reset (host-safe; recommended for embedding) |
218
+ | `@hirobius/design-system/tokens.css` | The complete stylesheet — styles.css PLUS a global Tailwind-preflight reset |
219
+ | `@hirobius/design-system/variables.css` | Design tokens as CSS custom properties ONLY — no reset/preflight (host-safe) |
220
+ | `@hirobius/design-system/tokens` | Design-token values as typed TS |
221
+ | `@hirobius/design-system/cn` | The `cn()` className-merge helper |
222
+ | `@hirobius/design-system/manifest` | Machine-readable component inventory (`hds-manifest.json`) |
223
+ | `@hirobius/design-system/contexts` | React context providers, incl. the router seam (see below) |
224
+ | `@hirobius/design-system/form` | Optional React Hook Form + Zod form adapter (see §8.5) |
225
+ | `@hirobius/design-system/mui` | Optional Material UI theme preset — maps HDS tokens to an MUI palette (see §6) |
226
+ | `@hirobius/design-system/brand` | Framework-free palette → HDS-semantic overlay bridge for static/SSR targets (see §12) |
227
+ | `@hirobius/design-system/scroll` | Opt-in scroll-motion: `SmoothScroll` (Lenis) + `useScrollProgress` (Motion). Optional peer `lenis` (see §13) |
228
+ | `@hirobius/design-system/static.css` | CSS-only static-primitive layer — `.hds-badge`/`.hds-card`/`.hds-alert`/`.hds-divider`/`.hds-tag` classes, no React (see §14) |
229
+
230
+ ### Semantic feedback / status tokens
231
+
232
+ For status UI (Saved / Applied / Interviewing / Offer / Rejected, or any
233
+ success/warning/error/in-progress state), theme from HDS's feedback tokens
234
+ instead of hardcoding hex. Each is a CSS custom property that auto-switches
235
+ light/dark and is AA-verified for small text on both the page and card
236
+ (`raised`) surfaces (asserted by `scripts/check-contrast.mjs`):
237
+
238
+ | Intent | Text color var | Tinted-surface var |
239
+ | ----------- | -------------------------------------- | ----------------------------------------- |
240
+ | success | `--semantic-color-feedback-success` | `--semantic-color-feedback-bg-success` |
241
+ | warning | `--semantic-color-feedback-warning` | `--semantic-color-feedback-bg-warning` |
242
+ | info | `--semantic-color-feedback-info` | `--semantic-color-feedback-bg-info` |
243
+ | error | `--semantic-color-feedback-error` | `--semantic-color-feedback-bg-error` |
244
+ | in-progress | `--semantic-color-feedback-inProgress` | `--semantic-color-feedback-bg-inProgress` |
245
+
246
+ ```css
247
+ .status-offer {
248
+ color: var(--semantic-color-feedback-success);
249
+ }
250
+ .status-pending {
251
+ color: var(--semantic-color-feedback-inProgress);
252
+ }
253
+ ```
254
+
255
+ The same values are available as typed TS via `@hirobius/design-system/tokens`
256
+ (`hds.color.feedback.*`), and the `Button` and `Badge` components expose them
257
+ directly through a `tone` prop so you rarely need the raw vars:
258
+
259
+ ```tsx
260
+ <Button tone="danger">Delete</Button> {/* AA in light + dark */}
261
+ <Badge tone="inProgress">Interviewing</Badge>
262
+ ```
263
+
264
+ ## 8. Optional providers — theming / i18n / multi-tenant / fonts
265
+
266
+ Only needed if you use those features. Mount them above your app:
267
+
268
+ ```tsx
269
+ import {
270
+ TenantProvider,
271
+ LanguageProvider,
272
+ ThemeProvider,
273
+ FontProvider,
274
+ } from '@hirobius/design-system/contexts';
275
+
276
+ <TenantProvider>
277
+ <LanguageProvider>
278
+ <ThemeProvider>
279
+ <FontProvider>{/* app */}</FontProvider>
280
+ </ThemeProvider>
281
+ </LanguageProvider>
282
+ </TenantProvider>;
283
+ ```
284
+
285
+ `ThemeProvider` toggles light/dark by setting `data-theme` + `.dark` on
286
+ `<html>`. It does not set `data-hds` — add that yourself (§4).
287
+
288
+ ## 8.5 Optional — typed, schema-validated forms (React Hook Form + Zod)
289
+
290
+ The main barrel is validation-agnostic: `FormField` takes a plain `error`
291
+ string, so you bring your own validation. If you want a batteries-included
292
+ layer, the `/form` subpath wires [React Hook Form](https://react-hook-form.com)
293
+ to [Zod](https://zod.dev). It's opt-in — `react-hook-form`, `zod`, and
294
+ `@hookform/resolvers` are **optional peer deps**, so they only land in your
295
+ bundle if you import this entry.
296
+
297
+ ```bash
298
+ pnpm add react-hook-form zod @hookform/resolvers
299
+ ```
300
+
301
+ ```tsx
302
+ import { z } from 'zod';
303
+ import { useHdsForm, HdsForm, HdsFormField } from '@hirobius/design-system/form';
304
+ import { Button } from '@hirobius/design-system';
305
+
306
+ const schema = z.object({
307
+ email: z.string().email('Enter a valid email'),
308
+ role: z.string().min(1, 'Pick a role'),
309
+ });
310
+
311
+ function ApplyForm() {
312
+ const form = useHdsForm(schema, { defaultValues: { email: '', role: '' } });
313
+ return (
314
+ <HdsForm form={form} onSubmit={(values) => save(values)}>
315
+ <HdsFormField name="email" label="Email" description="We never share it.">
316
+ {(props) => <input type="email" {...props} />}
317
+ </HdsFormField>
318
+ <HdsFormField name="role" label="Role">
319
+ {(props) => (
320
+ <select {...props}>
321
+ <option value="">Choose…</option>
322
+ <option value="ic">Individual contributor</option>
323
+ <option value="lead">Lead</option>
324
+ </select>
325
+ )}
326
+ </HdsFormField>
327
+ <Button type="submit">Apply</Button>
328
+ </HdsForm>
329
+ );
330
+ }
331
+ ```
332
+
333
+ - `useHdsForm(schema, options?)` — RHF's `useForm` with the Zod resolver and
334
+ `onTouched` validation pre-wired; the schema's parsed type drives the field
335
+ types so `onSubmit` gets a fully-typed, validated value object.
336
+ - `HdsForm` — wraps the form in RHF's `FormProvider`, routes submit through
337
+ `handleSubmit`, and sets `noValidate` so Zod is the single source of
338
+ validation (no native browser popups racing your schema).
339
+ - `HdsFormField` — a **render-prop**: spread the supplied props onto your
340
+ control. It binds the control to RHF by `name` and shows the field's Zod
341
+ error through the same label/error/aria markup as the core `FormField`. (It's
342
+ a render-prop, not a cloned child, so RHF's callback ref attaches cleanly.)
343
+
344
+ **SSR / Astro / Next.js RSC:** this layer is client-only (RHF uses hooks +
345
+ refs). In Next's app router it carries a `'use client'` marker; in Astro, mount
346
+ the form inside a hydrated island (e.g. `client:load`).
347
+
348
+ ## 9. TypeScript & bundler requirements
349
+
350
+ - **ESM-only.** Use a modern bundler (Vite, Next, Rspack, etc.). `require()` /
351
+ CommonJS resolution will not work.
352
+ - **Types ship from source.** The package serves types directly from its `src/`
353
+ TypeScript (no emitted `.d.ts`), so the consuming `tsconfig.json` should use
354
+ `"moduleResolution": "bundler"` (or `"node16"`/`"nodenext"`).
355
+
356
+ ## 10. Extending HDS — the consumption contract
357
+
358
+ HDS is **authoritative**. Consumers conform to its structure, extend in their
359
+ own namespace, and route gaps upstream — never the reverse. Follow these four
360
+ rules and token collisions between the package and your app become **structurally
361
+ impossible** — no shared registry, no coordination, no luck required.
362
+
363
+ The line is namespace ownership:
364
+
365
+ | DS-owned (never declare here) | Consumer-owned (yours to define) |
366
+ | --------------------------------------------------------------------------- | --------------------------------------------- |
367
+ | `--primitive-*` · `--semantic-*` · `--component-*` · `--role-*` · `--hds-*` | `--<prefix>-*` (pick one prefix for your app) |
368
+ | `data-hds` · `data-theme` · `data-tenant` | your own `data-*` attributes |
369
+
370
+ Any release may add names inside DS-owned space; consumers may never declare
371
+ there. That single invariant is what makes overlap impossible by construction.
372
+
373
+ ### C1 — The DS owns its namespaces
374
+
375
+ `--primitive-*`, `--semantic-*`, `--component-*`, `--role-*`, `--hds-*` and the
376
+ `data-hds` / `data-theme` / `data-tenant` attributes belong to the package.
377
+ **Never declare a token inside them**, and treat every release as free to add
378
+ names there. Declaring `--primitive-color-lilac-*` in your app, for example,
379
+ would silently override package tokens the day the DS ships its own — with no
380
+ error anywhere.
381
+
382
+ ### C2 — Extend under your own prefix
383
+
384
+ App-local tokens, aliases, and component styles live in a **consumer namespace**
385
+ and may reference DS tokens freely:
386
+
387
+ ```css
388
+ :root {
389
+ --lb-lilac-500: #6f3fd4; /* app-owned value */
390
+ --lb-cta-radius: var(--semantic-radius-action); /* references a DS token */
391
+ }
392
+ ```
393
+
394
+ Pick one prefix for your app (`--lb-*` here) and keep every
395
+ local declaration inside it.
396
+
397
+ ### C3 — Brand through the tenant mechanism
398
+
399
+ Re-theming goes through the DS's own structure, not around it: author a **tenant
400
+ overlay** conforming to
401
+ [`hirobius.tenant-tokens.schema.json`](../hirobius.tenant-tokens.schema.json)
402
+ (**semantic tier only** — the primitive tier is off-limits, per rule R1), and
403
+ activate it with `data-tenant`. A local mirror of a not-yet-merged overlay is
404
+ fine as a stopgap, but mark it temporary and delete it once the overlay ships in
405
+ the package.
406
+
407
+ ### C4 — Route gaps upstream, don't patch around them
408
+
409
+ A missing primitive, a token you need, or a fundamental flaw becomes an **issue
410
+ and PR against the DS** — not a permanent downstream fork. Every alias target
411
+ must exist in the base `hirobius.tokens.json` (rule R5), so a new brand ramp is
412
+ added upstream first, then the tenant overlay aliases it.
413
+
414
+ > **Optional (no DS obligation):** keep a downstream **drift-guard test** that
415
+ > pins the token paths your app consumes, so your CI fails when an upgrade moves
416
+ > something. The consumer adapts; it never gates a DS release.
417
+
418
+ **Reference implementation:** the first external consumer follows all four —
419
+ it consumes tokens via
420
+ `@hirobius/design-system/variables.css`, keeps every extension under `--lb-*`,
421
+ mirrors its brand through a tenant overlay, and pins consumed paths with a
422
+ CI-enforced drift guard.
423
+
424
+ ## 11. Lint discipline — the consumer ESLint plugin
425
+
426
+ The design system's own token/layout discipline is enforced inside this repo
427
+ by `scripts/check-hardcoded-colors.mjs`, `scripts/check-hardcoded-spacing.mjs`,
428
+ and `scripts/check-layout-discipline.mjs` — internal scripts that only run
429
+ here. `@hirobius/eslint-plugin-hds` (`scripts/eslint-plugin-hds/`) ships the
430
+ same discipline as an installable ESLint plugin for consumer apps, flagging
431
+ raw hex/px values in `style`, `className`, and `Box` `sx` props before they
432
+ reach code review.
433
+
434
+ ```bash
435
+ pnpm add -D "@hirobius/eslint-plugin-hds@github:hirobius/hirobius-design-system#path:/scripts/eslint-plugin-hds"
436
+ ```
437
+
438
+ ```js
439
+ // eslint.config.mjs
440
+ import hds from '@hirobius/eslint-plugin-hds';
441
+
442
+ export default [
443
+ {
444
+ files: ['**/*.{ts,tsx,jsx}'],
445
+ languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
446
+ },
447
+ ...hds.configs.recommended,
448
+ ];
449
+ ```
450
+
451
+ Four rules ship in `recommended`: `no-raw-hex` (error), `no-raw-px-spacing`
452
+ (error), `sx-token-first` (error — raw hex/px inside `Box` `sx`, where token
453
+ keys are mandatory), and `prefer-hds-layout-primitive` (warn — ad-hoc
454
+ `display: flex`/`grid` where Stack/Grid likely fits). Full rule docs, examples,
455
+ and the "why `scripts/eslint-plugin-hds/` and not a top-level workspace
456
+ package" packaging note live in
457
+ [`scripts/eslint-plugin-hds/README.md`](../scripts/eslint-plugin-hds/README.md).
458
+
459
+ ### Agent skill
460
+
461
+ An agent building a screen with HDS can load one consumer skill instead of reading
462
+ the manifest, `llms.txt` and this guide separately. It carries the allow-list of
463
+ importable components, the screen layout recipe, the do/don't rules, and the lint
464
+ step (§11) as its final required step. It is generated from the manifest by
465
+ `pnpm skill:generate` and drift-checked by `pnpm check:consumer-skill`.
466
+
467
+ ```bash
468
+ npx skills add hirobius/hds --skill hds-consumer
469
+ ```
470
+
471
+ ## Troubleshooting
472
+
473
+ | Symptom | Cause / fix |
474
+ | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
475
+ | `npm ERR! 404` on install | Check the package name spelling; if pinning a version, confirm it's published on npm (`npm view @hirobius/design-system version`). Public npm needs no auth (see §1–2). |
476
+ | `ERR_REQUIRE_ESM` / `require() of ES Module` | Consumer is CommonJS — switch to an ESM bundler (§9). |
477
+ | Components render unstyled / wrong font | Import `@hirobius/design-system/styles.css` (or `tokens.css`) at the app root (§3) **and** add `data-hds` to your root or section (§4). |
478
+ | Text uses the host font, not Satoshi | Missing `data-hds` on an ancestor (§4). |
479
+ | In-app links do a full page reload | Expected with no router. Inject your router via `<HdsRouterProvider>` for SPA nav (§5). |
480
+ | Host app's spacing/layout shifted after adding HDS | Tailwind preflight ships global this release (§6); scope HDS to a section and isolate where possible. |
481
+ | Brand overlay has no effect on an Astro page | Inject the overlay on an ancestor of the themed markup (usually `<html>`), and ensure `variables.css` is imported once (§12). |
482
+
483
+ ## 12. Static Astro sites + the brand overlay bridge
484
+
485
+ HDS is built for a React runtime, but its **tokens are just CSS custom
486
+ properties**, so a static Astro site can render HDS-consistent UI with **zero
487
+ React** — import the vars, set a few attributes, and let per-page markup read the
488
+ tokens through Tailwind utilities or plain CSS. Interactive pieces hydrate as
489
+ `client:*` islands only where you actually need them.
490
+
491
+ This is the path the `hirobius/clients` factory uses: one HDS token spine, many
492
+ brand skins, no parallel token system.
493
+
494
+ ### 12.1 Wire the token spine once (in your base layout)
495
+
496
+ ```astro
497
+ ---
498
+ // src/layouts/Base.astro
499
+ import '@hirobius/design-system/variables.css'; // tokens only — no reset, host-safe
500
+ import { brandOverlayStyle } from '@hirobius/design-system/brand';
501
+ import { client } from '../client.config';
502
+
503
+ // Map the site's brand palette onto HDS semantics (see §12.2).
504
+ const style = brandOverlayStyle(client.palette);
505
+ ---
506
+ <html lang="en" data-hds data-theme="light" style={style}>
507
+ <head><slot name="head" /></head>
508
+ <body><slot /></body>
509
+ </html>
510
+ ```
511
+
512
+ - `data-hds` opts the subtree into HDS's base type/reset (§4).
513
+ - `data-theme="light|dark"` picks the mode; `data-brand="slug"` is available too
514
+ if you prefer a checked-in overlay block over the inline style (§12.3).
515
+ - The inline `style` wins the cascade, so the brand always overrides defaults.
516
+
517
+ ### 12.2 The brand bridge — palette → HDS semantics
518
+
519
+ `@hirobius/design-system/brand` is **framework-free** (no React, no Node), so it
520
+ runs in an Astro build or an edge function. Give it a small palette; it returns
521
+ the HDS semantic custom properties, deriving interactive accent states with CSS
522
+ `color-mix()`:
523
+
524
+ ```ts
525
+ import {
526
+ brandOverlayStyle,
527
+ brandOverlayCss,
528
+ brandOverlayVars,
529
+ } from '@hirobius/design-system/brand';
530
+
531
+ const palette = {
532
+ primary: '#2f6f3e', // → --semantic-accent-rest + surface/border-accent
533
+ onPrimary: '#ffffff', // → --semantic-color-content-onAccent
534
+ bg: '#f7f8f3', // → --semantic-color-surface-page
535
+ fg: '#1f2a1c', // → --semantic-color-content-primary
536
+ muted: '#e7ecdd', // → --semantic-color-surface-raised
537
+ accent: '#a7c957', // optional — kept brand-level (--brand-accent)
538
+ radius: '8px', // optional — --semantic-radius-action
539
+ fontHeading: '"Work Sans", sans-serif', // optional → display family
540
+ fontBody: 'Inter, sans-serif', // optional → primary family
541
+ };
542
+
543
+ brandOverlayStyle(palette); // "…k:v;k:v" for <html style>
544
+ brandOverlayCss('[data-brand="acme"]', palette); // a scoped CSS rule block
545
+ brandOverlayVars(palette); // the raw { --var: value } record
546
+ ```
547
+
548
+ Only the accent ramp, page/content colours, and (when given) radius + fonts are
549
+ overridden; everything else inherits HDS defaults, so a partial palette still
550
+ yields a coherent theme. `color-mix()` is Baseline-supported (evergreen engines
551
+ since 2023).
552
+
553
+ ### 12.3 Multi-brand: `data-brand` blocks
554
+
555
+ For several brands on one deploy (per-niche reskins), emit a `data-brand` block
556
+ per brand with `brandOverlayCss` into a global stylesheet and switch by
557
+ attribute — no inline style, no rebuild:
558
+
559
+ ```ts
560
+ // build a static stylesheet from your brand registry
561
+ const css = brands.map((b) => brandOverlayCss(`[data-brand="${b.slug}"]`, b.palette)).join('\n');
562
+ ```
563
+
564
+ ```astro
565
+ <html data-hds data-brand={Astro.url.searchParams.get('brand') ?? 'default'}>
566
+ ```
567
+
568
+ ### 12.4 Hydrate only what's interactive
569
+
570
+ Static primitives (badge, card, alert, divider, tag, the layout primitives, and
571
+ `Box`/`sx`) render from tokens with no JS. Reach for a React island **only** for
572
+ genuinely interactive components:
573
+
574
+ ```astro
575
+ ---
576
+ import { ContactForm } from '@hirobius/design-system';
577
+ ---
578
+ <ContactForm client:visible />
579
+ ```
580
+
581
+ Import `@hirobius/design-system/styles.css` once (alongside `variables.css`, or
582
+ instead of it) when you hydrate components that need HDS's compiled component
583
+ CSS. For a purely static page, `variables.css` + your own Tailwind utilities is
584
+ enough.
585
+
586
+ ## 13. Scroll motion — the `/scroll` subpath
587
+
588
+ Opt-in scroll-motion primitives. `lenis` is an **optional peer dependency** — install it only if you use `SmoothScroll`:
589
+
590
+ ```bash
591
+ pnpm add lenis # required only for SmoothScroll; useScrollProgress needs nothing extra
592
+ ```
593
+
594
+ **Layering (cheapest first):**
595
+
596
+ 1. **CSS scroll-driven animation** (`animation-timeline: view()/scroll()` + `position: sticky`) — reveal / pin / parallax / scale with **zero JS**. Prefer this.
597
+ 2. **`useScrollProgress(ref)`** — a Motion-based `0→1` MotionValue for effects you need in React (canvas/WebGL, cross-element choreography). No new dependency; works with or without `SmoothScroll`.
598
+ 3. **`SmoothScroll`** — Lenis momentum ("inertia") scrolling, the one thing CSS/Motion can't do. Wrap the app/site root once. It **skips Lenis entirely under `prefers-reduced-motion`** (native scroll) and is SSR-safe.
599
+
600
+ ```tsx
601
+ import { SmoothScroll, useScrollProgress } from '@hirobius/design-system/scroll';
602
+ import { motion, useTransform } from 'motion/react';
603
+ import { useRef } from 'react';
604
+
605
+ function Root() {
606
+ return (
607
+ <SmoothScroll>
608
+ {' '}
609
+ {/* momentum scroll; reduced-motion aware */}
610
+ <Hero />
611
+ </SmoothScroll>
612
+ );
613
+ }
614
+
615
+ function Hero() {
616
+ const ref = useRef<HTMLDivElement>(null);
617
+ const progress = useScrollProgress(ref); // 0 → 1 through the viewport
618
+ const scale = useTransform(progress, [0, 1], [0.4, 1]);
619
+ return <motion.div ref={ref} style={{ scale }} />;
620
+ }
621
+ ```
622
+
623
+ `SmoothScroll` drives the real scroll position, so CSS `animation-timeline` and `useScrollProgress` keep working underneath it unchanged. See `docs/adr/021-animation-engine-motion-core-gsap-downstream.md` for why Lenis is opt-in and GSAP is out.
624
+
625
+ ## 14. CSS-only static primitives — Badge/Card/Alert/Divider/Tag with NO React
626
+
627
+ §12.4 promised that "static primitives (badge, card, alert, divider, tag, …)
628
+ render from tokens with no JS" — `@hirobius/design-system/static.css` is that
629
+ promise, made concrete. It ships plain `.hds-badge` / `.hds-card` /
630
+ `.hds-alert` / `.hds-divider` / `.hds-tag` classes bound to the **same**
631
+ `--semantic-*` / `--component-*` / `--role-*` custom properties the React
632
+ `Badge`/`Card`/`Alert`/`Divider`/`Tag` components use — so a `.astro`/`.html`
633
+ page with zero React still renders on-token, theme-aware UI. This is the path
634
+ the `hirobius/clients` factory uses for non-interactive primitives on its
635
+ static Astro pages.
636
+
637
+ Import it once alongside `variables.css` (both are host-safe — no reset, no
638
+ bare element selectors, every rule scoped to `.hds-*`):
639
+
640
+ ```astro
641
+ ---
642
+ // src/layouts/Base.astro
643
+ import '@hirobius/design-system/variables.css'; // tokens only
644
+ import '@hirobius/design-system/static.css'; // .hds-* primitive classes
645
+ ---
646
+ <html lang="en" data-hds data-theme="light">
647
+ <body><slot /></body>
648
+ </html>
649
+ ```
650
+
651
+ `data-theme="dark"` (or `data-brand="slug"` via the brand overlay bridge, §12)
652
+ retheme every class below automatically — no JS, no rebuild.
653
+
654
+ ### Badge
655
+
656
+ ```html
657
+ <span class="hds-badge">Neutral</span>
658
+ <span class="hds-badge hds-badge--info">Info</span>
659
+ <span class="hds-badge hds-badge--success">Success</span>
660
+ <span class="hds-badge hds-badge--danger">Danger</span>
661
+ <span class="hds-badge hds-badge--warning">Warning</span>
662
+ <span class="hds-badge hds-badge--in-progress">In progress</span>
663
+ ```
664
+
665
+ ### Card
666
+
667
+ Root surface only — `variant` (`--accent`) and `tone` (`--danger`/`--success`/
668
+ `--warning`/`--info`), matching the React `Card`'s structural axis. Compose
669
+ header/body/footer with your own markup inside it; reach for the real
670
+ `<Card>` compound parts (`Card.Header`, `Card.Progress`, `Card.Metric`, …) if
671
+ you need the full slot anatomy — that still requires React.
672
+
673
+ ```html
674
+ <div class="hds-card">Borderless default.</div>
675
+ <div class="hds-card hds-card--bordered">Opt-in neutral 1px border.</div>
676
+ <div class="hds-card hds-card--accent">Highlighted / "recommended" entry.</div>
677
+ <div class="hds-card hds-card--danger">Status-driven — the border is the signal.</div>
678
+ ```
679
+
680
+ ### Alert
681
+
682
+ `role="alert"` is an ARIA live-region role, not a styling concern — keep it on
683
+ the markup. Add `.hds-alert--with-title` alongside a tone modifier when using
684
+ the title + body pattern (it switches the cross-axis alignment from centered
685
+ to top-aligned, matching the React component's `hasTitle` behavior).
686
+
687
+ ```html
688
+ <div class="hds-alert hds-alert--info" role="alert">
689
+ <span class="hds-alert__body">Heads up — this is an info alert.</span>
690
+ </div>
691
+
692
+ <div class="hds-alert hds-alert--danger hds-alert--with-title" role="alert">
693
+ <span>
694
+ <span class="hds-alert__title">Something went wrong</span>
695
+ <span class="hds-alert__body">The build failed on step 3.</span>
696
+ </span>
697
+ </div>
698
+ ```
699
+
700
+ ### Divider
701
+
702
+ ```html
703
+ <hr class="hds-divider hds-divider--horizontal" />
704
+ <hr class="hds-divider hds-divider--horizontal hds-divider--strong" />
705
+
706
+ <div style="display: flex; align-items: center; gap: 12px;">
707
+ <span>Left</span>
708
+ <hr class="hds-divider hds-divider--vertical" aria-orientation="vertical" />
709
+ <span>Right</span>
710
+ </div>
711
+ ```
712
+
713
+ ### Tag
714
+
715
+ Two-part anatomy — a 44px accessible hit-target button (`.hds-tag`) wrapping
716
+ the compact visible pill (`.hds-tag__pill`), same split as the React
717
+ component. `.hds-tag--active` is the selected/pressed state (`aria-pressed`
718
+ still belongs on the markup):
719
+
720
+ ```html
721
+ <button type="button" class="hds-tag" aria-pressed="false">
722
+ <span class="hds-tag__pill">Filter</span>
723
+ </button>
724
+
725
+ <button type="button" class="hds-tag hds-tag--active" aria-pressed="true">
726
+ <span class="hds-tag__pill">Selected</span>
727
+ </button>
728
+ ```
729
+
730
+ ### What's out of scope
731
+
732
+ `static.css` covers non-interactive/CSS-only-interactive primitives. Anything
733
+ with real state (forms, dialogs, dropdowns, `ContactForm`) still needs a React
734
+ island (§12.4) — this layer doesn't attempt to reimplement component logic in
735
+ CSS, only the visual surface of the five primitives above.
736
+
737
+ ## 15. Icons — the `/icons` subpath
738
+
739
+ `IconButton` and `Icon` take an `icon` prop. Get one from the curated subpath;
740
+ `lucide-react` does not need to be installed separately.
741
+
742
+ ```tsx
743
+ import { IconButton } from '@hirobius/design-system';
744
+ import { Ellipsis } from '@hirobius/design-system/icons';
745
+
746
+ <IconButton icon={Ellipsis} label="Row actions" />;
747
+ ```
748
+
749
+ The full name list is `iconSet.names` in the manifest (and the Icons section of
750
+ `llms.txt`). Canonical Lucide 0.487.0 names only: `MoreHorizontal` is
751
+ `Ellipsis`, `MoreVertical` is `EllipsisVertical`, `AlertTriangle` is
752
+ `TriangleAlert`, `Home` is `House`, `Filter` is `Funnel`. Icon-only actions (row
753
+ menus, close, edit) use `IconButton`; do not hand-roll a button with a glyph. For
754
+ an icon outside the set, install `lucide-react@0.487.0` so the `LucideIcon` type
755
+ stays identical.
756
+
757
+ `Calendar` and `Menu` share names with HDS components; alias them:
758
+ `import { Calendar as CalendarIcon, Menu as MenuIcon } from '@hirobius/design-system/icons'`.