@hanzo/design 0.3.5 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/components/core/Avatar.jsx +1 -1
- package/components/core/Badge.jsx +10 -7
- package/components/core/Button.jsx +1 -2
- package/components/core/Card.jsx +1 -1
- package/components/core/Progress.jsx +1 -1
- package/components/core/Table.jsx +2 -2
- package/components/forms/Checkbox.jsx +1 -1
- package/components/forms/Input.jsx +4 -4
- package/components/forms/Select.jsx +3 -3
- package/components/forms/Slider.jsx +1 -1
- package/components/forms/Textarea.jsx +3 -3
- package/components/navigation/Accordion.jsx +2 -2
- package/components/navigation/navigation.card.html +1 -1
- package/components/overlays/Dialog.jsx +1 -1
- package/components/overlays/DropdownMenu.jsx +3 -3
- package/components/overlays/Sheet.jsx +3 -3
- package/components/overlays/Toaster.jsx +1 -1
- package/dist/tokens.gen.d.ts +92 -60
- package/dist/tokens.gen.d.ts.map +1 -1
- package/dist/tokens.gen.js +92 -60
- package/package.json +11 -11
- package/scripts/check-tokens.mjs +64 -1
- package/scripts/gen-tokens.mjs +79 -0
- package/src/tokens.gen.ts +92 -60
- package/styles.css +313 -81
- package/tailwind.css +787 -0
- package/tokens/base.css +125 -22
- package/tokens/colors.css +121 -52
- package/tokens/spacing.css +46 -7
- package/tokens/typography.css +21 -0
package/dist/tokens.gen.js
CHANGED
|
@@ -19,37 +19,50 @@ export const colors = {
|
|
|
19
19
|
'hanzo-black': '#0A0A0B',
|
|
20
20
|
'hanzo-white': '#FFFFFF',
|
|
21
21
|
'white-05': 'rgb(255 255 255 / .05)',
|
|
22
|
+
'white-06': 'rgb(255 255 255 / .06)',
|
|
23
|
+
'white-08': 'rgb(255 255 255 / .08)',
|
|
22
24
|
'white-10': 'rgb(255 255 255 / .10)',
|
|
23
25
|
'white-15': 'rgb(255 255 255 / .15)',
|
|
26
|
+
'white-16': 'rgb(255 255 255 / .16)',
|
|
24
27
|
'white-20': 'rgb(255 255 255 / .20)',
|
|
28
|
+
'white-22': 'rgb(255 255 255 / .22)',
|
|
25
29
|
'white-30': 'rgb(255 255 255 / .30)',
|
|
26
30
|
'white-40': 'rgb(255 255 255 / .40)',
|
|
31
|
+
'white-55': 'rgb(255 255 255 / .55)',
|
|
27
32
|
'white-60': 'rgb(255 255 255 / .60)',
|
|
33
|
+
'white-78': 'rgb(255 255 255 / .78)',
|
|
28
34
|
'white-80': 'rgb(255 255 255 / .80)',
|
|
29
|
-
'background': '#
|
|
30
|
-
'foreground': '#
|
|
31
|
-
'card': '#
|
|
32
|
-
'card-foreground': '#
|
|
33
|
-
'popover': '#
|
|
34
|
-
'popover-foreground': '#
|
|
35
|
-
'primary': '#
|
|
36
|
-
'primary-
|
|
37
|
-
'
|
|
38
|
-
'secondary
|
|
39
|
-
'
|
|
40
|
-
'muted
|
|
41
|
-
'
|
|
42
|
-
'accent
|
|
35
|
+
'background': '#0a0a0a',
|
|
36
|
+
'foreground': '#fafafa',
|
|
37
|
+
'card': '#0f0f0f',
|
|
38
|
+
'card-foreground': '#fafafa',
|
|
39
|
+
'popover': '#0f0f0f',
|
|
40
|
+
'popover-foreground': '#fafafa',
|
|
41
|
+
'primary': '#fafafa',
|
|
42
|
+
'primary-hover': '#d4d4d4',
|
|
43
|
+
'primary-foreground': '#0a0a0a',
|
|
44
|
+
'secondary': '#262626',
|
|
45
|
+
'secondary-foreground': '#fafafa',
|
|
46
|
+
'muted': '#171717',
|
|
47
|
+
'muted-foreground': '#a3a3a3',
|
|
48
|
+
'accent': '#262626',
|
|
49
|
+
'accent-foreground': '#fafafa',
|
|
43
50
|
'destructive': '#666666',
|
|
44
|
-
'destructive-foreground': '#
|
|
45
|
-
'border': '
|
|
46
|
-
'
|
|
51
|
+
'destructive-foreground': '#fafafa',
|
|
52
|
+
'border': 'var(--white-10)',
|
|
53
|
+
'border-strong': 'var(--white-16)',
|
|
54
|
+
'border-control': 'var(--neutral-500)',
|
|
55
|
+
'border-focus': 'var(--neutral-400)',
|
|
56
|
+
'input': 'var(--border-control)',
|
|
47
57
|
'ring': 'var(--neutral-500)',
|
|
48
58
|
'brand': '#e4e4e7',
|
|
49
59
|
'brand-foreground': '#09090b',
|
|
50
60
|
'brand-muted': '#a3a3a3',
|
|
51
61
|
'black': '#000000',
|
|
52
|
-
'white': '#
|
|
62
|
+
'white': '#fafafa',
|
|
63
|
+
'selection': 'var(--white-20)',
|
|
64
|
+
'glass': 'var(--white-05)',
|
|
65
|
+
'glass-strong': 'var(--white-08)',
|
|
53
66
|
'surface-page': 'var(--background)',
|
|
54
67
|
'surface-card': 'rgb(23 23 23 / .5)',
|
|
55
68
|
'surface-card-emphasis': 'rgb(23 23 23 / .8)',
|
|
@@ -57,16 +70,13 @@ export const colors = {
|
|
|
57
70
|
'surface-overlay': 'rgb(10 10 10 / .95)',
|
|
58
71
|
'surface-header': 'rgb(0 0 0 / .7)',
|
|
59
72
|
'surface-scrim': 'rgb(0 0 0 / .8)',
|
|
60
|
-
'border-hairline': 'var(--neutral-800)',
|
|
61
|
-
'border-card': 'var(--white-10)',
|
|
62
|
-
'border-strong': 'var(--neutral-500)',
|
|
63
73
|
'surface-0': 'var(--background)',
|
|
64
74
|
'surface-1': 'var(--card)',
|
|
65
75
|
'surface-2': 'var(--muted)',
|
|
66
76
|
'surface-3': 'var(--secondary)',
|
|
67
|
-
'text-primary': '
|
|
68
|
-
'text-secondary': 'var(--white-
|
|
69
|
-
'text-tertiary': 'var(--white-
|
|
77
|
+
'text-primary': '#fafafa',
|
|
78
|
+
'text-secondary': 'var(--white-78)',
|
|
79
|
+
'text-tertiary': 'var(--white-55)',
|
|
70
80
|
'text-helper': 'var(--muted-foreground)',
|
|
71
81
|
'text-disabled': 'var(--white-30)',
|
|
72
82
|
'state-error': '#ef4444',
|
|
@@ -119,6 +129,7 @@ export const typography = {
|
|
|
119
129
|
'font-size-7xl': 'var(--text-7xl)',
|
|
120
130
|
'font-size-8xl': 'var(--text-8xl)',
|
|
121
131
|
'font-size-9xl': 'var(--text-9xl)',
|
|
132
|
+
'text-control': 'var(--text-sm)',
|
|
122
133
|
'weight-normal': '400',
|
|
123
134
|
'weight-medium': '500',
|
|
124
135
|
'weight-semibold': '600',
|
|
@@ -172,14 +183,19 @@ export const spacing = {
|
|
|
172
183
|
'container-max': '80rem',
|
|
173
184
|
'container-prose': '48rem',
|
|
174
185
|
'container-wide': '72rem',
|
|
175
|
-
'gutter': '1rem',
|
|
186
|
+
'gutter': 'max(1rem,var(--safe-left),var(--safe-right))',
|
|
176
187
|
'gutter-sm': '1.5rem',
|
|
177
188
|
'gutter-lg': '2rem',
|
|
178
|
-
'section-y': '
|
|
179
|
-
'section-y-lg': '
|
|
180
|
-
'hero-y': '
|
|
181
|
-
'hero-y-lg': '
|
|
182
|
-
'header-height': '
|
|
189
|
+
'section-y': '2.5rem',
|
|
190
|
+
'section-y-lg': '3.5rem',
|
|
191
|
+
'hero-y': '3rem',
|
|
192
|
+
'hero-y-lg': '4.5rem',
|
|
193
|
+
'header-height': '3.5rem',
|
|
194
|
+
'tap-target': '44px',
|
|
195
|
+
'safe-top': 'env(safe-area-inset-top,0px)',
|
|
196
|
+
'safe-right': 'env(safe-area-inset-right,0px)',
|
|
197
|
+
'safe-bottom': 'env(safe-area-inset-bottom,0px)',
|
|
198
|
+
'safe-left': 'env(safe-area-inset-left,0px)',
|
|
183
199
|
};
|
|
184
200
|
/** grid tokens (from tokens/grid.css). Values are raw CSS. */
|
|
185
201
|
export const grid = {
|
|
@@ -280,37 +296,50 @@ export const cssVars = {
|
|
|
280
296
|
'--hanzo-black': '#0A0A0B',
|
|
281
297
|
'--hanzo-white': '#FFFFFF',
|
|
282
298
|
'--white-05': 'rgb(255 255 255 / .05)',
|
|
299
|
+
'--white-06': 'rgb(255 255 255 / .06)',
|
|
300
|
+
'--white-08': 'rgb(255 255 255 / .08)',
|
|
283
301
|
'--white-10': 'rgb(255 255 255 / .10)',
|
|
284
302
|
'--white-15': 'rgb(255 255 255 / .15)',
|
|
303
|
+
'--white-16': 'rgb(255 255 255 / .16)',
|
|
285
304
|
'--white-20': 'rgb(255 255 255 / .20)',
|
|
305
|
+
'--white-22': 'rgb(255 255 255 / .22)',
|
|
286
306
|
'--white-30': 'rgb(255 255 255 / .30)',
|
|
287
307
|
'--white-40': 'rgb(255 255 255 / .40)',
|
|
308
|
+
'--white-55': 'rgb(255 255 255 / .55)',
|
|
288
309
|
'--white-60': 'rgb(255 255 255 / .60)',
|
|
310
|
+
'--white-78': 'rgb(255 255 255 / .78)',
|
|
289
311
|
'--white-80': 'rgb(255 255 255 / .80)',
|
|
290
|
-
'--background': '#
|
|
291
|
-
'--foreground': '#
|
|
292
|
-
'--card': '#
|
|
293
|
-
'--card-foreground': '#
|
|
294
|
-
'--popover': '#
|
|
295
|
-
'--popover-foreground': '#
|
|
296
|
-
'--primary': '#
|
|
297
|
-
'--primary-
|
|
298
|
-
'--
|
|
299
|
-
'--secondary
|
|
300
|
-
'--
|
|
301
|
-
'--muted
|
|
302
|
-
'--
|
|
303
|
-
'--accent
|
|
312
|
+
'--background': '#0a0a0a',
|
|
313
|
+
'--foreground': '#fafafa',
|
|
314
|
+
'--card': '#0f0f0f',
|
|
315
|
+
'--card-foreground': '#fafafa',
|
|
316
|
+
'--popover': '#0f0f0f',
|
|
317
|
+
'--popover-foreground': '#fafafa',
|
|
318
|
+
'--primary': '#fafafa',
|
|
319
|
+
'--primary-hover': '#d4d4d4',
|
|
320
|
+
'--primary-foreground': '#0a0a0a',
|
|
321
|
+
'--secondary': '#262626',
|
|
322
|
+
'--secondary-foreground': '#fafafa',
|
|
323
|
+
'--muted': '#171717',
|
|
324
|
+
'--muted-foreground': '#a3a3a3',
|
|
325
|
+
'--accent': '#262626',
|
|
326
|
+
'--accent-foreground': '#fafafa',
|
|
304
327
|
'--destructive': '#666666',
|
|
305
|
-
'--destructive-foreground': '#
|
|
306
|
-
'--border': '
|
|
307
|
-
'--
|
|
328
|
+
'--destructive-foreground': '#fafafa',
|
|
329
|
+
'--border': 'var(--white-10)',
|
|
330
|
+
'--border-strong': 'var(--white-16)',
|
|
331
|
+
'--border-control': 'var(--neutral-500)',
|
|
332
|
+
'--border-focus': 'var(--neutral-400)',
|
|
333
|
+
'--input': 'var(--border-control)',
|
|
308
334
|
'--ring': 'var(--neutral-500)',
|
|
309
335
|
'--brand': '#e4e4e7',
|
|
310
336
|
'--brand-foreground': '#09090b',
|
|
311
337
|
'--brand-muted': '#a3a3a3',
|
|
312
338
|
'--black': '#000000',
|
|
313
|
-
'--white': '#
|
|
339
|
+
'--white': '#fafafa',
|
|
340
|
+
'--selection': 'var(--white-20)',
|
|
341
|
+
'--glass': 'var(--white-05)',
|
|
342
|
+
'--glass-strong': 'var(--white-08)',
|
|
314
343
|
'--surface-page': 'var(--background)',
|
|
315
344
|
'--surface-card': 'rgb(23 23 23 / .5)',
|
|
316
345
|
'--surface-card-emphasis': 'rgb(23 23 23 / .8)',
|
|
@@ -318,16 +347,13 @@ export const cssVars = {
|
|
|
318
347
|
'--surface-overlay': 'rgb(10 10 10 / .95)',
|
|
319
348
|
'--surface-header': 'rgb(0 0 0 / .7)',
|
|
320
349
|
'--surface-scrim': 'rgb(0 0 0 / .8)',
|
|
321
|
-
'--border-hairline': 'var(--neutral-800)',
|
|
322
|
-
'--border-card': 'var(--white-10)',
|
|
323
|
-
'--border-strong': 'var(--neutral-500)',
|
|
324
350
|
'--surface-0': 'var(--background)',
|
|
325
351
|
'--surface-1': 'var(--card)',
|
|
326
352
|
'--surface-2': 'var(--muted)',
|
|
327
353
|
'--surface-3': 'var(--secondary)',
|
|
328
|
-
'--text-primary': '
|
|
329
|
-
'--text-secondary': 'var(--white-
|
|
330
|
-
'--text-tertiary': 'var(--white-
|
|
354
|
+
'--text-primary': '#fafafa',
|
|
355
|
+
'--text-secondary': 'var(--white-78)',
|
|
356
|
+
'--text-tertiary': 'var(--white-55)',
|
|
331
357
|
'--text-helper': 'var(--muted-foreground)',
|
|
332
358
|
'--text-disabled': 'var(--white-30)',
|
|
333
359
|
'--state-error': '#ef4444',
|
|
@@ -377,6 +403,7 @@ export const cssVars = {
|
|
|
377
403
|
'--font-size-7xl': 'var(--text-7xl)',
|
|
378
404
|
'--font-size-8xl': 'var(--text-8xl)',
|
|
379
405
|
'--font-size-9xl': 'var(--text-9xl)',
|
|
406
|
+
'--text-control': 'var(--text-sm)',
|
|
380
407
|
'--weight-normal': '400',
|
|
381
408
|
'--weight-medium': '500',
|
|
382
409
|
'--weight-semibold': '600',
|
|
@@ -427,14 +454,19 @@ export const cssVars = {
|
|
|
427
454
|
'--container-max': '80rem',
|
|
428
455
|
'--container-prose': '48rem',
|
|
429
456
|
'--container-wide': '72rem',
|
|
430
|
-
'--gutter': '1rem',
|
|
457
|
+
'--gutter': 'max(1rem,var(--safe-left),var(--safe-right))',
|
|
431
458
|
'--gutter-sm': '1.5rem',
|
|
432
459
|
'--gutter-lg': '2rem',
|
|
433
|
-
'--section-y': '
|
|
434
|
-
'--section-y-lg': '
|
|
435
|
-
'--hero-y': '
|
|
436
|
-
'--hero-y-lg': '
|
|
437
|
-
'--header-height': '
|
|
460
|
+
'--section-y': '2.5rem',
|
|
461
|
+
'--section-y-lg': '3.5rem',
|
|
462
|
+
'--hero-y': '3rem',
|
|
463
|
+
'--hero-y-lg': '4.5rem',
|
|
464
|
+
'--header-height': '3.5rem',
|
|
465
|
+
'--tap-target': '44px',
|
|
466
|
+
'--safe-top': 'env(safe-area-inset-top,0px)',
|
|
467
|
+
'--safe-right': 'env(safe-area-inset-right,0px)',
|
|
468
|
+
'--safe-bottom': 'env(safe-area-inset-bottom,0px)',
|
|
469
|
+
'--safe-left': 'env(safe-area-inset-left,0px)',
|
|
438
470
|
'--grid-columns': '12',
|
|
439
471
|
'--grid-gap': 'var(--space-6)',
|
|
440
472
|
'--grid-gap-tight': 'var(--space-4)',
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/design",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
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.",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Hanzo Design System — monochrome, dark-default tokens + components + brand assets, the single source of truth for every Hanzo surface. CSS + typed programmatic tokens.",
|
|
6
5
|
"license": "BSD-3-Clause",
|
|
7
6
|
"repository": {
|
|
8
7
|
"type": "git",
|
|
@@ -27,20 +26,15 @@
|
|
|
27
26
|
"./prompts/*": "./prompts/*",
|
|
28
27
|
"./content/*": "./content/*",
|
|
29
28
|
"./assets/*": "./assets/*",
|
|
30
|
-
"./skills/*": "./skills/*"
|
|
31
|
-
|
|
32
|
-
"scripts": {
|
|
33
|
-
"gen": "node scripts/gen-tokens.mjs",
|
|
34
|
-
"test": "node scripts/check-tokens.mjs && node scripts/lint.mjs components",
|
|
35
|
-
"build": "pnpm gen && pnpm test && tsc -p tsconfig.json",
|
|
36
|
-
"prepublishOnly": "pnpm build",
|
|
37
|
-
"lint": "node scripts/lint.mjs"
|
|
29
|
+
"./skills/*": "./skills/*",
|
|
30
|
+
"./tailwind.css": "./tailwind.css"
|
|
38
31
|
},
|
|
39
32
|
"devDependencies": {
|
|
40
33
|
"typescript": "^5.9.3"
|
|
41
34
|
},
|
|
42
35
|
"files": [
|
|
43
36
|
"dist",
|
|
37
|
+
"tailwind.css",
|
|
44
38
|
"src",
|
|
45
39
|
"styles.css",
|
|
46
40
|
"tokens",
|
|
@@ -67,5 +61,11 @@
|
|
|
67
61
|
},
|
|
68
62
|
"bin": {
|
|
69
63
|
"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
70
|
}
|
|
71
71
|
}
|
package/scripts/check-tokens.mjs
CHANGED
|
@@ -59,6 +59,46 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
59
59
|
: pass(`all ${urls.length} asset url()s resolve from the package root`)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// ── 1b. the Tailwind bridge is complete and self-contained ───────────────
|
|
63
|
+
// An app should write ONE import and get working utilities. Each thing checked
|
|
64
|
+
// here failed silently in production before it was checked: a missing slot
|
|
65
|
+
// makes that utility resolve to nothing (`border-border` -> currentColor -> a
|
|
66
|
+
// white hairline on black), and a surviving @import makes the browser drop
|
|
67
|
+
// every token without a word.
|
|
68
|
+
{
|
|
69
|
+
const tw = strip(read(join(root, 'tailwind.css')))
|
|
70
|
+
tw.includes('@import')
|
|
71
|
+
? fail('tailwind.css contains an @import — invalid after `@import "tailwindcss"`, so the tokens are dropped')
|
|
72
|
+
: pass('tailwind.css has no @import to invalidate')
|
|
73
|
+
|
|
74
|
+
// Every Tailwind colour slot an app will reach for must be mapped.
|
|
75
|
+
const slots = ['background', 'foreground', 'card', 'popover', 'primary', 'secondary',
|
|
76
|
+
'muted', 'muted-foreground', 'accent', 'destructive', 'border', 'input', 'ring']
|
|
77
|
+
const unmapped = slots.filter((s) => !tw.includes(`--color-${s}:`))
|
|
78
|
+
unmapped.length
|
|
79
|
+
? fail(`tailwind.css does not map: ${unmapped.join(', ')}`)
|
|
80
|
+
: pass(`tailwind.css maps all ${slots.length} core colour slots`)
|
|
81
|
+
|
|
82
|
+
// And it must carry the values, not merely reference them.
|
|
83
|
+
tw.includes('--border:')
|
|
84
|
+
? pass('tailwind.css carries the token values inline')
|
|
85
|
+
: fail('tailwind.css maps slots but carries no tokens — every utility resolves to nothing')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── 1c. element defaults must LOSE to an app's utilities ─────────────────
|
|
89
|
+
// A rule outside a cascade layer beats a rule inside one regardless of
|
|
90
|
+
// specificity, so an unlayered `a{color:...}` here silently outranked every
|
|
91
|
+
// Tailwind text utility on every anchor — a `text-neutral-950` primary button
|
|
92
|
+
// rendered white-on-white and read as a rendering glitch. These are DEFAULTS;
|
|
93
|
+
// a default that cannot be overridden is not a default.
|
|
94
|
+
{
|
|
95
|
+
const base = strip(read(join(tokensDir, 'base.css'))).trim()
|
|
96
|
+
const layered = /^@layer\s+base\s*\{/.test(base) && base.endsWith('}')
|
|
97
|
+
layered
|
|
98
|
+
? pass('base.css element defaults are inside @layer base')
|
|
99
|
+
: fail('base.css is UNLAYERED — its element rules outrank every utility an app writes')
|
|
100
|
+
}
|
|
101
|
+
|
|
62
102
|
// ── 2. every var() used inside the token layer must resolve ──────────────
|
|
63
103
|
{
|
|
64
104
|
const declared = new Set()
|
|
@@ -104,7 +144,11 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
104
144
|
}
|
|
105
145
|
|
|
106
146
|
const CANVASES = ['background', 'card', 'popover', 'muted', 'secondary', 'surface-card', 'surface-overlay']
|
|
107
|
-
|
|
147
|
+
// The gate follows the DUTY, not the name. --border-control is the rung that
|
|
148
|
+
// draws a control's edge and --ring the focus indicator; those two owe 3:1.
|
|
149
|
+
// --border / --border-strong are decorative hairlines and owe nothing — that
|
|
150
|
+
// is exactly why they are allowed to be quiet enough to look like something.
|
|
151
|
+
const GATED = { ring: 3, 'border-control': 3 }
|
|
108
152
|
for (const [theme, scope] of Object.entries(themes)) {
|
|
109
153
|
for (const [tok, min] of Object.entries(GATED)) {
|
|
110
154
|
let worst = Infinity, where = ''
|
|
@@ -118,6 +162,25 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
118
162
|
: pass(`--${tok} (${theme}) ${worst.toFixed(2)}:1 worst case (${where})`)
|
|
119
163
|
}
|
|
120
164
|
}
|
|
165
|
+
|
|
166
|
+
// ── 4. every white-alpha token must be restated for the light theme ──────
|
|
167
|
+
// The opacity ladder does NOT invert. A semantic token whose dark value is
|
|
168
|
+
// rgb(255 255 255 / a) is white-on-white in `.light` — the border does not
|
|
169
|
+
// change colour, it stops existing — and nothing renders an error. The raw
|
|
170
|
+
// ladder itself (--white-*) is exempt: it is a palette, not a semantic token,
|
|
171
|
+
// and light-theme tokens are expected to restate rather than invert it.
|
|
172
|
+
{
|
|
173
|
+
const PALETTE = /^(white|neutral|pure|hanzo)-/
|
|
174
|
+
const lightKeys = new Set(Object.keys(block(/\.light\s*\{([\s\S]*?)\n\}/)))
|
|
175
|
+
const leaked = Object.keys(dark).filter((k) => {
|
|
176
|
+
if (PALETTE.test(k) || lightKeys.has(k)) return false
|
|
177
|
+
const c = rgb(deref(dark[k], dark))
|
|
178
|
+
return c && c[3] < 1 && c[0] > 200 && c[1] > 200 && c[2] > 200
|
|
179
|
+
})
|
|
180
|
+
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`)
|
|
183
|
+
}
|
|
121
184
|
}
|
|
122
185
|
|
|
123
186
|
console.log(failures ? `\n${failures} check(s) failed` : '\nall token checks passed')
|
package/scripts/gen-tokens.mjs
CHANGED
|
@@ -124,3 +124,82 @@ const bundle = banner + FILES.map((f) => {
|
|
|
124
124
|
}).join('')
|
|
125
125
|
writeFileSync(join(root, 'styles.css'), bundle)
|
|
126
126
|
console.log(`gen-tokens: wrote styles.css — ${FILES.length} token groups, flattened`)
|
|
127
|
+
|
|
128
|
+
// ── tailwind.css — the ONE line a Tailwind app writes ────────────────────
|
|
129
|
+
// Every app that consumed these tokens still had to hand-write the bridge from
|
|
130
|
+
// our variable names to Tailwind's utility namespace:
|
|
131
|
+
//
|
|
132
|
+
// @theme inline { --color-background: var(--background); ...twenty more }
|
|
133
|
+
//
|
|
134
|
+
// Twenty lines of boilerplate, copied into each new app, is a drift vector with
|
|
135
|
+
// a silent failure mode: miss one and that utility resolves to nothing —
|
|
136
|
+
// `border-border` becomes currentColor and paints a white hairline on black,
|
|
137
|
+
// which reads as a design choice rather than a missing line. Tabs shipped with
|
|
138
|
+
// three of these and used forty-one.
|
|
139
|
+
//
|
|
140
|
+
// So the bridge is generated here from SLOTS, and an app writes one import
|
|
141
|
+
// after `@import "tailwindcss"`. There is nothing left to get wrong, and adding
|
|
142
|
+
// a semantic token wires it up for every app at once.
|
|
143
|
+
const SLOTS = [
|
|
144
|
+
'background', 'foreground',
|
|
145
|
+
'card', 'card-foreground',
|
|
146
|
+
'popover', 'popover-foreground',
|
|
147
|
+
'primary', 'primary-foreground',
|
|
148
|
+
'secondary', 'secondary-foreground',
|
|
149
|
+
'muted', 'muted-foreground',
|
|
150
|
+
'accent', 'accent-foreground',
|
|
151
|
+
'destructive', 'destructive-foreground',
|
|
152
|
+
'border', 'input', 'ring',
|
|
153
|
+
]
|
|
154
|
+
// State colours are their own vocabulary and keep their names, so `text-error`
|
|
155
|
+
// and `bg-online` mean what they say instead of borrowing a neutral slot.
|
|
156
|
+
const STATE = { error: 'state-error', success: 'state-success', online: 'state-online' }
|
|
157
|
+
|
|
158
|
+
const declared = new Set(flat.map(([n]) => n.replace(/^--/, '')))
|
|
159
|
+
const missing = SLOTS.filter((s) => !declared.has(s))
|
|
160
|
+
if (missing.length) {
|
|
161
|
+
console.error(`gen-tokens: tokens/colors.css declares no --${missing.join(', --')}`)
|
|
162
|
+
process.exit(1)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tw = `/* Hanzo Design System — Tailwind v4 bridge. The ONE import a Tailwind app adds.
|
|
166
|
+
*
|
|
167
|
+
* AUTO-GENERATED by scripts/gen-tokens.mjs — DO NOT EDIT.
|
|
168
|
+
*
|
|
169
|
+
* @import "tailwindcss";
|
|
170
|
+
* @import "@hanzo/design/tailwind.css";
|
|
171
|
+
*
|
|
172
|
+
* It carries the tokens AND maps them onto Tailwind's utility namespace, so
|
|
173
|
+
* \`bg-background\`, \`text-muted-foreground\` and \`border-border\` resolve to the
|
|
174
|
+
* system's values with nothing to wire up by hand.
|
|
175
|
+
*
|
|
176
|
+
* Import it AFTER tailwindcss: @theme has to come after the framework it
|
|
177
|
+
* extends, and the element defaults are layered so your utilities still win.
|
|
178
|
+
*
|
|
179
|
+
* "inline" is deliberate — it compiles \`border-border\` to var(--border) rather
|
|
180
|
+
* than to that variable's build-time value, so switching .light at runtime moves
|
|
181
|
+
* the UI and not merely the variables.
|
|
182
|
+
*
|
|
183
|
+
* The tokens are inlined below rather than @imported for the reason styles.css
|
|
184
|
+
* is flattened: an @import must precede every other rule, so importing this
|
|
185
|
+
* after tailwindcss would invalidate it and the browser would drop the whole
|
|
186
|
+
* token layer without a word.
|
|
187
|
+
*/
|
|
188
|
+
${bundle}
|
|
189
|
+
|
|
190
|
+
@theme inline {
|
|
191
|
+
${SLOTS.map((s) => ` --color-${s}: var(--${s});`).join('\n')}
|
|
192
|
+
|
|
193
|
+
${Object.entries(STATE).map(([k, v]) => ` --color-${k}: var(--${v});`).join('\n')}
|
|
194
|
+
|
|
195
|
+
--radius-sm: var(--radius-sm);
|
|
196
|
+
--radius-md: var(--radius-md);
|
|
197
|
+
--radius-lg: var(--radius-lg);
|
|
198
|
+
--radius-xl: var(--radius-xl);
|
|
199
|
+
|
|
200
|
+
--font-sans: var(--font-sans);
|
|
201
|
+
--font-mono: var(--font-mono);
|
|
202
|
+
}
|
|
203
|
+
`
|
|
204
|
+
writeFileSync(join(root, 'tailwind.css'), tw)
|
|
205
|
+
console.log(`gen-tokens: wrote tailwind.css — ${SLOTS.length} colour slots + ${Object.keys(STATE).length} states`)
|