@tokyo3rdhq/magi-design-system 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -5,7 +5,8 @@ Shared visual foundation for the [MAGI](https://magi.website) product family —
5
5
  - **React 18** + **TypeScript** (strict)
6
6
  - **Plain CSS** with `magi-` prefixed classes — works in any React app, no Tailwind, no Next, no Vite, no Cloudflare coupling
7
7
  - **CSS custom properties** for every token — readable from any stylesheet
8
- - **~16 kB** stylesheet (gzip ~3 kB), **~4 kB** JS (gzip ~1 kB)
8
+ - **CSS `@layer` cascade** — consumer unlayered styles always win (declared architecture, not accidental)
9
+ - **~24 kB** stylesheet (gzip ~4 kB), **~9 kB** JS (gzip ~3 kB)
9
10
 
10
11
  ## Install
11
12
 
@@ -25,16 +26,20 @@ import '@tokyo3rdhq/magi-design-system/styles.css';
25
26
  </body>
26
27
 
27
28
  // 3. Wrap your app in <ProductTheme> to optionally override the accent.
29
+ // 0.3.0+: ProductTheme is a context provider + sets accent CSS
30
+ // variables on <html>. No DOM wrapper.
28
31
  import { ProductTheme } from '@tokyo3rdhq/magi-design-system';
29
32
 
30
- <ProductTheme accent="green">
33
+ <ProductTheme accent="green" name="magi-portal">
31
34
  <App />
32
35
  </ProductTheme>
33
36
  ```
34
37
 
35
38
  ## API
36
39
 
37
- ### `<Container>` — centers and constrains max-width
40
+ ### Layout primitives
41
+
42
+ #### `<Container>` — centers and constrains max-width
38
43
 
39
44
  ```tsx
40
45
  import { Container } from '@tokyo3rdhq/magi-design-system';
@@ -53,7 +58,7 @@ import { Container } from '@tokyo3rdhq/magi-design-system';
53
58
  | `wide` | 1400 px |
54
59
  | `full` | 100 % |
55
60
 
56
- ### `<Section>` — page-level vertical rhythm
61
+ #### `<Section>` — page-level vertical rhythm
57
62
 
58
63
  ```tsx
59
64
  import { Section } from '@tokyo3rdhq/magi-design-system';
@@ -67,7 +72,7 @@ import { Section } from '@tokyo3rdhq/magi-design-system';
67
72
 
68
73
  `spacing` maps to `--magi-space-12` / `20` / `32` / `40` (xl jumps to `40` on ≥ 768 px viewports).
69
74
 
70
- ### `<Stack>` — flex primitive with semantic gap tokens
75
+ #### `<Stack>` — flex primitive with semantic gap tokens
71
76
 
72
77
  ```tsx
73
78
  import { Stack } from '@tokyo3rdhq/magi-design-system';
@@ -82,7 +87,9 @@ import { Stack } from '@tokyo3rdhq/magi-design-system';
82
87
  // wrap: boolean (default: true)
83
88
  ```
84
89
 
85
- ### `<Button>` — pill action element
90
+ ### UI primitives
91
+
92
+ #### `<Button>` — pill action element
86
93
 
87
94
  ```tsx
88
95
  import { Button } from '@tokyo3rdhq/magi-design-system';
@@ -95,7 +102,7 @@ import { Button } from '@tokyo3rdhq/magi-design-system';
95
102
 
96
103
  All buttons include hover, active, focus-visible, disabled, and loading states. Focus ring uses `--magi-focus-ring`.
97
104
 
98
- ### `<Card>` — dark surface
105
+ #### `<Card>` — dark surface
99
106
 
100
107
  ```tsx
101
108
  import { Card } from '@tokyo3rdhq/magi-design-system';
@@ -107,7 +114,7 @@ import { Card } from '@tokyo3rdhq/magi-design-system';
107
114
 
108
115
  `interactive` adds hover (border lift + surface darken) and active (1 px lift) states.
109
116
 
110
- ### `<Badge>` — compact status / metadata label
117
+ #### `<Badge>` — compact status / metadata label
111
118
 
112
119
  ```tsx
113
120
  import { Badge } from '@tokyo3rdhq/magi-design-system';
@@ -117,7 +124,127 @@ import { Badge } from '@tokyo3rdhq/magi-design-system';
117
124
  // dot: boolean (default: false)
118
125
  ```
119
126
 
120
- ### `<ProductTheme>` — accent override
127
+ #### `<Checkbox>` — restyled native checkbox
128
+
129
+ ```tsx
130
+ import { Checkbox } from '@tokyo3rdhq/magi-design-system';
131
+
132
+ // Uncontrolled with native input attrs:
133
+ <Checkbox aria-label="Accept terms" defaultChecked />
134
+
135
+ // Controlled with label:
136
+ <Checkbox
137
+ checked={providers.includes('nvidia')}
138
+ onChange={(e) => toggle('nvidia', e.target.checked)}
139
+ >
140
+ NVIDIA
141
+ </Checkbox>
142
+ ```
143
+
144
+ Native `<input type="checkbox">` under the hood. `type` prop is locked to `'checkbox'` (not extensible to `'radio'` etc.) via `Omit<InputHTMLAttributes, 'type'>`.
145
+
146
+ #### `<Input>` — `<input>` wrapper
147
+
148
+ ```tsx
149
+ import { Input } from '@tokyo3rdhq/magi-design-system';
150
+
151
+ <Input placeholder="e.g. Coding assistant" />
152
+ <Input type="password" placeholder="••••••••" />
153
+ <Input type="search" placeholder="Search models" />
154
+ <Input invalid defaultValue="bad value" />
155
+ // inputSize: 'sm' | 'md' | 'lg' (default: 'md')
156
+ // invalid: boolean (default: false)
157
+ ```
158
+
159
+ `inputSize` (not `size`) to avoid collision with the native `<input size>` HTML attribute.
160
+
161
+ #### `<FormField>` — label + control + helper / error
162
+
163
+ ```tsx
164
+ import { FormField, Input } from '@tokyo3rdhq/magi-design-system';
165
+
166
+ <FormField label="Email" helper="We'll never share this.">
167
+ <Input type="email" placeholder="you@example.com" />
168
+ </FormField>
169
+
170
+ <FormField label="Max models" error="Pick a value between 1 and 10.">
171
+ <Segmented value="3" options={...} onChange={...} />
172
+ </FormField>
173
+ ```
174
+
175
+ `<FormField>` wires `aria-describedby` to the helper/error span, `aria-labelledby` to
176
+ the label, and `aria-invalid="true"` to the child control — automatically via
177
+ `Children.only + cloneElement`. Works with any single focusable element
178
+ (`Input`, `Select`, native `<input>`, `Segmented`, etc.). For complex
179
+ multi-element layouts, no wiring is performed — the label and helper/error
180
+ still render.
181
+
182
+ #### `<Segmented>` — single-select chip group
183
+
184
+ ```tsx
185
+ import { Segmented } from '@tokyo3rdhq/magi-design-system';
186
+
187
+ <Segmented
188
+ value={contextMin}
189
+ options={[
190
+ { value: '128k', label: '128K+' },
191
+ { value: '32k', label: '32K+' },
192
+ { value: '8k', label: '8K+' },
193
+ { value: 'any', label: 'Any' },
194
+ ]}
195
+ onChange={(v) => setContextMin(v as '128k' | '32k' | '8k' | 'any')}
196
+ />
197
+
198
+ <Segmented value={cost} options={...} onChange={...} accent />
199
+ ```
200
+
201
+ **Single-select only.** For multi-select (e.g. provider toggles, multi-tag pickers),
202
+ use a `<Checkbox>` group instead — a multi-item segmented control implies
203
+ exclusive selection and confuses the interaction model.
204
+
205
+ #### `<Banner>` — inline notice with left-border accent
206
+
207
+ ```tsx
208
+ import { Banner } from '@tokyo3rdhq/magi-design-system';
209
+
210
+ <Banner variant="info">
211
+ No requirements set yet. Go back to specify what you're building.
212
+ </Banner>
213
+ <Banner variant="warning">
214
+ This action will reset all your saved configurations.
215
+ </Banner>
216
+ <Banner variant="error">
217
+ Could not load KV catalog: {error.message}.
218
+ </Banner>
219
+ <Banner variant="success">
220
+ Saved successfully. <a href="#">View changes</a>.
221
+ </Banner>
222
+ // variant: 'warning' | 'error' | 'success' | 'info' (default: 'warning')
223
+ ```
224
+
225
+ `role="alert"` for warning/error, `role="status"` for info/success.
226
+
227
+ #### `<EmptyState>` — centered placeholder
228
+
229
+ ```tsx
230
+ import { EmptyState } from '@tokyo3rdhq/magi-design-system';
231
+
232
+ // Simple:
233
+ <EmptyState>
234
+ No models match. Try loosening the constraints — <Link to="/">edit requirements</Link>.
235
+ </EmptyState>
236
+
237
+ // Structured:
238
+ <EmptyState
239
+ title="Nothing selected"
240
+ description="Pick models on the Browse page first."
241
+ action={<Button variant="primary" onClick={goToBrowse}>Go to Browse</Button>}
242
+ />
243
+ ```
244
+
245
+ ### Theme
246
+
247
+ #### `<ProductTheme>` — accent override (0.3.0+: no DOM wrapper)
121
248
 
122
249
  ```tsx
123
250
  import { ProductTheme } from '@tokyo3rdhq/magi-design-system';
@@ -125,14 +252,49 @@ import { ProductTheme } from '@tokyo3rdhq/magi-design-system';
125
252
  <ProductTheme accent="cyan" name="token-factory">
126
253
  <App />
127
254
  </ProductTheme>
128
- // accent: 'green' | 'cyan' | 'violet' | 'amber' | 'white' (default: 'green')
129
- // name: string (optional product identifier)
130
- // tokens: Partial<CSSProperties> (optional additional overrides)
255
+ // accent: 'green' | 'cyan' | 'violet' | 'amber' | 'white' (default: 'green')
256
+ // name: string (optional product identifier)
131
257
  ```
132
258
 
133
- `<ProductTheme>` renders a `<div data-magi-product="…">` and sets the four accent tokens as inline custom properties on the wrapper. Children inherit the override automatically via CSS variable resolution.
259
+ `name` sets `data-magi-product="<name>"` on `<html>` (not on a wrapper div).
260
+ This is the contract for scoped application identification.
261
+
262
+ **Removed in 0.3.0**: `tokens?: Partial<CSSProperties>` prop. Consumers can no
263
+ longer redefine arbitrary `--magi-*` variables through this component. For
264
+ subtree accent overrides, use `<div data-magi-accent="<name>">` instead.
265
+
266
+ #### Subtree accent override via `data-magi-accent`
267
+
268
+ ```tsx
269
+ // In any consumer JSX:
270
+ <div data-magi-accent="danger">
271
+ <Button variant="primary">Delete</Button>
272
+ </div>
273
+
274
+ <div data-magi-accent="warning">
275
+ <Banner variant="warning">…</Banner>
276
+ </div>
277
+ ```
278
+
279
+ Maps to:
280
+ - Accent presets: `green` / `cyan` / `violet` / `amber` / `white`
281
+ - Semantic: `danger` / `warning` / `success`
282
+
283
+ The button / banner / etc. inside reads `--magi-accent` via CSS and picks up
284
+ the override automatically. The mapping is shipped as static CSS in
285
+ `foundation/globals.css`.
134
286
 
135
- **Allowed in `tokens`**: only overrideable tokens (accent family). Refrain from setting `--magi-space-*`, `--magi-text-primary`, or base surfaces — see spec §10.
287
+ #### `useProductTheme()` hook
288
+
289
+ ```tsx
290
+ import { useProductTheme } from '@tokyo3rdhq/magi-design-system';
291
+
292
+ function MyComponent() {
293
+ const { accent } = useProductTheme();
294
+ // Returns { accent: ProductAccent } or { accent: 'green' } outside
295
+ // a <ProductTheme>.
296
+ }
297
+ ```
136
298
 
137
299
  #### Preset accent palettes
138
300
 
@@ -176,13 +338,33 @@ Every token is a CSS custom property on `:root`. See [`docs/tokens.md`](../../do
176
338
  }
177
339
  ```
178
340
 
341
+ ## CSS cascade model (0.3.0+)
342
+
343
+ `styles/index.css` declares layer order:
344
+
345
+ ```css
346
+ @layer magi.reset, magi.tokens, magi.foundation, magi.layout, magi.components;
347
+ ```
348
+
349
+ Consumer styles (unlayered) always win over our layered rules, regardless of
350
+ import order. This is declared architecture, not accidental source-order.
351
+
179
352
  ## Accessibility
180
353
 
181
354
  - All interactive elements have visible `:focus-visible` rings via `--magi-focus-ring`
182
355
  - `@media (prefers-reduced-motion: reduce)` collapses all motion durations to `0.01ms`
183
- - Buttons: `aria-busy` toggles with `loading`; `disabled` + `aria-disabled` covered
356
+ - `<Button>`: `aria-busy` toggles with `loading`; `disabled` + `aria-disabled` covered
357
+ - `<FormField>`: auto-wires `aria-describedby` / `aria-labelledby` / `aria-invalid` on the child control (0.3.0+)
358
+ - `<Banner>`: `role="alert"` for warning/error; `role="status"` for info/success
359
+ - `<Segmented>`: `role="radiogroup"` + `role="radio"` + `aria-checked`
184
360
  - Color tokens chosen to meet WCAG AA contrast on the dark background
185
361
 
362
+ ## Architecture decisions
363
+
364
+ See [`docs/adr/`](../../docs/adr/) for the 8 ADRs documenting key decisions:
365
+ token architecture, CSS scope, theme architecture, package boundary,
366
+ CSS bundling, React peer range, accessibility, visual regression.
367
+
186
368
  ## Build
187
369
 
188
370
  ```bash
@@ -201,7 +383,7 @@ Per spec §36 / §4:
201
383
  - Animation library
202
384
  - i18n (consumers handle it themselves)
203
385
  - Light theme (package is dark-first per spec §9)
204
- - Navbar, Footer, Tabs, Input, CodeBlock, ProductHeader — Phase 4, only after duplication is observed in 2+ products
386
+ - Navbar, Footer, Tabs, Spinner, Select, CodeBlock, ProductHeader — Phase 4+, only after duplication is observed in 2+ products
205
387
 
206
388
  ## License
207
389
 
@@ -4,7 +4,7 @@ export interface FormFieldProps {
4
4
  label: string;
5
5
  /** Optional helper text below the control. */
6
6
  helper?: string;
7
- /** Error message — overrides helper styling + sets aria-describedby. */
7
+ /** Error message — overrides helper styling + sets aria-invalid + aria-describedby. */
8
8
  error?: string;
9
9
  /** The control itself (Input, Select, Segmented, etc.). */
10
10
  children: ReactNode;
@@ -12,18 +12,29 @@ export interface FormFieldProps {
12
12
  align?: 'start' | 'center' | 'end' | 'stretch';
13
13
  /** Extra className for the wrapper. */
14
14
  className?: string;
15
- /** Optional id for the helper/error element (for aria-describedby). */
15
+ /** Optional id for the helper/error element (overrides the auto-generated one). */
16
16
  helperId?: string;
17
17
  }
18
18
  /**
19
19
  * FormField — label + control + optional helper/error wrapper.
20
20
  *
21
- * Wires the helper or error element to the input via `aria-describedby`,
22
- * so screen readers announce them when the control receives focus.
21
+ * Wires `aria-describedby` to the helper/error span and `aria-labelledby`
22
+ * to the label, by cloning the single child element with the appropriate
23
+ * ARIA attributes. Sets `aria-invalid="true"` on the child when `error` is
24
+ * present.
25
+ *
26
+ * **Consumer-supplied ARIA attributes are preserved (merged, not overwritten).**
27
+ * For example, if the consumer passes `aria-describedby="external-help"`,
28
+ * the resulting attribute is `"external-help <generated-helper-id>"`.
29
+ * This matches the WAI-ARIA spec for multi-value id lists.
30
+ *
31
+ * The child MUST be a single focusable element (Input, Select, native
32
+ * `<input>`, Segmented, etc.). If `children` is not a valid React element,
33
+ * the ARIA wiring is skipped — the label and helper/error still render.
23
34
  *
24
35
  * @example
25
36
  * <FormField label="Email" helper="We'll never share this.">
26
- * <Input type="email" />
37
+ * <Input type="email" placeholder="you@example.com" />
27
38
  * </FormField>
28
39
  *
29
40
  * <FormField label="Max models" error="Pick a value between 1 and 10.">
@@ -1 +1 @@
1
- {"version":3,"file":"FormField.d.ts","sourceRoot":"","sources":["../../../src/components/FormField/FormField.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG9C,MAAM,WAAW,cAAc;IAC7B,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,QAAQ,EAAE,SAAS,CAAC;IACpB,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAC;IAC/C,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,MAAM,EACN,KAAK,EACL,QAAQ,EACR,KAAiB,EACjB,SAAS,EACT,QAAQ,GACT,EAAE,cAAc,+BA6BhB"}
1
+ {"version":3,"file":"FormField.d.ts","sourceRoot":"","sources":["../../../src/components/FormField/FormField.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAUf,MAAM,WAAW,cAAc;IAC7B,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,QAAQ,EAAE,SAAS,CAAC;IACpB,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAC;IAC/C,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAeD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,MAAM,EACN,KAAK,EACL,QAAQ,EACR,KAAiB,EACjB,SAAS,EACT,QAAQ,GACT,EAAE,cAAc,+BAsDhB"}