@n8mills/design-tokens 1.0.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 +429 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/components.css +943 -0
- package/design-system.md +466 -0
- package/package.json +58 -0
- package/tokens.css +498 -0
- package/tokens.d.ts +266 -0
- package/tokens.js +265 -0
- package/tokens.json +441 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
---
|
|
2
|
+
# The value blocks below (colors, typography, rounded, spacing, elevation) are GENERATED from
|
|
3
|
+
# tokens.json by tokens/designmd.mjs and drift-gated on deploy. Do not hand-edit between the
|
|
4
|
+
# `# DESIGN:*:start` / `# DESIGN:*:end` markers; edit tokens.json and regenerate. The prose, the
|
|
5
|
+
# component recipe map, and everything outside the markers is hand-authored.
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
name: Nate Mills Portfolio
|
|
8
|
+
description: >
|
|
9
|
+
A design-systems consultant's portfolio where the system IS the pitch. The voltage is a single
|
|
10
|
+
high-chroma lime held as a scarce accent against warm off-white and warm near-black, with honest
|
|
11
|
+
hairline borders instead of decorative shadows or gradients. It counter-positions against the
|
|
12
|
+
typical portfolio that scatters colour and leans on drop shadows: here depth is carried by a
|
|
13
|
+
surface ladder and 1px hairlines, type hierarchy comes from weight not opacity, and every value
|
|
14
|
+
traces to a token. Both a light theme (off-white canvas, ink accent) and a dark theme (near-black
|
|
15
|
+
canvas, lime accent) are first-class, and the whole thing is authored to WCAG 2.2 AA in both.
|
|
16
|
+
|
|
17
|
+
colors:
|
|
18
|
+
# DESIGN:colors:start
|
|
19
|
+
# Brand identity (fixed, never theme-flips)
|
|
20
|
+
brand-lime: "#d2ff37" # --brand-lime
|
|
21
|
+
brand-lime-dim: "#b8e030" # --brand-lime-dim
|
|
22
|
+
brand-lime-vivid: "#eeff00" # --brand-lime-vivid
|
|
23
|
+
brand-ink: "#1c1c1f" # --brand-ink
|
|
24
|
+
brand-gradient: "linear-gradient(135deg, #d2ff37 0%, #eeff00 100%)" # --brand-gradient
|
|
25
|
+
|
|
26
|
+
# Light theme semantics (the default page)
|
|
27
|
+
bg: "#f5f5f5" # --color-bg
|
|
28
|
+
surface: "#ffffff" # --color-surface
|
|
29
|
+
surface-sunken: "#ececec" # --color-surface-sunken
|
|
30
|
+
surface-inverse: "#0a0a0b" # --color-surface-inverse (always-dark, both themes)
|
|
31
|
+
text-primary: "#0a0a0b" # --color-text-primary
|
|
32
|
+
text-secondary: "#3f3f46" # --color-text-secondary
|
|
33
|
+
text-tertiary: "#67676f" # --color-text-tertiary
|
|
34
|
+
border: "#e4e4e7" # --color-border
|
|
35
|
+
border-strong: "#d4d4d8" # --color-border-strong
|
|
36
|
+
border-brand: "#d2ff37" # --color-border-brand (lime, fixed both themes)
|
|
37
|
+
accent: "#1c1c1f" # --color-accent (theme-aware: ink light, lime dark)
|
|
38
|
+
accent-hover: "#0a0a0b" # --color-accent-hover
|
|
39
|
+
on-accent: "#ffffff" # --color-on-accent
|
|
40
|
+
focus-ring: "#1c1c1f" # --color-focus-ring (theme-aware)
|
|
41
|
+
link: "#1c1c1f" # --color-link (theme-aware)
|
|
42
|
+
link-hover: "#0a0a0b" # --color-link-hover
|
|
43
|
+
|
|
44
|
+
# Text on always-dark slabs (theme-invariant)
|
|
45
|
+
on-ink-primary: "rgba(255, 255, 255, 0.88)" # --color-on-ink-primary
|
|
46
|
+
on-ink-secondary: "rgba(255, 255, 255, 0.66)" # --color-on-ink-secondary
|
|
47
|
+
on-ink-muted: "rgba(255, 255, 255, 0.4)" # --color-on-ink-muted
|
|
48
|
+
on-ink-border: "rgba(255, 255, 255, 0.08)" # --color-on-ink-border
|
|
49
|
+
|
|
50
|
+
# Dark theme semantics (data-theme="dark" or OS dark)
|
|
51
|
+
dark-bg: "#0a0a0b" # --darkTheme-color-bg
|
|
52
|
+
dark-surface: "#1c1c1f" # --darkTheme-color-surface
|
|
53
|
+
dark-surface-sunken: "#141416" # --darkTheme-color-surface-sunken
|
|
54
|
+
dark-text-primary: "#f4f4f5" # --darkTheme-color-text-primary
|
|
55
|
+
dark-text-secondary: "#a1a1aa" # --darkTheme-color-text-secondary
|
|
56
|
+
dark-text-tertiary: "#909096" # --darkTheme-color-text-tertiary
|
|
57
|
+
dark-border: "#27272a" # --darkTheme-color-border
|
|
58
|
+
dark-accent: "#d2ff37" # --darkTheme-color-accent (brand lime, deliberate)
|
|
59
|
+
dark-accent-hover: "#b8e030" # --darkTheme-color-accent-hover
|
|
60
|
+
dark-focus-ring: "#d2ff37" # --darkTheme-color-focus-ring
|
|
61
|
+
dark-link: "#d2ff37" # --darkTheme-color-link
|
|
62
|
+
|
|
63
|
+
# Scrims
|
|
64
|
+
overlay-soft: "rgba(0, 0, 0, 0.35)" # --color-overlay-soft
|
|
65
|
+
overlay-base: "rgba(0, 0, 0, 0.62)" # --color-overlay-base
|
|
66
|
+
overlay-strong: "rgba(0, 0, 0, 0.88)" # --color-overlay-strong
|
|
67
|
+
# DESIGN:colors:end
|
|
68
|
+
|
|
69
|
+
typography:
|
|
70
|
+
families:
|
|
71
|
+
# DESIGN:type-families:start
|
|
72
|
+
display: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' # --font-display
|
|
73
|
+
body: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' # --font-body
|
|
74
|
+
mono: '"JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace' # --font-mono
|
|
75
|
+
serif: '"PT Serif", Georgia, serif' # --font-serif
|
|
76
|
+
# DESIGN:type-families:end
|
|
77
|
+
# DESIGN:type-styles:start
|
|
78
|
+
hero: # hero headline only
|
|
79
|
+
family: display
|
|
80
|
+
size: "clamp(40px, 9vw, 118px)" # --display-hero
|
|
81
|
+
weight: 600 # --display-weight
|
|
82
|
+
lineHeight: 1.05 # --line-display
|
|
83
|
+
tracking: "-0.04em" # --display-tight
|
|
84
|
+
h1: # every section heading
|
|
85
|
+
family: display
|
|
86
|
+
size: "clamp(34px, 5.8vw, 72px)" # --text-h1
|
|
87
|
+
weight: 600 # --display-weight
|
|
88
|
+
lineHeight: 1.05 # --line-display
|
|
89
|
+
tracking: "-0.04em" # --display-tight
|
|
90
|
+
h2: # card titles, sub-heads
|
|
91
|
+
family: display
|
|
92
|
+
size: "clamp(22px, 4vw, 32px)" # --text-h2
|
|
93
|
+
weight: 600 # --display-weight
|
|
94
|
+
lineHeight: 1.15 # --line-heading
|
|
95
|
+
tracking: "-0.02em" # --tracking-tight
|
|
96
|
+
card-title: # large feature-card titles
|
|
97
|
+
family: display
|
|
98
|
+
size: "clamp(22px, 4vw, 44px)" # --display-card-title
|
|
99
|
+
weight: 600 # --display-weight
|
|
100
|
+
lineHeight: 1.15 # --line-heading
|
|
101
|
+
tracking: "-0.04em" # --display-tight
|
|
102
|
+
stat-num: # key-metrics numerals
|
|
103
|
+
family: display
|
|
104
|
+
size: "clamp(40px, 7vw, 88px)" # --display-stat-num
|
|
105
|
+
weight: 600 # --display-weight
|
|
106
|
+
lineHeight: 1.05 # --line-display
|
|
107
|
+
tracking: "-0.04em" # --display-tight
|
|
108
|
+
intro: # section intro line
|
|
109
|
+
family: body
|
|
110
|
+
size: "clamp(20px, 5vw, 24px)" # --display-intro
|
|
111
|
+
weight: 400 # --weight-regular
|
|
112
|
+
lineHeight: 1.45 # --line-snug
|
|
113
|
+
tracking: "-0.02em" # --tracking-tight
|
|
114
|
+
body: # paragraphs
|
|
115
|
+
family: body
|
|
116
|
+
size: "16px" # --text-body
|
|
117
|
+
weight: 400 # --weight-regular
|
|
118
|
+
lineHeight: 1.5 # --line-body
|
|
119
|
+
tracking: 0 # --tracking-normal
|
|
120
|
+
body-compact: # footer, card bullets, dense UI
|
|
121
|
+
family: body
|
|
122
|
+
size: "14px" # --size-sm
|
|
123
|
+
weight: 400 # --weight-regular
|
|
124
|
+
lineHeight: 1.5 # --line-body
|
|
125
|
+
caption: # captions, footnotes
|
|
126
|
+
family: body
|
|
127
|
+
size: "12px" # --text-caption
|
|
128
|
+
weight: 400 # --weight-regular
|
|
129
|
+
lineHeight: 1.5 # --line-body
|
|
130
|
+
label: # uppercase eyebrows, chips, badges; line-height 1.4
|
|
131
|
+
family: mono
|
|
132
|
+
size: "11px" # --text-label
|
|
133
|
+
weight: 500 # --weight-medium
|
|
134
|
+
tracking: "0.08em" # --tracking-label
|
|
135
|
+
# DESIGN:type-styles:end
|
|
136
|
+
|
|
137
|
+
rounded:
|
|
138
|
+
# DESIGN:rounded:start
|
|
139
|
+
none: 0 # --radius-none (square)
|
|
140
|
+
sm: "8px" # --radius-sm (inputs, small controls)
|
|
141
|
+
md: "10px" # --radius-md (mid-size controls, insets)
|
|
142
|
+
lg: "14px" # --radius-lg (DEFAULT for cards and modals)
|
|
143
|
+
xl: "20px" # --radius-xl (prominent panels)
|
|
144
|
+
full: "999px" # --radius-full (pills, chips, badges, buttons, icon buttons)
|
|
145
|
+
# DESIGN:rounded:end
|
|
146
|
+
|
|
147
|
+
spacing:
|
|
148
|
+
# DESIGN:spacing:start
|
|
149
|
+
s1: "4px" # --space-1
|
|
150
|
+
s2: "8px" # --space-2
|
|
151
|
+
s3: "12px" # --space-3
|
|
152
|
+
s4: "16px" # --space-4
|
|
153
|
+
s5: "24px" # --space-5
|
|
154
|
+
s6: "32px" # --space-6
|
|
155
|
+
s7: "48px" # --space-7
|
|
156
|
+
s8: "64px" # --space-8
|
|
157
|
+
s9: "96px" # --space-9 (section padding lower bound)
|
|
158
|
+
s10: "128px" # --space-10 (section padding upper bound)
|
|
159
|
+
s11: "192px" # --space-11
|
|
160
|
+
s12: "256px" # --space-12
|
|
161
|
+
section-padding: "clamp(96px, 12vw, 128px)" # --section-padding
|
|
162
|
+
card-pad-compact: "16px" # --card-pad-compact
|
|
163
|
+
card-pad-standard: "24px" # --card-pad-standard
|
|
164
|
+
card-pad-spacious: "32px" # --card-pad-spacious
|
|
165
|
+
# DESIGN:spacing:end
|
|
166
|
+
|
|
167
|
+
elevation:
|
|
168
|
+
# DESIGN:elevation:start
|
|
169
|
+
none: "none" # --shadow-none (default; borders carry depth)
|
|
170
|
+
soft: "0 1px 2px rgb(28 28 31 / 0.04), 0 1px 3px rgb(28 28 31 / 0.06)" # --shadow-soft (rare resting lift)
|
|
171
|
+
lift: "0 4px 12px rgb(28 28 31 / 0.06), 0 2px 4px rgb(28 28 31 / 0.04)" # --shadow-lift (overlays / float-on-scroll only)
|
|
172
|
+
modal: "0 24px 64px rgba(0, 0, 0, 0.5), 0 8px 16px rgba(0, 0, 0, 0.18)" # --shadow-modal (lightboxes above a scrim)
|
|
173
|
+
# DESIGN:elevation:end
|
|
174
|
+
focus: "0 0 0 2px {colors.focus-ring}" # 2px keyboard-focus outline, offset 2px
|
|
175
|
+
|
|
176
|
+
# Component recipes reference {token} names that resolve against the blocks above, so a value change
|
|
177
|
+
# propagates automatically and no raw value can drift here. Each variant is its own entry.
|
|
178
|
+
components:
|
|
179
|
+
nav:
|
|
180
|
+
background: "transparent (frosted glass pill once scrolled)"
|
|
181
|
+
textColor: "{colors.text-tertiary}"
|
|
182
|
+
typography: "{typography.label}"
|
|
183
|
+
activeTextColor: "{colors.text-primary}"
|
|
184
|
+
activeUnderline: "1px, {colors.accent} (ink on light, lime on dark)"
|
|
185
|
+
note: "State reads from underline shape PLUS colour, never colour alone."
|
|
186
|
+
hero-headline:
|
|
187
|
+
typography: "{typography.hero}"
|
|
188
|
+
textColor: "{colors.text-primary}"
|
|
189
|
+
emphasisWord: "one word may be italic and a muted colour; never the whole heading"
|
|
190
|
+
button-primary:
|
|
191
|
+
backgroundColor: "{colors.brand-lime}"
|
|
192
|
+
textColor: "{colors.brand-ink}"
|
|
193
|
+
border: "1px solid {colors.border-brand}"
|
|
194
|
+
typography: "{typography.label} at 14px, weight 600"
|
|
195
|
+
rounded: "{rounded.full}"
|
|
196
|
+
padding: "10px 18px"
|
|
197
|
+
height: "44px (md); 32px (sm); 52px (lg)"
|
|
198
|
+
button-primary-hover:
|
|
199
|
+
backgroundColor: "{colors.brand-ink}"
|
|
200
|
+
textColor: "{colors.brand-lime}"
|
|
201
|
+
button-secondary:
|
|
202
|
+
backgroundColor: "transparent"
|
|
203
|
+
textColor: "{colors.text-primary}"
|
|
204
|
+
border: "1px solid {colors.text-tertiary}"
|
|
205
|
+
rounded: "{rounded.full}"
|
|
206
|
+
padding: "10px 18px"
|
|
207
|
+
button-secondary-hover:
|
|
208
|
+
backgroundColor: "{colors.surface-sunken}"
|
|
209
|
+
border: "1px solid {colors.text-primary}"
|
|
210
|
+
button-icon:
|
|
211
|
+
shape: "circle, {rounded.full}"
|
|
212
|
+
border: "1px solid {colors.text-tertiary}"
|
|
213
|
+
iconColor: "{colors.text-secondary}"
|
|
214
|
+
size: "44px, glyph 16px"
|
|
215
|
+
note: "The one canonical icon button. An icon-only button is never a pill with a label."
|
|
216
|
+
card:
|
|
217
|
+
backgroundColor: "{colors.surface}"
|
|
218
|
+
textColor: "{colors.text-primary}"
|
|
219
|
+
border: "1px solid {colors.border}"
|
|
220
|
+
rounded: "{rounded.lg}"
|
|
221
|
+
padding: "{spacing.card-pad-standard}"
|
|
222
|
+
hover: "outline and slight lift, NOT a fill"
|
|
223
|
+
card-dark:
|
|
224
|
+
backgroundColor: "{colors.brand-ink}"
|
|
225
|
+
textColor: "{colors.on-ink-primary}"
|
|
226
|
+
border: "1px solid {colors.on-ink-border}"
|
|
227
|
+
carousel-nav:
|
|
228
|
+
layout: "bare chevron buttons and a counter on a rail below the cards"
|
|
229
|
+
dotActive: "{colors.accent}"
|
|
230
|
+
dotInactive: "{colors.text-tertiary} (tuned to clear 3:1)"
|
|
231
|
+
dotInactiveOnInverse: "{colors.on-ink-muted}"
|
|
232
|
+
counter: "{typography.label} mono"
|
|
233
|
+
badge:
|
|
234
|
+
backgroundColor: "transparent"
|
|
235
|
+
textColor: "{colors.text-secondary}"
|
|
236
|
+
border: "1px solid {colors.border}"
|
|
237
|
+
typography: "{typography.label} mono, weight 500"
|
|
238
|
+
rounded: "{rounded.full}"
|
|
239
|
+
padding: "4px 12px"
|
|
240
|
+
dot: "{colors.brand-lime} (6px, stays lime in both themes; the word carries meaning)"
|
|
241
|
+
chip:
|
|
242
|
+
backgroundColor: "{colors.surface-sunken}"
|
|
243
|
+
textColor: "{colors.text-primary}"
|
|
244
|
+
border: "1px solid {colors.border}"
|
|
245
|
+
typography: "{typography.label} mono, weight 500"
|
|
246
|
+
rounded: "{rounded.full}"
|
|
247
|
+
padding: "4px 10px"
|
|
248
|
+
footer:
|
|
249
|
+
backgroundColor: "{colors.brand-lime}"
|
|
250
|
+
textColor: "{colors.brand-ink}"
|
|
251
|
+
note: "A fixed lime slab, lime in BOTH themes; one of the few large lime fills allowed."
|
|
252
|
+
link:
|
|
253
|
+
textColor: "{colors.link}"
|
|
254
|
+
decoration: "underline"
|
|
255
|
+
hover: "{colors.link-hover}"
|
|
256
|
+
external: "opens a new tab, rel set, trailing up-right arrow glyph plus a screen-reader cue"
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
# Nate Mills Portfolio: Brand Contract
|
|
260
|
+
|
|
261
|
+
This is a portable brand contract. Hand it to any AI coding agent, or a contractor, and it has
|
|
262
|
+
everything needed to build a page that feels like natemills.me: no repo, no build tools, just this
|
|
263
|
+
file. Every colour, size, radius, and spacing value in the frontmatter above is resolved to a literal
|
|
264
|
+
and generated straight from the design token source, so it cannot drift from the real system.
|
|
265
|
+
|
|
266
|
+
## Overview
|
|
267
|
+
|
|
268
|
+
This is the portfolio of a Senior Product Designer (Design Systems). The site is the argument: a
|
|
269
|
+
calm, Swiss-minimal editorial surface where a single high-chroma lime does all the talking and
|
|
270
|
+
everything else stays quiet. The atmosphere is warm off-white paper in light mode and warm near-black
|
|
271
|
+
in dark mode, never pure white and never pure black. Depth is built from a surface ladder and honest
|
|
272
|
+
1px hairlines, not from drop shadows or decorative gradients. Type is Inter, set large and tight for
|
|
273
|
+
display, with JetBrains Mono for eyebrows and labels and PT Serif reserved for editorial quotes only.
|
|
274
|
+
|
|
275
|
+
The counter-position is deliberate: most portfolios scatter accent colour across whole sections and
|
|
276
|
+
lean on soft shadows to fake hierarchy. This one refuses both. Colour is scarce, borders are honest,
|
|
277
|
+
and hierarchy comes from weight and scale. If you are building a new page, the system is the pitch:
|
|
278
|
+
make it look composed, restrained, and inevitable.
|
|
279
|
+
|
|
280
|
+
### Key characteristics
|
|
281
|
+
|
|
282
|
+
- Warm off-white canvas (`{colors.bg}`), not white. Ink is warm near-black, not pure black.
|
|
283
|
+
- One accent, one lime (`{colors.brand-lime}`), used scarcely. Never a section fill or body text.
|
|
284
|
+
- Borders over shadows. Shadows are rare and reserved for overlays.
|
|
285
|
+
- Display type is Inter at weight 600, tight tracking. Never italic as a whole heading.
|
|
286
|
+
- Mono (JetBrains Mono) is for uppercase eyebrows, chips, and badges only.
|
|
287
|
+
- Both themes are first-class. In dark mode the accent IS the lime, by design.
|
|
288
|
+
- WCAG 2.2 AA in both themes is a floor, not a feature.
|
|
289
|
+
|
|
290
|
+
## Color roles
|
|
291
|
+
|
|
292
|
+
The palette is exactly two families: a warm neutral ramp and one lime. There is no red, amber, or
|
|
293
|
+
blue in the system (external brand colours like a client blue are locked constants used only inside
|
|
294
|
+
their own case-study or logo context, never as system colour).
|
|
295
|
+
|
|
296
|
+
**Brand voltage rule: use the lime (`{colors.brand-lime}`) ONLY for** the primary button fill, brand
|
|
297
|
+
outlines (`{colors.border-brand}`), focus rings on dark surfaces, the lime period after a section
|
|
298
|
+
title, the active carousel dot on dark, status dots, and the footer slab. **Never** use lime as body
|
|
299
|
+
text, as a link on a light background, as a section background, or as a large fill on the light page.
|
|
300
|
+
On the off-white canvas raw lime composites to roughly 1.06:1 and fails WCAG outright.
|
|
301
|
+
|
|
302
|
+
**The theme-aware accent.** `{colors.accent}` is the one interactive role that follows the theme: it
|
|
303
|
+
resolves to ink on light and to lime on dark. Anything that signals an active or selected state
|
|
304
|
+
(current nav item, pressed toggle, active dot) uses `accent`, never raw lime, so it stays legible on
|
|
305
|
+
the light page. `{colors.border-brand}` is the deliberate exception: it stays fixed lime in both
|
|
306
|
+
themes because it is a brand-identity outline, not a state signal.
|
|
307
|
+
|
|
308
|
+
**Dark-mode accent is the lime, deliberately.** On the dark theme, `accent`, `link`, and `focus-ring`
|
|
309
|
+
all resolve to brand lime. This is a locked brand ruling, not a contrast bug: lime on near-black reads
|
|
310
|
+
at roughly 10:1 (AAA). Do not "fix" it to a near-white. What is forbidden in dark mode is a solid lime
|
|
311
|
+
panel; the calm dark treatment is charcoal with lime text, not lime wallpaper.
|
|
312
|
+
|
|
313
|
+
**Text on dark slabs.** Some surfaces (testimonials, contact) stay near-black in both themes. Text on
|
|
314
|
+
them uses the `on-ink-*` family (88% / 66% / 40% white), never the theme-aware text tokens. Do not use
|
|
315
|
+
an `on-ink` colour on a light surface; it is white-on-dark only and will vanish on a white card.
|
|
316
|
+
|
|
317
|
+
## Typography rules
|
|
318
|
+
|
|
319
|
+
- Weight and scale carry hierarchy, never opacity. Do not dim body text with alpha to make it read
|
|
320
|
+
secondary; step down a text style instead.
|
|
321
|
+
- Display is always roman. A full italic heading is banned. Italic is allowed on a single word or short
|
|
322
|
+
phrase inside a heading, and only when paired with a muted colour to mark it as secondary emphasis.
|
|
323
|
+
- Eyebrows and labels are uppercase JetBrains Mono with positive tracking (0.08em labels, 0.18em
|
|
324
|
+
eyebrows) to mark them as taxonomy.
|
|
325
|
+
- Body copy is `text-align: start`, ragged right. Never justify.
|
|
326
|
+
- Line length caps at 52ch. The 11px mono label is the floor; never go smaller.
|
|
327
|
+
|
|
328
|
+
## Component stylings
|
|
329
|
+
|
|
330
|
+
- **Buttons** are full pills, Inter semibold at 14px, padding 10px/18px, default height 44px. The
|
|
331
|
+
primary button is a lime fill with ink label; on hover it inverts to an ink fill with a lime label.
|
|
332
|
+
The secondary button is transparent with a `{colors.text-tertiary}` outline (that outline colour is
|
|
333
|
+
chosen to clear the 3:1 non-text contrast floor).
|
|
334
|
+
- **Icon button** is the one canonical circle: a `{colors.text-tertiary}` hairline ring,
|
|
335
|
+
`{colors.text-secondary}` glyph, 44px, `{rounded.full}`. Never a pill with a label.
|
|
336
|
+
- **Cards** sit on `{colors.surface}` with a 1px `{colors.border}` hairline and `{rounded.lg}`, padded
|
|
337
|
+
24px (compact 16, spacious 32). Interaction is outline-plus-lift, not a background fill. Cards on a
|
|
338
|
+
dark slab use the `card-dark` recipe.
|
|
339
|
+
- **Chips vs badges vs counters** resolve by job: a label is a chip, a status is a badge, a count is
|
|
340
|
+
the carousel counter. A badge is read-only and never interactive; its dot stays lime in both themes
|
|
341
|
+
but colour is never the only signal, every status also carries a word.
|
|
342
|
+
- **Carousel nav** is bare chevrons plus a mono counter on a rail below the cards. The active dot
|
|
343
|
+
follows `{colors.accent}`; the inactive dot is a solid tuned grey (not a translucent one, which
|
|
344
|
+
would fail 3:1).
|
|
345
|
+
- **Footer** is a fixed lime slab with ink text, lime in both themes.
|
|
346
|
+
- **Links** are underlined, `{colors.link}` (ink on light, lime on dark). External links open a new
|
|
347
|
+
tab, set `rel`, and carry a trailing up-right arrow glyph plus a screen-reader cue.
|
|
348
|
+
|
|
349
|
+
## Layout principles
|
|
350
|
+
|
|
351
|
+
- Content sits in a wide band that stays proportional on normal screens and caps at 1680px so
|
|
352
|
+
ultrawide layouts do not stretch without earning it.
|
|
353
|
+
- Vertical rhythm is `{spacing.section-padding}` between sections.
|
|
354
|
+
- Spacing is a strict 4px scale. Never use raw px for spacing; reach for a scale step.
|
|
355
|
+
- Full-bleed inverse bands (testimonials, contact) break out to the screen edge and stay near-black in
|
|
356
|
+
both themes.
|
|
357
|
+
- Stacking uses a named z-index scale with 100-unit gaps. Never hand-write a raw page-level z-index.
|
|
358
|
+
|
|
359
|
+
## Depth and elevation
|
|
360
|
+
|
|
361
|
+
Depth is carried by the surface ladder plus hairline borders, not by shadows. The ladder, light to
|
|
362
|
+
recessed: `{colors.bg}` (page) to `{colors.surface}` (card, a genuine lift) to
|
|
363
|
+
`{colors.surface-sunken}` (wells and insets). Dark mode runs the same three steps in near-blacks. The
|
|
364
|
+
generated `elevation` values above are the light-theme shadows; dark mode swaps `soft` and `lift` to
|
|
365
|
+
deeper black-alpha. Radii: cards and modals use `{rounded.lg}`; small controls use `{rounded.sm}`;
|
|
366
|
+
pills, chips, badges, and buttons use `{rounded.full}`.
|
|
367
|
+
|
|
368
|
+
## Do's and Don'ts
|
|
369
|
+
|
|
370
|
+
Do:
|
|
371
|
+
- Do keep the lime scarce. If two primary buttons would appear in one viewport, make one secondary.
|
|
372
|
+
- Do build hierarchy from weight and scale.
|
|
373
|
+
- Do use `{colors.accent}` for any active or selected state so it stays legible on light.
|
|
374
|
+
- Do give every status a word, not just a coloured dot.
|
|
375
|
+
- Do design the static, reduced-motion frame first; motion is garnish that never carries meaning.
|
|
376
|
+
|
|
377
|
+
Don't:
|
|
378
|
+
- Don't use pure white as the page canvas or pure black as ink. The canvas is `{colors.bg}`.
|
|
379
|
+
- Don't put lime on a light background as text, a link, or a large fill (it fails WCAG at ~1.06:1).
|
|
380
|
+
- Don't paint a solid lime panel in dark mode; use charcoal with lime text.
|
|
381
|
+
- Don't dim text with opacity to fake a secondary tone; step down a text style.
|
|
382
|
+
- Don't italicise a whole heading, and don't use an alpha colour for heading emphasis.
|
|
383
|
+
- Don't reach for a drop shadow to separate a card; use a hairline border.
|
|
384
|
+
- Don't justify body text, and don't drop below the 11px mono label floor.
|
|
385
|
+
- Don't leak an external client brand colour into general UI; those are locked constants for their
|
|
386
|
+
own case-study context only.
|
|
387
|
+
|
|
388
|
+
## Responsive behavior
|
|
389
|
+
|
|
390
|
+
Reference breakpoints: mobile 375px, tablet 768px, desktop 1024px, wide 1440px, ultrawide 1600px.
|
|
391
|
+
|
|
392
|
+
- Type is fluid via `clamp()` and capped, so headings grow on wider screens and stop; they never balloon.
|
|
393
|
+
- At and below 768px, section-level primary CTAs go full-width and centre for a thumb-friendly tap
|
|
394
|
+
target; inline buttons inside paragraphs stay auto-width.
|
|
395
|
+
- Dense value tables tighten below ~640px but never drop a column and never shrink labels below 11px.
|
|
396
|
+
- Touch targets meet 44px; small dots keep a 24px hit area.
|
|
397
|
+
- Verify layout at ~375px alongside both light and dark themes whenever you touch a layout.
|
|
398
|
+
|
|
399
|
+
## Agent prompt guide
|
|
400
|
+
|
|
401
|
+
When building a new page or prototype with this system:
|
|
402
|
+
|
|
403
|
+
1. Work one component at a time and reference tokens by name (`{colors.accent}`, `{typography.h1}`,
|
|
404
|
+
`{rounded.lg}`), never by inventing a hex or px value.
|
|
405
|
+
2. Decide first which surface a section sits on (`bg`, `surface`, `surface-sunken`, or an always-dark
|
|
406
|
+
`surface-inverse`), then pick text colours to match: `text-*` on light, `on-ink-*` on dark slabs.
|
|
407
|
+
3. Keep the lime scarce. It appears on the primary button, brand outlines, focus rings on dark, status
|
|
408
|
+
dots, and the footer. Nowhere else.
|
|
409
|
+
4. For any active or selected state use `{colors.accent}` (theme-aware), not raw lime, so it survives
|
|
410
|
+
the light theme.
|
|
411
|
+
5. Build hierarchy from weight (600 display, 400 body) and scale, not opacity.
|
|
412
|
+
6. Use hairline borders for separation; reach for a shadow only on an overlay or lightbox.
|
|
413
|
+
7. Support both themes: the dark accent is intentionally the lime, and the dark treatment for a dark
|
|
414
|
+
surface is charcoal with lime text, never lime wallpaper.
|
|
415
|
+
8. Hold WCAG 2.2 AA in both themes: 4.5:1 for body text, 3:1 for non-text and large text, visible
|
|
416
|
+
focus, 44px targets.
|
|
417
|
+
|
|
418
|
+
## Known gaps
|
|
419
|
+
|
|
420
|
+
- This contract carries resolved values for portability; the source of truth authors every colour in
|
|
421
|
+
OKLCH as well, used automatically where the browser supports it. The values here are exact
|
|
422
|
+
round-trips of those OKLCH colours.
|
|
423
|
+
- The system has no semantic status colours (success/warning/danger); a portfolio has no such UI. If a
|
|
424
|
+
consumer needs them, add at the token layer first, then route a semantic.
|
|
425
|
+
- Fonts are Inter, JetBrains Mono, and PT Serif. Open-source substitutes if unavailable: Inter is
|
|
426
|
+
itself open source; fall back to a neutral grotesque for display and body, any monospace with a
|
|
427
|
+
slashed zero for labels, and a transitional serif (Georgia) for quotes.
|
|
428
|
+
- Exact computed contrast ratios are verified in a live specimen; the floors above (4.5:1 text, 3:1
|
|
429
|
+
non-text) are the targets to design to.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nate Mills
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://raw.githubusercontent.com/n8mills-UI/nate-mills-design-system/main/assets/header-banner.png" alt="Nate Mills Design System. Authored once as DTCG tokens, generated to CSS, audited to WCAG 2.2 AA." width="860">
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@n8mills/design-tokens)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://www.designtokens.org/)
|
|
8
|
+
[](https://www.w3.org/WAI/WCAG22/quickref/)
|
|
9
|
+
[](https://styledictionary.com)
|
|
10
|
+
|
|
11
|
+
[](https://nate-mills-portfolio.netlify.app/#uth-colour)
|
|
12
|
+
[](https://natemills.me)
|
|
13
|
+
[](https://bupa.natemills.me)
|
|
14
|
+
[](https://www.linkedin.com/in/millsdesign)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
This is the real design system behind my portfolio, opened up so you can see how it is built, not just that it exists. It is small on purpose: one accent, a tuned neutral scale, two fonts, and a strict two-tier token model. Everything starts in one `tokens.json` file and propagates outward, so reskinning the brand is a one-line change and every component follows.
|
|
19
|
+
|
|
20
|
+
I publish it because the discipline is the point. A design system is easy to claim and hard to govern. This repo is the audit trail for the claims: the token source, the generated CSS, the component layer, and the written rules that keep them honest.
|
|
21
|
+
|
|
22
|
+
> **See it live.** The [under-the-hood design system](https://nate-mills-portfolio.netlify.app/#uth-colour) resolves every token in real time, in light and dark, section by section. This repo is the source it runs on.
|
|
23
|
+
|
|
24
|
+
## What's inside
|
|
25
|
+
|
|
26
|
+
One source, three generated outputs, and the discipline between them. That is the whole system.
|
|
27
|
+
|
|
28
|
+
- **`tokens.json`** : the single source of truth, in the [W3C Design Tokens Community Group](https://www.designtokens.org/) (DTCG) format. Two tiers: primitives (raw values) and semantics (roles).
|
|
29
|
+
- **`tokens.css`** : the generated CSS custom properties, built from `tokens.json` by [Style Dictionary](https://styledictionary.com). All three themes (light, dark, high-contrast) included. Never hand-edited.
|
|
30
|
+
- **`tokens.js` / `tokens.d.ts`** : the same tokens as a typed JavaScript object, resolved to literal values for the default theme. For build tools that read tokens in JS, not CSS. Generated, never hand-edited.
|
|
31
|
+
- **`components.css`** : the component layer. Buttons, cards, chips, and the section heading pattern, each reading semantic tokens only.
|
|
32
|
+
- **[`design-system.md`](./design-system.md)** : the full write-up. Philosophy, the token model, the type and spacing scales, accessibility, contribution rules, and the content voice guide.
|
|
33
|
+
- **[`DESIGN.md`](./DESIGN.md)** : the portable brand contract. One self-contained file that hands an AI coding agent (or a contractor) everything it needs to build on-brand without the repo: resolved colour, type, spacing and radius values in the frontmatter, a component recipe map, and the do's and don'ts. The value blocks are generated from `tokens.json` and drift-gated, so they never fall out of sync with the system.
|
|
34
|
+
|
|
35
|
+
Prefer the guided tour to the raw source? The [live view](https://nate-mills-portfolio.netlify.app/#uth-colour) walks every section with the real values resolved.
|
|
36
|
+
|
|
37
|
+
## The model in one breath
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
PRIMITIVES raw values, named by hue and stop --> e.g. --neutral-850: #1c1c1f
|
|
41
|
+
SEMANTICS roles, reference primitives via var() --> e.g. --color-accent: var(--neutral-850)
|
|
42
|
+
COMPONENTS classes that read semantics only --> e.g. .btn--primary { background: var(--brand-lime) }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
One way, top to bottom: components read semantics, semantics read primitives, primitives are literals. Nothing reaches back up. So one edit at the top, a new brand hue, propagates through every semantic and component that uses it. No find-and-replace, no drift. That propagation is the whole point.
|
|
46
|
+
|
|
47
|
+
## The foundations at a glance
|
|
48
|
+
|
|
49
|
+
<div align="center">
|
|
50
|
+
<picture>
|
|
51
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/n8mills-UI/nate-mills-design-system/main/assets/bento-foundations-dark.png">
|
|
52
|
+
<img src="https://raw.githubusercontent.com/n8mills-UI/nate-mills-design-system/main/assets/bento-foundations-lime.png" alt="The design system foundations in one view: the lime and ink colour ramps, the Inter display face, the type ladder, the motion curve, and the 12-step spacing scale" width="860">
|
|
53
|
+
</picture>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
Colour, type, motion, and spacing, all generated from `tokens.json`. The type is Inter for everything, JetBrains Mono for labels and numbers, and PT Serif for the one place that earns it: the pull quotes.
|
|
57
|
+
|
|
58
|
+
## Accessibility is a constraint, not a feature
|
|
59
|
+
|
|
60
|
+
It is wired into the tokens, so you cannot opt out of it by accident:
|
|
61
|
+
|
|
62
|
+
- Every text token is contrast-checked for WCAG 2.2 AA: a blocking CI lint over the token source in both themes, plus live ratio computation on the Under-the-Hood page.
|
|
63
|
+
- Focus rings are never removed without a replacement.
|
|
64
|
+
- Touch targets clear 44 by 44 px.
|
|
65
|
+
- Every animation is gated behind `prefers-reduced-motion`, collapsed in one place, not per component.
|
|
66
|
+
|
|
67
|
+
There is one honest carveout. The brand lime is a background and accent only. It fails as foreground text on the light surface, roughly 1.06 to 1, so the system refuses to let you use it there and hands you a theme-aware token instead. The full reasoning is in [`design-system.md`](./design-system.md#10-accessibility).
|
|
68
|
+
|
|
69
|
+
## Use it
|
|
70
|
+
|
|
71
|
+
Install from npm:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install @n8mills/design-tokens
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Pull in the CSS custom properties (light, dark, and high-contrast themes all included), through your bundler:
|
|
78
|
+
|
|
79
|
+
```css
|
|
80
|
+
@import "@n8mills/design-tokens/css";
|
|
81
|
+
@import "@n8mills/design-tokens/components.css"; /* optional component layer */
|
|
82
|
+
|
|
83
|
+
.cta {
|
|
84
|
+
background: var(--brand-lime);
|
|
85
|
+
color: var(--brand-ink);
|
|
86
|
+
border-radius: var(--radius-md);
|
|
87
|
+
padding: var(--space-3) var(--space-5);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or read the resolved values in JavaScript or TypeScript (the default theme, fully typed):
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import { tokens } from "@n8mills/design-tokens";
|
|
95
|
+
|
|
96
|
+
tokens["brand-lime"]; // "#d2ff37"
|
|
97
|
+
tokens["color-text-primary"]; // "#0a0a0b"
|
|
98
|
+
tokens["space-5"]; // "24px"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The raw DTCG source ships too, for your own token pipeline:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import dtcg from "@n8mills/design-tokens/tokens.json" with { type: "json" };
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Prefer the source directly? Clone it:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/n8mills-UI/nate-mills-design-system.git
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Read [`design-system.md`](./design-system.md) for the why. Read `tokens.json` and `components.css` for the how. Fork it for another brand by changing `--green-500` and watching every consumer follow. That is the system working as designed.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
[MIT](./LICENSE). Use it, learn from it, build on it.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
<div align="center">
|
|
122
|
+
|
|
123
|
+
Built by Nate Mills, Design Systems and Full-Stack Design.
|
|
124
|
+
[natemills.me](https://natemills.me) | [LinkedIn](https://www.linkedin.com/in/millsdesign)
|
|
125
|
+
|
|
126
|
+
</div>
|