@oxide/design-system 6.5.4-canary.fb43c60 → 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 +664 -0
- package/package.json +5 -2
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).*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxide/design-system",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.6.0",
|
|
4
4
|
"description": "Home of reusable design assets and token for oxide internal sites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"color-gen:watch": "tsx watch color-gen/generate.ts",
|
|
25
25
|
"color-gen:dev": "vite --config color-gen/vite.config.ts color-gen",
|
|
26
26
|
"color-gen:apply": "tsx color-gen/apply-colors.ts",
|
|
27
|
-
"preview:dev": "vite --config preview/vite.config.ts"
|
|
27
|
+
"preview:dev": "vite --config preview/vite.config.ts",
|
|
28
|
+
"design-md": "node scripts/design-md.mjs",
|
|
29
|
+
"design-md:check": "node scripts/design-md.mjs --check"
|
|
28
30
|
},
|
|
29
31
|
"author": "Oxide Computer Company <bots@oxidecomputer.com>",
|
|
30
32
|
"license": "MPL 2.0",
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
"shiki": "^4.4.3"
|
|
78
80
|
},
|
|
79
81
|
"files": [
|
|
82
|
+
"design.md",
|
|
80
83
|
"styles/*.css",
|
|
81
84
|
"icons/**/*.svg",
|
|
82
85
|
"dist/**"
|