@hanzo/design 0.4.1 → 0.4.2
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/components/core/Button.jsx +35 -9
- package/components/core/Card.jsx +20 -5
- package/components/forms/Input.jsx +13 -3
- package/components/navigation/Tabs.jsx +18 -7
- package/components/overlays/Dialog.jsx +2 -2
- package/components/overlays/DropdownMenu.jsx +9 -5
- package/dist/tokens.gen.d.ts +40 -22
- package/dist/tokens.gen.d.ts.map +1 -1
- package/dist/tokens.gen.js +40 -22
- package/package.json +10 -8
- package/scripts/check-tokens.mjs +62 -18
- package/src/tokens.gen.ts +40 -22
- package/styles.css +245 -60
- package/tailwind.css +245 -60
- package/tokens/base.css +56 -3
- package/tokens/colors.css +98 -52
- package/tokens/elevation.css +76 -5
- package/tokens/motion.css +15 -0
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/design",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"packageManager": "pnpm@11.17.0",
|
|
5
|
+
"description": "Hanzo Design System \u2014 monochrome, dark-default tokens + components + brand assets, the single source of truth for every Hanzo surface. CSS + typed programmatic tokens.",
|
|
5
6
|
"license": "BSD-3-Clause",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -29,6 +30,13 @@
|
|
|
29
30
|
"./skills/*": "./skills/*",
|
|
30
31
|
"./tailwind.css": "./tailwind.css"
|
|
31
32
|
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"gen": "node scripts/gen-tokens.mjs",
|
|
35
|
+
"test": "node scripts/check-tokens.mjs && node scripts/lint.mjs components",
|
|
36
|
+
"build": "pnpm gen && pnpm test && tsc -p tsconfig.json",
|
|
37
|
+
"prepublishOnly": "pnpm build",
|
|
38
|
+
"lint": "node scripts/lint.mjs"
|
|
39
|
+
},
|
|
32
40
|
"devDependencies": {
|
|
33
41
|
"typescript": "^5.9.3"
|
|
34
42
|
},
|
|
@@ -61,11 +69,5 @@
|
|
|
61
69
|
},
|
|
62
70
|
"bin": {
|
|
63
71
|
"hanzo-design-lint": "./scripts/lint.mjs"
|
|
64
|
-
},
|
|
65
|
-
"scripts": {
|
|
66
|
-
"gen": "node scripts/gen-tokens.mjs",
|
|
67
|
-
"test": "node scripts/check-tokens.mjs && node scripts/lint.mjs components",
|
|
68
|
-
"build": "pnpm gen && pnpm test && tsc -p tsconfig.json",
|
|
69
|
-
"lint": "node scripts/lint.mjs"
|
|
70
72
|
}
|
|
71
73
|
}
|
package/scripts/check-tokens.mjs
CHANGED
|
@@ -114,9 +114,17 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
114
114
|
: pass(`all ${used.size} internal var() references resolve`)
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// ── 3. the
|
|
118
|
-
//
|
|
119
|
-
//
|
|
117
|
+
// ── 3. the focus-indicator contrast gate (WCAG 2.4.11) ───────────────────
|
|
118
|
+
// The focus indicator must clear 3:1 against EVERY surface it can land on, in
|
|
119
|
+
// BOTH themes. --ring was #333333 = 1.66:1 on --background.
|
|
120
|
+
//
|
|
121
|
+
// --border-control was gated here too and no longer is. That was not a
|
|
122
|
+
// regression, it was the point: pinning a control's RESTING edge at 3:1 forces
|
|
123
|
+
// it to a mid-grey on a near-black ground, four times heavier than the hairline
|
|
124
|
+
// beside it, and a form drawn that way reads as a wireframe. The edge is now
|
|
125
|
+
// rgb(255 255 255 / .15) — the reference's own value — and the contrast budget
|
|
126
|
+
// is spent entirely on --ring, which is what a keyboard user actually navigates
|
|
127
|
+
// by. A resting edge is an affordance; a focus ring is a position.
|
|
120
128
|
{
|
|
121
129
|
const css = strip(read(join(tokensDir, 'colors.css')))
|
|
122
130
|
const block = (re) => { const m = css.match(re); const o = {}; if (m) for (const [, n, v] of m[1].matchAll(/--([A-Za-z0-9-]+)\s*:\s*([^;]+);/g)) o[n] = v.trim(); return o }
|
|
@@ -144,11 +152,12 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
const CANVASES = ['background', 'card', 'popover', 'muted', 'secondary', 'surface-card', 'surface-overlay']
|
|
147
|
-
// The gate follows the DUTY, not the name
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
155
|
+
// The gate follows the DUTY, not the name — and exactly ONE duty owes a
|
|
156
|
+
// ratio. --ring is the focus indicator: transient, keyboard-only, and the
|
|
157
|
+
// whole of how someone knows where they are. Every boundary that is merely an
|
|
158
|
+
// affordance — --border, --border-strong, --border-control, --border-focus,
|
|
159
|
+
// --border-selected — is free to be as quiet as it looks.
|
|
160
|
+
const GATED = { ring: 3 }
|
|
152
161
|
for (const [theme, scope] of Object.entries(themes)) {
|
|
153
162
|
for (const [tok, min] of Object.entries(GATED)) {
|
|
154
163
|
let worst = Infinity, where = ''
|
|
@@ -164,22 +173,57 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
// ── 4. every white-alpha token must be restated for the light theme ──────
|
|
167
|
-
// The opacity ladder does NOT invert. A semantic token
|
|
176
|
+
// The opacity ladder does NOT invert. A semantic token built from
|
|
168
177
|
// rgb(255 255 255 / a) is white-on-white in `.light` — the border does not
|
|
169
|
-
// change colour, it
|
|
178
|
+
// change colour, it STOPS EXISTING — and nothing renders an error. The raw
|
|
170
179
|
// ladder itself (--white-*) is exempt: it is a palette, not a semantic token,
|
|
171
180
|
// and light-theme tokens are expected to restate rather than invert it.
|
|
181
|
+
//
|
|
182
|
+
// Two things widen this from where it started. It scans tokens/elevation.css
|
|
183
|
+
// as well as tokens/colors.css, because that is where the light lives now —
|
|
184
|
+
// an inset white highlight, a white hover bloom and a white panel sheen all
|
|
185
|
+
// fail in `.light` exactly the way a white border does, and more invisibly,
|
|
186
|
+
// since a missing lift looks like a design choice. And it matches white-alpha
|
|
187
|
+
// ANYWHERE INSIDE a value rather than requiring the whole value to be one
|
|
188
|
+
// colour, because every one of those tokens is a shadow or a gradient with
|
|
189
|
+
// the colour buried in it — `inset 0 1px 0 0 rgb(255 255 255 / .07)` would
|
|
190
|
+
// have sailed through a whole-value parse.
|
|
172
191
|
{
|
|
173
192
|
const PALETTE = /^(white|neutral|pure|hanzo)-/
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
})
|
|
193
|
+
const FILES = ['colors', 'elevation']
|
|
194
|
+
const blockOf = (src, re) => { const m = strip(src).match(re); const o = {}; if (m) for (const [, n, v] of m[1].matchAll(/--([A-Za-z0-9-]+)\s*:\s*([^;]+);/g)) o[n] = v.trim(); return o }
|
|
195
|
+
const ROOT = /:root\s*\{([\s\S]*?)\n\}/
|
|
196
|
+
const LIGHT = /\.light\s*\{([\s\S]*?)\n\}/
|
|
197
|
+
|
|
198
|
+
const sources = Object.fromEntries(FILES.map((f) => [f, read(join(tokensDir, `${f}.css`))]))
|
|
199
|
+
// One scope to resolve against: elevation references --white-* rungs that
|
|
200
|
+
// colors declares, so a per-file scope could not expand them.
|
|
201
|
+
const scope = {}
|
|
202
|
+
for (const f of FILES) Object.assign(scope, blockOf(sources[f], ROOT))
|
|
203
|
+
// Restating in ANY file's .light block counts — the cascade does not care
|
|
204
|
+
// which file a declaration came from.
|
|
205
|
+
const lightKeys = new Set()
|
|
206
|
+
for (const f of FILES) for (const k of Object.keys(blockOf(sources[f], LIGHT))) lightKeys.add(k)
|
|
207
|
+
|
|
208
|
+
// Expansion STOPS at any token that is itself restated in `.light`.
|
|
209
|
+
// `--ring-focus: 0 0 0 3px var(--ring-halo)` is theme-safe precisely
|
|
210
|
+
// because --ring-halo inverts underneath it — var() resolves per theme at
|
|
211
|
+
// use time, so deferring to a token that flips IS the fix, and a checker
|
|
212
|
+
// that expanded through it would demand every wrapper be restated too.
|
|
213
|
+
const expand = (v, d = 0) =>
|
|
214
|
+
d > 10 ? v : String(v).replace(/var\(\s*--([A-Za-z0-9-]+)\s*\)/g,
|
|
215
|
+
(m, n) => (n in scope && !lightKeys.has(n) ? expand(scope[n], d + 1) : m))
|
|
216
|
+
const WHITE_ALPHA = /rgba?\(\s*255[\s,]+255[\s,]+255\s*[/,]\s*(?:0?\.\d+|0)\s*\)/i
|
|
217
|
+
|
|
218
|
+
const leaked = []
|
|
219
|
+
for (const f of FILES)
|
|
220
|
+
for (const k of Object.keys(blockOf(sources[f], ROOT))) {
|
|
221
|
+
if (PALETTE.test(k) || lightKeys.has(k)) continue
|
|
222
|
+
if (WHITE_ALPHA.test(expand(scope[k]))) leaked.push(`${k} (tokens/${f}.css)`)
|
|
223
|
+
}
|
|
180
224
|
leaked.length
|
|
181
|
-
? leaked.forEach((k) => fail(`--${k} is white-alpha and has no .light value — it vanishes in the light theme`))
|
|
182
|
-
: pass(`every white-alpha token is restated in .light`)
|
|
225
|
+
? leaked.forEach((k) => fail(`--${k} is built from white-alpha and has no .light value — it vanishes in the light theme`))
|
|
226
|
+
: pass(`every white-alpha token in ${FILES.join('/')}.css is restated in .light`)
|
|
183
227
|
}
|
|
184
228
|
}
|
|
185
229
|
|
package/src/tokens.gen.ts
CHANGED
|
@@ -40,22 +40,26 @@ export const colors = {
|
|
|
40
40
|
'popover': '#0f0f0f',
|
|
41
41
|
'popover-foreground': '#fafafa',
|
|
42
42
|
'primary': '#fafafa',
|
|
43
|
-
'primary-hover': '#
|
|
43
|
+
'primary-hover': '#e5e5e5',
|
|
44
44
|
'primary-foreground': '#0a0a0a',
|
|
45
45
|
'secondary': '#262626',
|
|
46
|
+
'secondary-hover': '#333333',
|
|
46
47
|
'secondary-foreground': '#fafafa',
|
|
47
48
|
'muted': '#171717',
|
|
48
49
|
'muted-foreground': '#a3a3a3',
|
|
49
50
|
'accent': '#262626',
|
|
50
51
|
'accent-foreground': '#fafafa',
|
|
51
|
-
'destructive': '
|
|
52
|
+
'destructive': 'var(--state-error)',
|
|
53
|
+
'destructive-hover': '#dc2626',
|
|
52
54
|
'destructive-foreground': '#fafafa',
|
|
53
55
|
'border': 'var(--white-10)',
|
|
54
56
|
'border-strong': 'var(--white-16)',
|
|
55
|
-
'border-control': 'var(--
|
|
56
|
-
'border-focus': 'var(--
|
|
57
|
+
'border-control': 'var(--white-15)',
|
|
58
|
+
'border-focus': 'var(--white-22)',
|
|
59
|
+
'border-selected': 'var(--white-30)',
|
|
57
60
|
'input': 'var(--border-control)',
|
|
58
|
-
'ring': 'var(--
|
|
61
|
+
'ring': 'var(--white-40)',
|
|
62
|
+
'ring-halo': 'var(--white-10)',
|
|
59
63
|
'brand': '#e4e4e7',
|
|
60
64
|
'brand-foreground': '#09090b',
|
|
61
65
|
'brand-muted': '#a3a3a3',
|
|
@@ -65,10 +69,10 @@ export const colors = {
|
|
|
65
69
|
'glass': 'var(--white-05)',
|
|
66
70
|
'glass-strong': 'var(--white-08)',
|
|
67
71
|
'surface-page': 'var(--background)',
|
|
68
|
-
'surface-card': 'rgb(
|
|
69
|
-
'surface-card-emphasis': 'rgb(
|
|
70
|
-
'surface-card-quiet': 'rgb(
|
|
71
|
-
'surface-overlay': 'rgb(
|
|
72
|
+
'surface-card': 'rgb(38 38 38 / .5)',
|
|
73
|
+
'surface-card-emphasis': 'rgb(38 38 38 / .75)',
|
|
74
|
+
'surface-card-quiet': 'rgb(38 38 38 / .35)',
|
|
75
|
+
'surface-overlay': 'rgb(23 23 23 / .92)',
|
|
72
76
|
'surface-header': 'rgb(0 0 0 / .7)',
|
|
73
77
|
'surface-scrim': 'rgb(0 0 0 / .8)',
|
|
74
78
|
'surface-0': 'var(--background)',
|
|
@@ -231,9 +235,11 @@ export const radius = {
|
|
|
231
235
|
/** elevation tokens (from tokens/elevation.css). Values are raw CSS. */
|
|
232
236
|
export const elevation = {
|
|
233
237
|
'shadow-none': 'none',
|
|
234
|
-
'shadow-floating': '0
|
|
238
|
+
'shadow-floating': '0 24px 60px -16px rgb(0 0 0 / .75)',
|
|
235
239
|
'shadow-inset-hairline': 'inset 0 0 0 1px var(--white-10)',
|
|
236
|
-
'
|
|
240
|
+
'edge-highlight': 'inset 0 1px 0 0 rgb(255 255 255 / .10)',
|
|
241
|
+
'bloom': '0 0 24px -2px rgb(255 255 255 / .28)',
|
|
242
|
+
'ring-focus': '0 0 0 3px var(--ring-halo)',
|
|
237
243
|
'shadow-sm': '0 1px 2px 0 rgb(0 0 0 / .40)',
|
|
238
244
|
'shadow': '0 1px 3px 0 rgb(0 0 0 / .45), 0 1px 2px -1px rgb(0 0 0 / .45)',
|
|
239
245
|
'shadow-md': '0 4px 6px -1px rgb(0 0 0 / .50), 0 2px 4px -2px rgb(0 0 0 / .50)',
|
|
@@ -243,6 +249,7 @@ export const elevation = {
|
|
|
243
249
|
'glow-hero': 'radial-gradient(circle,rgb(255 255 255 / .12) 0%,transparent 68%)',
|
|
244
250
|
'glow-hero-blur': '120px',
|
|
245
251
|
'sheen-card': 'radial-gradient(120% 120% at 80% 0%,rgb(255 255 255 / .08) 0%,transparent 55%)',
|
|
252
|
+
'sheen-edge': 'linear-gradient(90deg,transparent,rgb(255 255 255 / .14) 50%,transparent)',
|
|
246
253
|
'gradient-chrome': 'linear-gradient(to right,#ffffff,var(--white-80),var(--white-60))',
|
|
247
254
|
'gradient-chrome-2': 'linear-gradient(to right,#ffffff,var(--neutral-500))',
|
|
248
255
|
'gradient-protect': 'linear-gradient(to bottom,var(--white-10),transparent)',
|
|
@@ -255,8 +262,10 @@ export const motion = {
|
|
|
255
262
|
'duration-slow': '400ms',
|
|
256
263
|
'duration-slower': '500ms',
|
|
257
264
|
'duration-glow': '9s',
|
|
265
|
+
'duration-press': '90ms',
|
|
258
266
|
'ease-out': 'cubic-bezier(0,0,0.2,1)',
|
|
259
267
|
'ease-in-out': 'cubic-bezier(0.4,0,0.2,1)',
|
|
268
|
+
'ease-emphasis': 'cubic-bezier(0.16,1,0.3,1)',
|
|
260
269
|
'stagger': '60ms',
|
|
261
270
|
'entry-rise': '16px',
|
|
262
271
|
'entry-rise-lg': '24px',
|
|
@@ -329,22 +338,26 @@ export const cssVars = {
|
|
|
329
338
|
'--popover': '#0f0f0f',
|
|
330
339
|
'--popover-foreground': '#fafafa',
|
|
331
340
|
'--primary': '#fafafa',
|
|
332
|
-
'--primary-hover': '#
|
|
341
|
+
'--primary-hover': '#e5e5e5',
|
|
333
342
|
'--primary-foreground': '#0a0a0a',
|
|
334
343
|
'--secondary': '#262626',
|
|
344
|
+
'--secondary-hover': '#333333',
|
|
335
345
|
'--secondary-foreground': '#fafafa',
|
|
336
346
|
'--muted': '#171717',
|
|
337
347
|
'--muted-foreground': '#a3a3a3',
|
|
338
348
|
'--accent': '#262626',
|
|
339
349
|
'--accent-foreground': '#fafafa',
|
|
340
|
-
'--destructive': '
|
|
350
|
+
'--destructive': 'var(--state-error)',
|
|
351
|
+
'--destructive-hover': '#dc2626',
|
|
341
352
|
'--destructive-foreground': '#fafafa',
|
|
342
353
|
'--border': 'var(--white-10)',
|
|
343
354
|
'--border-strong': 'var(--white-16)',
|
|
344
|
-
'--border-control': 'var(--
|
|
345
|
-
'--border-focus': 'var(--
|
|
355
|
+
'--border-control': 'var(--white-15)',
|
|
356
|
+
'--border-focus': 'var(--white-22)',
|
|
357
|
+
'--border-selected': 'var(--white-30)',
|
|
346
358
|
'--input': 'var(--border-control)',
|
|
347
|
-
'--ring': 'var(--
|
|
359
|
+
'--ring': 'var(--white-40)',
|
|
360
|
+
'--ring-halo': 'var(--white-10)',
|
|
348
361
|
'--brand': '#e4e4e7',
|
|
349
362
|
'--brand-foreground': '#09090b',
|
|
350
363
|
'--brand-muted': '#a3a3a3',
|
|
@@ -354,10 +367,10 @@ export const cssVars = {
|
|
|
354
367
|
'--glass': 'var(--white-05)',
|
|
355
368
|
'--glass-strong': 'var(--white-08)',
|
|
356
369
|
'--surface-page': 'var(--background)',
|
|
357
|
-
'--surface-card': 'rgb(
|
|
358
|
-
'--surface-card-emphasis': 'rgb(
|
|
359
|
-
'--surface-card-quiet': 'rgb(
|
|
360
|
-
'--surface-overlay': 'rgb(
|
|
370
|
+
'--surface-card': 'rgb(38 38 38 / .5)',
|
|
371
|
+
'--surface-card-emphasis': 'rgb(38 38 38 / .75)',
|
|
372
|
+
'--surface-card-quiet': 'rgb(38 38 38 / .35)',
|
|
373
|
+
'--surface-overlay': 'rgb(23 23 23 / .92)',
|
|
361
374
|
'--surface-header': 'rgb(0 0 0 / .7)',
|
|
362
375
|
'--surface-scrim': 'rgb(0 0 0 / .8)',
|
|
363
376
|
'--surface-0': 'var(--background)',
|
|
@@ -500,9 +513,11 @@ export const cssVars = {
|
|
|
500
513
|
'--radius-composer': '28px',
|
|
501
514
|
'--radius-full': '9999px',
|
|
502
515
|
'--shadow-none': 'none',
|
|
503
|
-
'--shadow-floating': '0
|
|
516
|
+
'--shadow-floating': '0 24px 60px -16px rgb(0 0 0 / .75)',
|
|
504
517
|
'--shadow-inset-hairline': 'inset 0 0 0 1px var(--white-10)',
|
|
505
|
-
'--
|
|
518
|
+
'--edge-highlight': 'inset 0 1px 0 0 rgb(255 255 255 / .10)',
|
|
519
|
+
'--bloom': '0 0 24px -2px rgb(255 255 255 / .28)',
|
|
520
|
+
'--ring-focus': '0 0 0 3px var(--ring-halo)',
|
|
506
521
|
'--shadow-sm': '0 1px 2px 0 rgb(0 0 0 / .40)',
|
|
507
522
|
'--shadow': '0 1px 3px 0 rgb(0 0 0 / .45), 0 1px 2px -1px rgb(0 0 0 / .45)',
|
|
508
523
|
'--shadow-md': '0 4px 6px -1px rgb(0 0 0 / .50), 0 2px 4px -2px rgb(0 0 0 / .50)',
|
|
@@ -512,6 +527,7 @@ export const cssVars = {
|
|
|
512
527
|
'--glow-hero': 'radial-gradient(circle,rgb(255 255 255 / .12) 0%,transparent 68%)',
|
|
513
528
|
'--glow-hero-blur': '120px',
|
|
514
529
|
'--sheen-card': 'radial-gradient(120% 120% at 80% 0%,rgb(255 255 255 / .08) 0%,transparent 55%)',
|
|
530
|
+
'--sheen-edge': 'linear-gradient(90deg,transparent,rgb(255 255 255 / .14) 50%,transparent)',
|
|
515
531
|
'--gradient-chrome': 'linear-gradient(to right,#ffffff,var(--white-80),var(--white-60))',
|
|
516
532
|
'--gradient-chrome-2': 'linear-gradient(to right,#ffffff,var(--neutral-500))',
|
|
517
533
|
'--gradient-protect': 'linear-gradient(to bottom,var(--white-10),transparent)',
|
|
@@ -520,8 +536,10 @@ export const cssVars = {
|
|
|
520
536
|
'--duration-slow': '400ms',
|
|
521
537
|
'--duration-slower': '500ms',
|
|
522
538
|
'--duration-glow': '9s',
|
|
539
|
+
'--duration-press': '90ms',
|
|
523
540
|
'--ease-out': 'cubic-bezier(0,0,0.2,1)',
|
|
524
541
|
'--ease-in-out': 'cubic-bezier(0.4,0,0.2,1)',
|
|
542
|
+
'--ease-emphasis': 'cubic-bezier(0.16,1,0.3,1)',
|
|
525
543
|
'--stagger': '60ms',
|
|
526
544
|
'--entry-rise': '16px',
|
|
527
545
|
'--entry-rise-lg': '24px',
|