@oxide/design-system 6.5.4 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/design.md ADDED
@@ -0,0 +1,664 @@
1
+ # Oxide Design System
2
+
3
+ > The styles, tokens, themes, and base components shared across Oxide's web
4
+ > surfaces. Published as `@oxide/design-system`.
5
+
6
+ ```yaml
7
+ system:
8
+ name: Oxide Design System
9
+ package: "@oxide/design-system"
10
+ default_theme: dark # :root is dark; light is opt-in via [data-theme="light"]
11
+ color_space: oklch # every color is authored in OKLCH
12
+ framework: tailwind v4 # tokens are exposed as @theme + @utility, consumed as classes
13
+
14
+ typography:
15
+ sans: "SuisseIntl, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif"
16
+ mono: "'GT America Mono', monospace" # uppercase, used for UI chrome & labels
17
+ scale_px: [11, 12, 14, 16, 18, 20, 22, 25, 28, 36, 50, 52, 65]
18
+ weights: [400 regular, 500 semi]
19
+ tracking: optical # letter-spacing ∝ 1/size; small opens up, display tightens
20
+
21
+ color_scales: # OKLCH, perceptually even
22
+ neutral: [0, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300]
23
+ hue: [blue, purple, red, yellow, green] # steps 200–1300
24
+ # step encodes lightness: 0 = darkest, 1300 = lightest
25
+
26
+ semantic_intents: # mapped per theme onto the scales above
27
+ accent: green # the brand accent; themeable (see "Accent Theming")
28
+ accent-alt: purple
29
+ success: green
30
+ error: red
31
+ destructive: red
32
+ notice: yellow
33
+ info: blue
34
+
35
+ token_layers: # the three families you compose UI from
36
+ surface: bg-* # backgrounds & fills
37
+ content: text-* # text & foreground
38
+ stroke: border-* / ring-* / outline-* # 1px lines
39
+
40
+ radius_px: { sm: 1, md: 2, lg: 4, xl: 6, full: 9999 }
41
+
42
+ shadows: [border, border-small, border-medium, border-large, menu, menu-inset, toast, modal, tooltip]
43
+
44
+ breakpoints_px: [300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1600]
45
+
46
+ components: [Button, Badge, Checkbox, Listbox, Spinner, Tabs]
47
+ asciidoc: [Section, Admonition, Table, TableOfContents] # long-form content rendering
48
+ icons: { react: "@oxide/design-system/icons/react", sprite: "@oxide/design-system/icons" }
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Overview
54
+
55
+ Oxide builds hardware and the software that runs on it. The design system is
56
+ **dense, precise, and technical** to match.
57
+
58
+ The brand combines two elements: a modernist **Swiss Style** (type, color,
59
+ composition) for structure, and a low-level **ASCII "machine language" texture** as
60
+ the expressive layer.
61
+
62
+ - **Dark-first.** Default theme is dark; light is an explicit opt-in.
63
+ - **Monospace.** UI chrome — labels, badges, buttons, table headers, nav — is
64
+ uppercase `GT America Mono`; prose and headings are `SuisseIntl`. The contrast is
65
+ the system's signature.
66
+ - **Tight geometry.** Radii top out at 6px. Edges are crisp; surfaces separate by
67
+ hairline strokes plus soft ambient shadow, never heavy fills.
68
+ - **Visible grids.** Let the underlying structure show — an ASCII character grid, the
69
+ alignment of a server room. See [ASCII](#ascii).
70
+ - **OKLCH throughout.** Colors are authored in OKLCH so scales are perceptually even
71
+ and lightness is predictable across hues.
72
+ - **Semantic, not literal.** Name a *role* (`bg-raise`, `text-secondary`,
73
+ `border-error`), not a color; the theme resolves it.
74
+
75
+ Ships as CSS (Tailwind v4 `@theme`/`@utility`) plus a small set of React components.
76
+ Most UI composes the semantic utility classes directly; components cover the
77
+ interactions that are hard to get right by hand.
78
+
79
+ ---
80
+
81
+ ## Colors
82
+
83
+ The palette is **neutral-first with saturated accents**, drawn from (in-part) the monochrome
84
+ phosphors of early CRTs. Green is the primary accent; yellow, blue, purple, and red
85
+ carry information and state. Neither canvas goes to pure black or white — both pull a
86
+ step inward (`neutral-0`, `neutral-1300`) to avoid clipping contrast.
87
+
88
+ ### Scales
89
+
90
+ OKLCH scales. The **neutral** scale runs fifteen steps (`0, 50, 100, 200 … 1300`);
91
+ the five **hue** scales — `blue`, `purple`, `red`, `yellow`, `green` — run `200` to
92
+ `1300`. The step encodes lightness: **`0` darkest, `1300` lightest**, regardless of
93
+ hue. Because steps are spaced in OKLCH, the same step reads as the same perceived
94
+ lightness across hues.
95
+
96
+ <!-- @generated colors:examples (run: npm run design-md) -->
97
+ ```css
98
+ --color-neutral-0: oklch(0.162 0.01 260); /* dark: surface-default · light: content-raise */
99
+ --color-neutral-300: oklch(0.316 0.006 260); /* dark: surface-tertiary, stroke-default · light: content-default */
100
+ --color-neutral-800: oklch(0.79 0.0011 260); /* dark: content-default · light: content-quaternary, stroke-raise */
101
+ --color-neutral-1300: oklch(0.995 0.0001 260); /* light: surface-default */
102
+ --color-green-800: oklch(0.77 0.1919 163.7); /* the default accent anchor */
103
+ --color-red-800: oklch(0.712 0.185 11.3); /* error / destructive anchor */
104
+ --color-yellow-800: oklch(0.837 0.14 75); /* notice anchor */
105
+ --color-blue-800: oklch(0.71 0.15 272); /* info anchor */
106
+ ```
107
+ <!-- /@generated -->
108
+
109
+ Raw scale colors are available as Tailwind classes (`bg-green-800`, `text-red-200`),
110
+ but reach for them **only inside the design system** or for rare decoration. Product
111
+ code uses semantic tokens.
112
+
113
+ `800` is the canonical highlight for each accent, mid-scale. Dark themes use mostly
114
+ `800` and below, light `800` and above.
115
+
116
+ ### Semantic intents
117
+
118
+ Each scale maps to **intents** that carry meaning — stable names that hold across
119
+ themes:
120
+
121
+ | Intent | Meaning | Mapped scale |
122
+ | ------------- | ------------------------------------ | ------------ |
123
+ | `accent` | brand, primary actions, selection | green* |
124
+ | `accent-alt` | secondary accent / decorative | purple |
125
+ | `success` | confirmation, healthy state | green |
126
+ | `error` | validation failure | red |
127
+ | `destructive` | dangerous actions (delete) | red |
128
+ | `notice` | warnings, attention | yellow |
129
+ | `info` | neutral informational state | blue |
130
+
131
+ \* `accent` is themeable — see [Accent Theming](#accent-theming).
132
+
133
+ ### The three token layers
134
+
135
+ All product UI is painted from three families of semantic tokens. Each resolves to
136
+ the right scale step for the active theme, so the *same class works in light and
137
+ dark*.
138
+
139
+ **`surface` → backgrounds** (`bg-*`)
140
+ ```
141
+ bg-default page / base canvas
142
+ bg-raise raised panels, cards, popovers (one step toward the viewer)
143
+ bg-secondary inset / secondary panels
144
+ bg-tertiary deepest inset
145
+ bg-hover interactive hover fill
146
+ bg-disabled disabled fill
147
+ bg-accent accent fill (selected, primary button)
148
+ bg-accent-hover / bg-accent-secondary
149
+ bg-destructive / bg-error / bg-notice / bg-info (+ -hover, -secondary, -inverse)
150
+ bg-scrim modal backdrop
151
+ ```
152
+
153
+ **`content` → text & foreground** (`text-*`)
154
+ ```
155
+ text-raise highest-contrast text (headings, emphasis)
156
+ text-default body text
157
+ text-secondary supporting text
158
+ text-tertiary hints, metadata
159
+ text-quaternary faintest (placeholder-level)
160
+ text-disabled disabled text
161
+ text-accent accent text & icons (+ -secondary, -tertiary, -disabled)
162
+ text-error / text-success / text-notice / text-info (+ tiers)
163
+ text-inverse text on inverse surfaces
164
+ ```
165
+
166
+ **`stroke` → 1px lines** (`border-*`, `ring-*`, `outline-*`)
167
+ ```
168
+ border-default standard hairline
169
+ border-raise stroke on raised surfaces
170
+ border-secondary quieter divider
171
+ border-tertiary faintest divider
172
+ border-accent accent outline (focus, selection) (+ -secondary … -quaternary)
173
+ border-error / border-success / border-notice / border-info (+ tiers)
174
+ ```
175
+
176
+ Each `stroke` token exists identically as `border-`, `ring-`, and `outline-`, so
177
+ focus rings and borders stay in sync.
178
+
179
+ > **Hierarchy through tier, not opacity.** Step through `default → secondary →
180
+ > tertiary → quaternary` for hierarchy. Don't fake it with `/50` opacity on a solid
181
+ > token — the tiers are tuned per theme; opacity isn't.
182
+
183
+ ### Accent Theming
184
+
185
+ The default accent is **green**. Re-accent any subtree with a theme class —
186
+ `blue-theme`, `red-theme`, `yellow-theme`, `purple-theme`, `green-theme` — which
187
+ remaps `--theme-accent-*` onto that hue. Everything painted with `accent` tokens
188
+ (`bg-accent`, `text-accent`, `border-accent`, focus rings) follows automatically.
189
+
190
+ ```html
191
+ <!-- An info-flavored region: accent tokens now resolve to blue -->
192
+ <section class="blue-theme">
193
+ <span class="text-accent">Linked</span>
194
+ <div class="border-accent-secondary">…</div>
195
+ </section>
196
+ ```
197
+
198
+ This is how status surfaces are built — an error toast wraps its content in
199
+ `red-theme`, a warning in `yellow-theme` — so one set of `accent` classes renders in
200
+ the right semantic color.
201
+
202
+ ### Colorways
203
+
204
+ Compositions hold to one of two structures, both on a dark neutral base with white
205
+ hero text:
206
+
207
+ - **Mono** — base + one hue.
208
+ - **Duo** — base + two hues.
209
+
210
+ Hierarchy *within* a colorway comes from tonal steps of one hue (`green-800` beside
211
+ `green-600` with white), not from adding hues. Add color deliberately; fewer hues
212
+ read as more precise.
213
+
214
+ **The accent is rationed.** Aim for one, maybe two accent objects on a screen. Use
215
+ `accent` only when it *means* something (the primary action, focus, a healthy or
216
+ active state, a link), never decoration; used everywhere, it signals nothing. The
217
+ other hues are stricter — `notice`, `error`, `info`, and `accent-alt` appear only for
218
+ their semantic role. And there are **no gradients** anywhere except the modal scrim:
219
+ flat fills only.
220
+
221
+ ### Accessibility
222
+
223
+ Targets **WCAG 2.1 AA** minimum. Since neither canvas is pure black or white, check
224
+ contrast against the *actual* surface token, not `#000`/`#fff`. The content tiers
225
+ (`text-default → secondary → tertiary → quaternary`) stay legible on their intended
226
+ surfaces — go below `tertiary` only for non-essential text. The palette is OKLCH, so
227
+ lean on a perceptual model (**APCA**) alongside the WCAG 2 ratio; it better predicts
228
+ reading on mid-tone surfaces.
229
+
230
+ - **Never carry meaning in color alone.** A state that says "error" only by being red
231
+ disappears for a color-blind user. Pair the hue with a second cue — a label, icon,
232
+ or shape. A status `Badge` reads *Failed*; it doesn't just turn red.
233
+ - **Match `color-scheme` to the theme.** Set `color-scheme: dark` on root (`light`
234
+ under `[data-theme="light"]`) so native controls, scrollbars, and form widgets
235
+ follow, and point `<meta name="theme-color">` at the canvas token so browser chrome
236
+ blends into the page.
237
+
238
+ ---
239
+
240
+ ## Typography
241
+
242
+ Two typefaces with a clear division of labor:
243
+
244
+ - **`SuisseIntl`** (`--font-sans`) — prose, headings, form values, anything read as
245
+ language.
246
+ - **`GT America Mono`** (`--font-mono`) — UI chrome: labels, badges, buttons, table
247
+ headers, nav, timestamps. **Uppercase**, with stylistic sets (`ss02/03/06/07/08/09`)
248
+ on and contextual alternates off; tracking opens ~4%. Lowercase mono is the
249
+ exception, only where casing carries meaning (code).
250
+
251
+ Following Swiss practice, **hierarchy comes from tone, not weight**. Sans is Regular
252
+ almost everywhere; emphasis steps a heading lighter (`text-raise`) against copy a few
253
+ tiers darker (`text-secondary` / `text-tertiary`) at the same size. The `500` "semi"
254
+ weight is used sparingly — UI labels, the occasional blog heading — never as the
255
+ default for emphasis.
256
+
257
+ ### Optical tracking
258
+
259
+ Sans letter-spacing follows an optical curve, not a fixed value: tracking ∝ `1/size`
260
+ (in `em`, so it scales with the type). Small text **opens up** (`+0.049em` at 11px);
261
+ display **tightens** (`−0.025em` at 65px); the zero crossing sits near 24px. The type
262
+ utilities apply it — never set `letter-spacing` by hand.
263
+
264
+ ### Type scale
265
+
266
+ Sans primitives are named by pixel size (`text-sans-11 … text-sans-65`). Prefer the
267
+ **semantic aliases** in product code so intent survives a scale change:
268
+
269
+ | Semantic | Size | Use |
270
+ | ------------------- | ---- | ---------------------------- |
271
+ | `text-sans-sm` | 12 | dense secondary text |
272
+ | `text-sans-md` | 14 | **default body** |
273
+ | `text-sans-lg` | 16 | lead body |
274
+ | `text-sans-xl` | 18 | small headings |
275
+ | `text-sans-2xl` | 25 | section heading |
276
+ | `text-sans-3xl` | 36 | page heading |
277
+ | `text-sans-4xl` | 52 | display |
278
+ | `text-sans-5xl` | 65 | hero display |
279
+
280
+ A `500`-weight set mirrors the small sizes: `text-sans-semi-sm/md/lg/xl`.
281
+
282
+ **Responsive headings** ramp across breakpoints automatically — prefer these for page
283
+ structure:
284
+
285
+ ```
286
+ heading-display 36 → 52 @800 → 65 @1000 hero / banner titles
287
+ heading-xl 25 → 36 @600 → 52 @1000 primary section <h2>
288
+ heading-lg 18 → 25 @600 → 36 @1000 secondary heading <h3>
289
+ heading-md 16 → 18 @600 → 25 @1000 subsection <h4>
290
+ ```
291
+
292
+ ### Mono scale
293
+
294
+ ```
295
+ text-mono-xs 11px uppercase smallest labels, eyebrows
296
+ text-mono-sm 12px uppercase badges, buttons, table headers (the workhorse)
297
+ text-mono-md 14px uppercase larger labels
298
+ text-mono-code 12px normal case inline & block code (alternates off, no tracking)
299
+ ```
300
+
301
+ `text-mono-code` is the only mono utility **not** uppercased — for code, where casing
302
+ matters. Inline code in prose uses the `inline-code` utility (tinted background +
303
+ hairline border, sized to `0.825em` of its context).
304
+
305
+ ### ASCII as type
306
+
307
+ ASCII-inspired characters double as typographic elements: list markers, dividers,
308
+ arrows, accents. A list might swap its bullets for colored terminal characters (`▸`,
309
+ `●`, `└─`); a divider might be a run of box characters. Set these in `GT America Mono`
310
+ to keep the character grid (see [ASCII](#ascii)).
311
+
312
+ ---
313
+
314
+ ## ASCII
315
+
316
+ ASCII is the expressive counterpart to the Swiss frame. On the web it appears as
317
+ decorative patterns, illustrative diagrams, and typographic accents — the reason the
318
+ [Visible grids](#overview) principle exists.
319
+
320
+ **Grid integrity is the one hard rule.** Each cell is exactly one monospace
321
+ character, wide and tall. Add **no** extra letter- or line-spacing — `GT America Mono`
322
+ at `0%` tracking, `110%` leading — or the art shears off its grid. Box-drawing and
323
+ block glyphs (`▁▂▃▄▅▆▇█ ▏▎▍▌▋▊▉ ╲╱ ● ○ ■ □ ▲ ▶ ▼ ◀`) rely on stylistic sets `ss06` +
324
+ `ss07`.
325
+
326
+ Patterns range from bold macro shapes to fine textural fields, from a single repeated
327
+ symbol to a varied set. In color, ASCII follows the [colorways](#colorways): structure
328
+ in the accent hue over a fainter background layer suggesting a grid or digital
329
+ surface, without competing with legibility. Keep it minimal; let the ASCII support the
330
+ content, not overpower it.
331
+
332
+ Two tools produce brand-correct output:
333
+
334
+ - **Mitos** (`mitos.shared.oxide.computer`) — Oxide's ASCII generator: control over
335
+ character set, column density, and scale, image-to-ASCII conversion, and custom code
336
+ for generative or animated textures. Exports SVG or pastes into Figma (`GT America
337
+ Mono` at `0%` letter-spacing).
338
+ - **Monodraw** — a Mac ASCII editor for hand-drawn diagrams; set its preview font to
339
+ the brand mono with the extended character set.
340
+
341
+ For terminal/system moments, ASCII can drive motion — text resolving through cycling
342
+ binary and symbols, as in lower-third title reveals.
343
+
344
+ ---
345
+
346
+ ## Imagery & Texture
347
+
348
+ Photography and texture stay as disciplined as the type.
349
+
350
+ - **Backgrounds are flat.** Solid black (`bg-default`) on dark, solid white on light —
351
+ no gradient washes, no bluish-purple hero fades. The only "gradient" is the modal
352
+ scrim.
353
+ - **Hardware, shot cool.** Brand imagery is photographic hardware — the rack, the
354
+ sled, the boards — with a cool-to-neutral cast, never warm. No stock-photo poses,
355
+ clouds-with-faces, or mascots.
356
+ - **ASCII is the illustration language.** Where another brand reaches for an isometric
357
+ vector scene or a 3D blob, Oxide reaches for [ASCII](#ascii). Hand-drawn or
358
+ "friendly" illustration is off-brand.
359
+ - **Grain, used quietly.** A subtle noise/grain may sit over hero photography, and a
360
+ diamond-stripe motif appears on covers and avatars — texture, never loud enough to
361
+ compete with content.
362
+
363
+ ---
364
+
365
+ ## Layout & Spacing
366
+
367
+ Spacing uses Tailwind's default `0.25rem` (4px) step scale (`p-2` = 8px, `gap-4` =
368
+ 16px). Conventions:
369
+
370
+ - **Control height is the rhythm unit.** Controls are `h-10` (40px) at base, `h-8`
371
+ (32px) when compact. Buttons, inputs, and listboxes share these heights so they
372
+ align side by side.
373
+ - **Group with 8, separate with 16.** Tighten related items to `gap-2`; separate
374
+ groups with `gap-4` or more.
375
+ - **Twelve-column grid.** Layouts are generally (not exclusively) on a 12-column grid
376
+ — enough divisions for consistent alignment across varied content.
377
+ - **Breakpoints are pixel-named** (`300 … 1600`), used as `min-width` prefixes:
378
+ `800:text-sans-lg`. The dense range (`300–700`) exists because product views pack a
379
+ lot of instrumentation into narrow columns.
380
+ - **Hit targets reach 44px on touch.** Visual heights stay tight (32–40px), but touch
381
+ needs a **44px minimum** target — extend the hit area with padding or a
382
+ pseudo-element rather than enlarging the visible control. Gate hover affordances
383
+ behind `@media (hover: hover) and (pointer: fine)` so a tap doesn't leave a control
384
+ stuck in hover.
385
+ - **No layout shift.** Reserve space for anything that changes: set changing numbers
386
+ (counters, metrics, timers) in `tabular-nums`, give async content fixed dimensions
387
+ or a skeleton, and never swap font weight on hover or select. This is the practical
388
+ companion to *hierarchy from tone, not weight* — see [Typography](#typography).
389
+ - **Optical over geometric.** Trust the eye: nudge a glyph, icon, or edge by ±1px when
390
+ the math looks wrong. Align every element to *something* — nothing floats unanchored.
391
+
392
+ ---
393
+
394
+ ## Elevation & Depth
395
+
396
+ Depth is **a hairline border plus a soft ambient shadow**, layered — not a single drop
397
+ shadow. The border keeps edges crisp at any zoom; the shadow supplies the light. Both
398
+ have light- and dark-theme values.
399
+
400
+ ```
401
+ shadow-border 1px ambient border (the base for everything else)
402
+ shadow-border-small border + subtle lift — buttons, inputs at rest
403
+ shadow-border-medium border + medium lift — raised cards
404
+ shadow-border-large border + large lift — prominent cards
405
+ shadow-menu stacked shadow for dropdowns
406
+ shadow-menu-inset menu shadow + inset outline — overlap ring without clipping
407
+ shadow-toast toast elevation
408
+ shadow-modal modal elevation (largest)
409
+ shadow-tooltip tooltip elevation (smallest)
410
+ ```
411
+
412
+ There are **three elevation levels** — resting, raised, floating — expressed by the
413
+ `small` / `medium` / `large` border-shadows; `menu`, `modal`, `toast`, and `tooltip`
414
+ are tuned floating variants for specific overlays. Match the shadow to the job: a
415
+ resting control gets `shadow-border-small`, a popover `shadow-menu`, a dialog
416
+ `shadow-modal`. Don't stack a `shadow-*` on a separate `border` — the `shadow-border-*`
417
+ family already includes the line.
418
+
419
+ ---
420
+
421
+ ## Shapes
422
+
423
+ Radii are intentionally tight.
424
+
425
+ ```
426
+ rounded-sm 1px checkboxes, tiny chips
427
+ rounded-md 2px default for most controls & cards (the implicit default)
428
+ rounded-lg 4px larger cards, modals
429
+ rounded-xl 6px the largest corner the system uses
430
+ rounded-full pill badges-as-pills, avatars, toggles
431
+ ```
432
+
433
+ When nesting rounded elements, the inner radius should be smaller than the outer so
434
+ the curves stay concentric. Because the scale is tight, most nesting resolves to `md`
435
+ inside `lg`.
436
+
437
+ ---
438
+
439
+ ## Motion
440
+
441
+ Motion **explains a change, not decorates it** — what appeared, where it came from,
442
+ where it went. If an animation doesn't clarify cause and effect, leave it out.
443
+
444
+ - **Honor `prefers-reduced-motion`.** Where necessary add reduced variants also. E.g. the
445
+ `Spinner` slows its rotation and drops the dash animation.
446
+ - **Animate compositor-friendly properties** — `transform` and `opacity`. Avoid layout
447
+ (`width`, `top`, `height`) and `transition: all`.
448
+ - **Match easing to the motion.** `ease-out` for anything **entering or leaving**
449
+ (dropdowns, modals, toasts) — the fast start reads as instant. `ease-in-out` for
450
+ elements **already on screen** that reposition or morph. Plain `ease` for **hover and
451
+ color**. Avoid `ease-in` (the slow start feels sluggish); reserve `linear` for
452
+ constant motion (marquees, hold-to-confirm progress).
453
+ - **Keep it short; scale with size and frequency.** UI transitions run **150–300ms**;
454
+ let an exit run quicker than its entrance, and give larger moves a longer
455
+ curve (the TOC accordion uses ~300ms `cubic-bezier(0.87, 0, 0.13, 1)`). **The more
456
+ often a control is used, the less it should animate** — something hit dozens of times
457
+ a day should feel instant. A quick `scale(0.97)` on `:active` is the exception:
458
+ tactile feedback with no duration to wait through.
459
+ - **Animate from near, not nothing.** Enter from `scale(0.95)` and a few pixels of
460
+ offset, not `scale(0)` or a long slide — elements should settle in, not fly in.
461
+ Elements that move as a unit (modal + scrim, tooltip + arrow) share one easing and
462
+ duration.
463
+ - **Make motion interruptible.** State-driven transitions reverse cleanly if the user
464
+ changes their mind mid-animation; never trap input behind an animation.
465
+ - **`transform-origin` matters.** Scale/reveal animations originate from the element's
466
+ anchor (the trigger it expanded from), not its center.
467
+ - **Skeletons for content, spinners for actions.** A skeleton that mirrors the final
468
+ layout beats a spinner for loading regions — it holds the space and avoids a shift on
469
+ arrival. Reserve the `Spinner` for button-load states and small inline waits. No
470
+ spring physics, no bounce, no scroll-jacked choreography; the vocabulary is short
471
+ fades and small translates.
472
+
473
+ ---
474
+
475
+ ## Interaction States
476
+
477
+ Feedback is quiet — a control acknowledges you without restyling itself.
478
+
479
+ - **Hover** shifts the surface one step up the neutral ramp (`bg-default → bg-hover`);
480
+ text holds its color, weight, and position. Body text never recolors on hover.
481
+ - **Press** acknowledges with a slight transform, not a color or shape change — same
482
+ size and weight, it just registers the tap.
483
+ - **Disabled** dims the control, sets `cursor: not-allowed`, and — as the `Button` does
484
+ — drops pointer events so the state is real, not just visual.
485
+ - **Focus** is a 2px ring in `outline-accent-secondary`, offset 0; destructive controls
486
+ focus in `outline-destructive-secondary`.
487
+
488
+ ---
489
+
490
+ ## Components
491
+
492
+ A small selection of shared React components are found under `@oxide/design-system/ui`. Most
493
+ live within the repos directly.
494
+
495
+ ### Button
496
+
497
+ ```tsx
498
+ import { Button } from '@oxide/design-system/ui'
499
+
500
+ <Button variant="primary" size="base" onClick={save}>Save</Button>
501
+ <Button variant="secondary" size="sm">Cancel</Button>
502
+ <Button variant="danger" loading={deleting}>Delete</Button>
503
+ <Button variant="ghost" size="icon" aria-label="Settings"><Settings16Icon /></Button>
504
+ ```
505
+
506
+ - `variant`: `primary` (accent fill) · `secondary` (neutral) · `ghost` (transparent
507
+ until hover) · `danger` (destructive). Default `primary`.
508
+ - `size`: `base` (h-10) · `sm` (h-8) · `icon` (square). Default `base`.
509
+ - `loading` overlays a centered `Spinner`, hides the label, blocks clicks.
510
+ - Disabled and loading set `aria-disabled` and drop pointer events — not just dimmed.
511
+ Focus ring `outline-accent-secondary` (`outline-destructive-secondary` for `danger`).
512
+ - Labels are uppercase mono (`text-mono-sm`). A `buttonStyle()` helper gives the button
513
+ look on a non-`<button>` element (e.g. a link).
514
+
515
+ ### Badge
516
+
517
+ ```tsx
518
+ import { Badge } from '@oxide/design-system/ui'
519
+
520
+ <Badge color="default">Running</Badge>
521
+ <Badge color="destructive" variant="solid">Failed</Badge>
522
+ <Badge color="blue">GET</Badge>
523
+ ```
524
+
525
+ - `color`: `default` (accent) · `destructive` · `notice` · `neutral` · `purple` ·
526
+ `blue`. `variant`: `default` (tinted) · `solid` (filled).
527
+ - Small uppercase mono, inset ring (`ring-current/15`), `h-4`. For status and metadata
528
+ — HTTP methods in the docs, resource states in the console.
529
+
530
+ ### Checkbox
531
+
532
+ ```tsx
533
+ import { Checkbox } from '@oxide/design-system/ui'
534
+
535
+ <Checkbox checked={on} onChange={toggle}>Enable telemetry</Checkbox>
536
+ <Checkbox indeterminate={some} onChange={toggleAll}>Select all</Checkbox>
537
+ ```
538
+
539
+ - Native `<input type="checkbox">` with appearance reset. `indeterminate` is applied
540
+ via ref (no HTML attribute) and renders a bar instead of the checkmark. Checked fills
541
+ with `bg-accent`.
542
+
543
+ ### Listbox
544
+
545
+ ```tsx
546
+ import { Listbox } from '@oxide/design-system/ui'
547
+
548
+ <Listbox
549
+ selected={zone}
550
+ onChange={setZone}
551
+ items={[{ value: 'a', label: 'Zone A' }, { value: 'b', label: 'Zone B' }]}
552
+ placeholder="Select a zone"
553
+ />
554
+ ```
555
+
556
+ - An accessible select on Headless UI + Floating UI (flip, width-sync, 12px offset).
557
+ Props: `selected`, `onChange`, `items`, `placeholder`, `disabled`, `hasError`,
558
+ `isLoading`, `name`.
559
+ - Items are `{ value, label }`; when `label` is a React node, supply `labelString` for
560
+ the accessible/typeahead name. `hasError` switches strokes to `border-error-*`;
561
+ `isLoading` shows a `SpinnerLoader`. The menu uses `shadow-menu-inset` so its focus
562
+ ring overlaps the border without clipping.
563
+
564
+ ### Spinner
565
+
566
+ ```tsx
567
+ import { Spinner, SpinnerLoader } from '@oxide/design-system/ui'
568
+
569
+ <Spinner size="base" variant="primary" />
570
+ <SpinnerLoader isLoading={pending} minTime={500}><Result /></SpinnerLoader>
571
+ ```
572
+
573
+ - `size`: `base` (12px) · `lg` (36px). `variant` matches the button variants.
574
+ - `SpinnerLoader` enforces a **`minTime` floor (default 500ms)** so a spinner never
575
+ flickers out instantly — it stays visible long enough to read. Respects
576
+ `prefers-reduced-motion`.
577
+
578
+ ### Tabs
579
+
580
+ ```tsx
581
+ import { Tabs } from '@oxide/design-system/ui'
582
+
583
+ <Tabs.Root defaultValue="overview">
584
+ <Tabs.List>
585
+ <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
586
+ <Tabs.Trigger value="metrics">Metrics <Badge>12</Badge></Tabs.Trigger>
587
+ </Tabs.List>
588
+ <Tabs.Content value="overview">…</Tabs.Content>
589
+ <Tabs.Content value="metrics">…</Tabs.Content>
590
+ </Tabs.Root>
591
+ ```
592
+
593
+ - Radix-based compound component. Triggers are uppercase mono; the active one is marked
594
+ with `text-accent` and an accent bottom border. Triggers can carry a count `Badge`.
595
+
596
+ ### AsciiDoc rendering
597
+
598
+ For long-form content (the site and docs render AsciiDoc), the system exports
599
+ `Section`, `Admonition`, `Table`, and `TableOfContents`. Behaviors: auto-anchored
600
+ headings with a hover link icon; admonitions that re-accent by type (`note` → green,
601
+ `tip` → purple, `caution`/`important` → yellow, `warning` → red); horizontally
602
+ scrollable tables with sticky hairline borders; and a `useActiveSectionTracking` hook
603
+ driving a scroll-synced table of contents.
604
+
605
+ ---
606
+
607
+ ## Icons
608
+
609
+ Two delivery formats from the same Figma-exported source:
610
+
611
+ - **React components** — `@oxide/design-system/icons/react`. Import named components
612
+ (`Error12Icon`, `Sparkle16Icon`, …) and style with `currentColor`. Best for app code
613
+ with SVGR support (the console uses this exclusively).
614
+
615
+ ```tsx
616
+ import { Error12Icon } from '@oxide/design-system/icons/react'
617
+ <Error12Icon className="text-error" />
618
+ ```
619
+
620
+ - **Spritesheet** — `@oxide/design-system/icons` ships a `sprite.svg` and an `Icon`
621
+ type. For where SVGR isn't available (the site and docs), via a wrapper rendering
622
+ `<use href="…#name-size">`.
623
+
624
+ Names encode size (`Error12Icon` is the 12px artwork), provided at **24, 16, and
625
+ 12px**. Pick the size matching adjacent type — 12/16px icons sit with mono labels; size
626
+ icons to the cap height of neighboring text, not larger.
627
+
628
+ Icons are **filled by default**; outlined variants exist for the lighter weight wanted
629
+ inline with text. Drawn on a **24×24 grid with a ~2px margin**, with optical exceptions
630
+ — some shapes extend past the margin to keep visual weight even across the set.
631
+
632
+ ---
633
+
634
+ ## Do & Don't
635
+
636
+ **Do**
637
+ - Use semantic tokens (`bg-raise`, `text-secondary`, `border-error`) so UI re-themes
638
+ for free across light/dark and accent themes.
639
+ - Establish hierarchy through token tiers (`default → secondary → tertiary`), not
640
+ opacity.
641
+ - Use the type utilities for everything — they carry the optical tracking, line height,
642
+ and mono features you can't easily reproduce by hand.
643
+ - Give every interactive control a visible focus ring (`outline-accent-secondary`) and
644
+ an accessible name.
645
+ - Set changing numbers in `tabular-nums` and reserve space for dynamic content.
646
+
647
+ **Don't**
648
+ - Frequently reach for raw scale colors (`bg-green-800`) in product code — those are for
649
+ system internals and occasional decoration.
650
+ - Splash the accent around or use a hue as decoration; color is rationed, and `notice`,
651
+ `error`, `info`, and `accent-alt` appear only for their meaning.
652
+ - Use a gradient anywhere but the modal scrim — backgrounds are flat black or white.
653
+ - Invent radii, shadows, or type sizes outside the scales.
654
+ - Animate a control a user touches dozens of times a day, or start an entrance from
655
+ `scale(0)` — animate from `scale(0.95)` so elements settle in.
656
+ - Encode status in color alone; back every intent with a label, icon, or shape.
657
+ - Uppercase text by typing capitals; let the mono utilities transform it.
658
+ - Hardcode light/dark colors when a semantic token resolves correctly in both.
659
+
660
+ ---
661
+
662
+ *Parts of this guidance build on prior art: [Emil Kowalski](https://emilkowal.ski/) on web
663
+ animation and design engineering ([animations.dev](https://animations.dev)), and Vercel's
664
+ [Web Interface Guidelines](https://vercel.com/design/guidelines).*