@hanzo/design 0.4.13 → 0.5.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/LICENSE +24 -10
- package/README.md +1 -1
- package/dist/brand.d.ts +30 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +58 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/tokens.gen.d.ts +70 -74
- package/dist/tokens.gen.d.ts.map +1 -1
- package/dist/tokens.gen.js +70 -74
- package/package.json +6 -8
- package/scripts/check-tokens.mjs +0 -34
- package/skills/design-system/SKILL.md +1 -1
- package/src/brand.ts +76 -0
- package/src/index.ts +1 -6
- package/src/tokens.gen.ts +70 -74
- package/styles.css +50 -82
- package/tailwind.css +50 -82
- package/tokens/base.css +13 -33
- package/tokens/colors.css +9 -9
- package/tokens/spacing.css +14 -20
- package/tokens/typography.css +14 -20
- package/dist/preference.d.ts +0 -65
- package/dist/preference.d.ts.map +0 -1
- package/dist/preference.js +0 -102
- package/scripts/check-dist.mjs +0 -81
- package/scripts/check-preference.mjs +0 -168
- package/src/preference.ts +0 -122
package/tailwind.css
CHANGED
|
@@ -252,23 +252,23 @@
|
|
|
252
252
|
silent (the border simply stops existing) and has shipped before. */
|
|
253
253
|
.light{
|
|
254
254
|
color-scheme:light;
|
|
255
|
-
--background:#
|
|
255
|
+
--background:#ffffff;
|
|
256
256
|
--foreground:#0a0a0a;
|
|
257
257
|
/* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
|
|
258
258
|
dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
|
|
259
|
-
--card:#
|
|
259
|
+
--card:#fafafa;
|
|
260
260
|
--card-foreground:#0a0a0a;
|
|
261
|
-
--popover:#
|
|
261
|
+
--popover:#ffffff;
|
|
262
262
|
--popover-foreground:#0a0a0a;
|
|
263
263
|
--primary:#0a0a0a;
|
|
264
264
|
--primary-hover:#262626;
|
|
265
265
|
--primary-foreground:#fafafa;
|
|
266
|
-
--secondary:#
|
|
266
|
+
--secondary:#ededed;
|
|
267
267
|
--secondary-hover:#e0e0e0;
|
|
268
268
|
--secondary-foreground:#0a0a0a;
|
|
269
|
-
--muted:#
|
|
269
|
+
--muted:#f5f5f5;
|
|
270
270
|
--muted-foreground:#525252;
|
|
271
|
-
--accent:#
|
|
271
|
+
--accent:#ededed;
|
|
272
272
|
--accent-foreground:#0a0a0a;
|
|
273
273
|
--destructive:var(--state-error);
|
|
274
274
|
--destructive-hover:#dc2626;
|
|
@@ -293,9 +293,9 @@
|
|
|
293
293
|
--selection:rgb(0 0 0 / .16);
|
|
294
294
|
--glass:rgb(0 0 0 / .04);
|
|
295
295
|
--glass-strong:rgb(0 0 0 / .07);
|
|
296
|
-
--surface-card:#
|
|
297
|
-
--surface-card-emphasis:#
|
|
298
|
-
--surface-card-quiet:#
|
|
296
|
+
--surface-card:#fafafa;
|
|
297
|
+
--surface-card-emphasis:#ffffff;
|
|
298
|
+
--surface-card-quiet:#fcfcfc;
|
|
299
299
|
--surface-overlay:rgb(255 255 255 / .95);
|
|
300
300
|
--surface-header:rgb(255 255 255 / .8);
|
|
301
301
|
--surface-scrim:rgb(0 0 0 / .5);
|
|
@@ -313,25 +313,19 @@
|
|
|
313
313
|
— the two are the SAME scale, mirrored. A surface/tenant overrides any --text-*
|
|
314
314
|
on :root to retune density on demand. */
|
|
315
315
|
:root{
|
|
316
|
-
/*
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
--
|
|
321
|
-
|
|
322
|
-
--text-
|
|
323
|
-
--text-
|
|
324
|
-
--text-
|
|
325
|
-
--text-
|
|
326
|
-
--text-
|
|
327
|
-
--text-
|
|
328
|
-
--text-
|
|
329
|
-
--text-4xl:calc(2rem * var(--type-scale, 1)); --leading-4xl:calc(2.25rem * var(--type-scale, 1)); /* 32px */
|
|
330
|
-
--text-5xl:calc(2.5rem * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px */
|
|
331
|
-
--text-6xl:calc(3.25rem * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
|
|
332
|
-
--text-7xl:calc(4rem * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
|
|
333
|
-
--text-8xl:calc(5.25rem * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
|
|
334
|
-
--text-9xl:calc(7rem * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
|
|
316
|
+
--text-xs:0.6875rem; --leading-xs:1rem; /* 11px — eyebrows / section labels */
|
|
317
|
+
--text-sm:0.8125rem; --leading-sm:1.15rem; /* 13px — nav labels, dense body */
|
|
318
|
+
--text-base:0.875rem; --leading-base:1.35rem; /* 14px — base app text (was 16px) */
|
|
319
|
+
--text-lg:0.9375rem; --leading-lg:1.4rem; /* 15px */
|
|
320
|
+
--text-xl:1.0625rem; --leading-xl:1.55rem; /* 17px */
|
|
321
|
+
--text-2xl:1.3125rem; --leading-2xl:1.7rem; /* 21px */
|
|
322
|
+
--text-3xl:1.625rem; --leading-3xl:1.95rem; /* 26px */
|
|
323
|
+
--text-4xl:2rem; --leading-4xl:2.25rem; /* 32px */
|
|
324
|
+
--text-5xl:2.5rem; --leading-5xl:1.05; /* 40px */
|
|
325
|
+
--text-6xl:3.25rem; --leading-6xl:1; /* 52px */
|
|
326
|
+
--text-7xl:4rem; --leading-7xl:1; /* 64px */
|
|
327
|
+
--text-8xl:5.25rem; --leading-8xl:1; /* 84px */
|
|
328
|
+
--text-9xl:7rem; --leading-9xl:1; /* 112px */
|
|
335
329
|
|
|
336
330
|
/* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
|
|
337
331
|
Hanzo Studio address the ramp as --font-size-*; both names are one value, so
|
|
@@ -396,7 +390,7 @@
|
|
|
396
390
|
form feels broken. pointer:coarse is the real signal — a desktop mouse keeps
|
|
397
391
|
the compact 13px field. */
|
|
398
392
|
@media (pointer:coarse){
|
|
399
|
-
:root{--text-control:
|
|
393
|
+
:root{--text-control:1rem}
|
|
400
394
|
}
|
|
401
395
|
|
|
402
396
|
/* ── tokens/spacing.css ─────────────────────────────────────── */
|
|
@@ -404,27 +398,21 @@
|
|
|
404
398
|
declared in hanzo.ai's tailwind.config.ts (legacy v3 config, kept for
|
|
405
399
|
reference) — use it for editorial layouts, not for component padding. */
|
|
406
400
|
:root{
|
|
407
|
-
/* The density knob. 1 is the published spacing; every --space-* rung below
|
|
408
|
-
multiplies by it, and --grid-gap-* reference those rungs rather than
|
|
409
|
-
restating them, so gaps, padding and section rhythm move together.
|
|
410
|
-
@hanzo/design's `vars({density})` emits exactly this name. */
|
|
411
|
-
--density:1;
|
|
412
|
-
|
|
413
401
|
--space-0:0;
|
|
414
|
-
--space-1:
|
|
415
|
-
--space-2:
|
|
416
|
-
--space-3:
|
|
417
|
-
--space-4:
|
|
418
|
-
--space-5:
|
|
419
|
-
--space-6:
|
|
420
|
-
--space-8:
|
|
421
|
-
--space-10:
|
|
422
|
-
--space-12:
|
|
423
|
-
--space-14:
|
|
424
|
-
--space-16:
|
|
425
|
-
--space-20:
|
|
426
|
-
--space-24:
|
|
427
|
-
--space-32:
|
|
402
|
+
--space-1:0.25rem;
|
|
403
|
+
--space-2:0.5rem;
|
|
404
|
+
--space-3:0.75rem;
|
|
405
|
+
--space-4:1rem;
|
|
406
|
+
--space-5:1.25rem;
|
|
407
|
+
--space-6:1.5rem;
|
|
408
|
+
--space-8:2rem;
|
|
409
|
+
--space-10:2.5rem;
|
|
410
|
+
--space-12:3rem;
|
|
411
|
+
--space-14:3.5rem;
|
|
412
|
+
--space-16:4rem;
|
|
413
|
+
--space-20:5rem;
|
|
414
|
+
--space-24:6rem;
|
|
415
|
+
--space-32:8rem;
|
|
428
416
|
|
|
429
417
|
/* golden ramp (φ) — hanzo.ai tailwind.config.ts */
|
|
430
418
|
--golden-1:0.25rem;
|
|
@@ -869,26 +857,19 @@
|
|
|
869
857
|
padding:0 var(--space-3);
|
|
870
858
|
}
|
|
871
859
|
:where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
|
|
872
|
-
/*
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
And it was SUPPRESSIBLE. It carried the indicator on `border-color`, so any
|
|
887
|
-
app that states `border` on its own fields overrode it — @hanzo/id does,
|
|
888
|
-
unlayered, which beats this layer whatever its specificity, and its focused
|
|
889
|
-
fields sat at the resting .15 while both files read as correct. An outline
|
|
890
|
-
is not a border: nothing in an app's field styling reaches it, so the ring
|
|
891
|
-
paints whether or not the app has opinions about edges. */
|
|
860
|
+
/* ——— focus, on a control ———
|
|
861
|
+
Not the generic ring. A field already HAS an edge, so focus brightens that
|
|
862
|
+
edge (.15 -> .22) and adds a soft halo just outside it — which is what the
|
|
863
|
+
reference does (.composer-box:focus-within) and what separates a focused
|
|
864
|
+
field from a browser default. The generic `outline` is suppressed here
|
|
865
|
+
precisely because it would draw a second, harder box around this one. */
|
|
866
|
+
:where(input,select,textarea):focus-visible{
|
|
867
|
+
outline:none;
|
|
868
|
+
border-color:var(--border-focus);
|
|
869
|
+
box-shadow:var(--ring-focus);
|
|
870
|
+
}
|
|
871
|
+
/* A hovered field lifts its surface a rung. Nudging its EDGE from .15 to .16
|
|
872
|
+
is a state nobody can see. */
|
|
892
873
|
:where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
|
|
893
874
|
:where(input,textarea)::placeholder{color:var(--text-disabled)}
|
|
894
875
|
:where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
|
|
@@ -928,19 +909,6 @@
|
|
|
928
909
|
}
|
|
929
910
|
}
|
|
930
911
|
|
|
931
|
-
/* THE focus indicator. One rule, every focusable thing, no exceptions — a
|
|
932
|
-
button, a link, a summary, a field. 2px at --ring is 3.77:1 on the darkest
|
|
933
|
-
canvas and clears the 2px perimeter WCAG 2.4.13 asks for; the gate in
|
|
934
|
-
check-tokens holds --ring to that and nothing else here may weaken it.
|
|
935
|
-
|
|
936
|
-
There were two rules until 0.4.9, and they collided invisibly. Both computed
|
|
937
|
-
to (0,1,0) — :where() zeroes whatever it wraps, leaving one pseudo-class on
|
|
938
|
-
each side — so the cascade fell through to SOURCE ORDER inside this layer,
|
|
939
|
-
this rule was written later, and it overrode the `outline:none` the field
|
|
940
|
-
rule stated expressly to prevent it. Every focused input on every consumer
|
|
941
|
-
drew BOTH the ring and the edge+halo. Each rule read as correct alone, which
|
|
942
|
-
is why it survived review in both files; the defect existed only in their
|
|
943
|
-
order. One rule cannot disagree with itself. */
|
|
944
912
|
:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
945
913
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
946
914
|
--selection, which BOTH themes define. */
|
package/tokens/base.css
CHANGED
|
@@ -99,26 +99,19 @@
|
|
|
99
99
|
padding:0 var(--space-3);
|
|
100
100
|
}
|
|
101
101
|
:where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
|
|
102
|
-
/*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
And it was SUPPRESSIBLE. It carried the indicator on `border-color`, so any
|
|
117
|
-
app that states `border` on its own fields overrode it — @hanzo/id does,
|
|
118
|
-
unlayered, which beats this layer whatever its specificity, and its focused
|
|
119
|
-
fields sat at the resting .15 while both files read as correct. An outline
|
|
120
|
-
is not a border: nothing in an app's field styling reaches it, so the ring
|
|
121
|
-
paints whether or not the app has opinions about edges. */
|
|
102
|
+
/* ——— focus, on a control ———
|
|
103
|
+
Not the generic ring. A field already HAS an edge, so focus brightens that
|
|
104
|
+
edge (.15 -> .22) and adds a soft halo just outside it — which is what the
|
|
105
|
+
reference does (.composer-box:focus-within) and what separates a focused
|
|
106
|
+
field from a browser default. The generic `outline` is suppressed here
|
|
107
|
+
precisely because it would draw a second, harder box around this one. */
|
|
108
|
+
:where(input,select,textarea):focus-visible{
|
|
109
|
+
outline:none;
|
|
110
|
+
border-color:var(--border-focus);
|
|
111
|
+
box-shadow:var(--ring-focus);
|
|
112
|
+
}
|
|
113
|
+
/* A hovered field lifts its surface a rung. Nudging its EDGE from .15 to .16
|
|
114
|
+
is a state nobody can see. */
|
|
122
115
|
:where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
|
|
123
116
|
:where(input,textarea)::placeholder{color:var(--text-disabled)}
|
|
124
117
|
:where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
|
|
@@ -158,19 +151,6 @@
|
|
|
158
151
|
}
|
|
159
152
|
}
|
|
160
153
|
|
|
161
|
-
/* THE focus indicator. One rule, every focusable thing, no exceptions — a
|
|
162
|
-
button, a link, a summary, a field. 2px at --ring is 3.77:1 on the darkest
|
|
163
|
-
canvas and clears the 2px perimeter WCAG 2.4.13 asks for; the gate in
|
|
164
|
-
check-tokens holds --ring to that and nothing else here may weaken it.
|
|
165
|
-
|
|
166
|
-
There were two rules until 0.4.9, and they collided invisibly. Both computed
|
|
167
|
-
to (0,1,0) — :where() zeroes whatever it wraps, leaving one pseudo-class on
|
|
168
|
-
each side — so the cascade fell through to SOURCE ORDER inside this layer,
|
|
169
|
-
this rule was written later, and it overrode the `outline:none` the field
|
|
170
|
-
rule stated expressly to prevent it. Every focused input on every consumer
|
|
171
|
-
drew BOTH the ring and the edge+halo. Each rule read as correct alone, which
|
|
172
|
-
is why it survived review in both files; the defect existed only in their
|
|
173
|
-
order. One rule cannot disagree with itself. */
|
|
174
154
|
:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
175
155
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
176
156
|
--selection, which BOTH themes define. */
|
package/tokens/colors.css
CHANGED
|
@@ -214,23 +214,23 @@
|
|
|
214
214
|
silent (the border simply stops existing) and has shipped before. */
|
|
215
215
|
.light{
|
|
216
216
|
color-scheme:light;
|
|
217
|
-
--background:#
|
|
217
|
+
--background:#ffffff;
|
|
218
218
|
--foreground:#0a0a0a;
|
|
219
219
|
/* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
|
|
220
220
|
dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
|
|
221
|
-
--card:#
|
|
221
|
+
--card:#fafafa;
|
|
222
222
|
--card-foreground:#0a0a0a;
|
|
223
|
-
--popover:#
|
|
223
|
+
--popover:#ffffff;
|
|
224
224
|
--popover-foreground:#0a0a0a;
|
|
225
225
|
--primary:#0a0a0a;
|
|
226
226
|
--primary-hover:#262626;
|
|
227
227
|
--primary-foreground:#fafafa;
|
|
228
|
-
--secondary:#
|
|
228
|
+
--secondary:#ededed;
|
|
229
229
|
--secondary-hover:#e0e0e0;
|
|
230
230
|
--secondary-foreground:#0a0a0a;
|
|
231
|
-
--muted:#
|
|
231
|
+
--muted:#f5f5f5;
|
|
232
232
|
--muted-foreground:#525252;
|
|
233
|
-
--accent:#
|
|
233
|
+
--accent:#ededed;
|
|
234
234
|
--accent-foreground:#0a0a0a;
|
|
235
235
|
--destructive:var(--state-error);
|
|
236
236
|
--destructive-hover:#dc2626;
|
|
@@ -255,9 +255,9 @@
|
|
|
255
255
|
--selection:rgb(0 0 0 / .16);
|
|
256
256
|
--glass:rgb(0 0 0 / .04);
|
|
257
257
|
--glass-strong:rgb(0 0 0 / .07);
|
|
258
|
-
--surface-card:#
|
|
259
|
-
--surface-card-emphasis:#
|
|
260
|
-
--surface-card-quiet:#
|
|
258
|
+
--surface-card:#fafafa;
|
|
259
|
+
--surface-card-emphasis:#ffffff;
|
|
260
|
+
--surface-card-quiet:#fcfcfc;
|
|
261
261
|
--surface-overlay:rgb(255 255 255 / .95);
|
|
262
262
|
--surface-header:rgb(255 255 255 / .8);
|
|
263
263
|
--surface-scrim:rgb(0 0 0 / .5);
|
package/tokens/spacing.css
CHANGED
|
@@ -2,27 +2,21 @@
|
|
|
2
2
|
declared in hanzo.ai's tailwind.config.ts (legacy v3 config, kept for
|
|
3
3
|
reference) — use it for editorial layouts, not for component padding. */
|
|
4
4
|
:root{
|
|
5
|
-
/* The density knob. 1 is the published spacing; every --space-* rung below
|
|
6
|
-
multiplies by it, and --grid-gap-* reference those rungs rather than
|
|
7
|
-
restating them, so gaps, padding and section rhythm move together.
|
|
8
|
-
@hanzo/design's `vars({density})` emits exactly this name. */
|
|
9
|
-
--density:1;
|
|
10
|
-
|
|
11
5
|
--space-0:0;
|
|
12
|
-
--space-1:
|
|
13
|
-
--space-2:
|
|
14
|
-
--space-3:
|
|
15
|
-
--space-4:
|
|
16
|
-
--space-5:
|
|
17
|
-
--space-6:
|
|
18
|
-
--space-8:
|
|
19
|
-
--space-10:
|
|
20
|
-
--space-12:
|
|
21
|
-
--space-14:
|
|
22
|
-
--space-16:
|
|
23
|
-
--space-20:
|
|
24
|
-
--space-24:
|
|
25
|
-
--space-32:
|
|
6
|
+
--space-1:0.25rem;
|
|
7
|
+
--space-2:0.5rem;
|
|
8
|
+
--space-3:0.75rem;
|
|
9
|
+
--space-4:1rem;
|
|
10
|
+
--space-5:1.25rem;
|
|
11
|
+
--space-6:1.5rem;
|
|
12
|
+
--space-8:2rem;
|
|
13
|
+
--space-10:2.5rem;
|
|
14
|
+
--space-12:3rem;
|
|
15
|
+
--space-14:3.5rem;
|
|
16
|
+
--space-16:4rem;
|
|
17
|
+
--space-20:5rem;
|
|
18
|
+
--space-24:6rem;
|
|
19
|
+
--space-32:8rem;
|
|
26
20
|
|
|
27
21
|
/* golden ramp (φ) — hanzo.ai tailwind.config.ts */
|
|
28
22
|
--golden-1:0.25rem;
|
package/tokens/typography.css
CHANGED
|
@@ -5,25 +5,19 @@
|
|
|
5
5
|
— the two are the SAME scale, mirrored. A surface/tenant overrides any --text-*
|
|
6
6
|
on :root to retune density on demand. */
|
|
7
7
|
:root{
|
|
8
|
-
/*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
--
|
|
13
|
-
|
|
14
|
-
--text-
|
|
15
|
-
--text-
|
|
16
|
-
--text-
|
|
17
|
-
--text-
|
|
18
|
-
--text-
|
|
19
|
-
--text-
|
|
20
|
-
--text-
|
|
21
|
-
--text-4xl:calc(2rem * var(--type-scale, 1)); --leading-4xl:calc(2.25rem * var(--type-scale, 1)); /* 32px */
|
|
22
|
-
--text-5xl:calc(2.5rem * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px */
|
|
23
|
-
--text-6xl:calc(3.25rem * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
|
|
24
|
-
--text-7xl:calc(4rem * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
|
|
25
|
-
--text-8xl:calc(5.25rem * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
|
|
26
|
-
--text-9xl:calc(7rem * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
|
|
8
|
+
--text-xs:0.6875rem; --leading-xs:1rem; /* 11px — eyebrows / section labels */
|
|
9
|
+
--text-sm:0.8125rem; --leading-sm:1.15rem; /* 13px — nav labels, dense body */
|
|
10
|
+
--text-base:0.875rem; --leading-base:1.35rem; /* 14px — base app text (was 16px) */
|
|
11
|
+
--text-lg:0.9375rem; --leading-lg:1.4rem; /* 15px */
|
|
12
|
+
--text-xl:1.0625rem; --leading-xl:1.55rem; /* 17px */
|
|
13
|
+
--text-2xl:1.3125rem; --leading-2xl:1.7rem; /* 21px */
|
|
14
|
+
--text-3xl:1.625rem; --leading-3xl:1.95rem; /* 26px */
|
|
15
|
+
--text-4xl:2rem; --leading-4xl:2.25rem; /* 32px */
|
|
16
|
+
--text-5xl:2.5rem; --leading-5xl:1.05; /* 40px */
|
|
17
|
+
--text-6xl:3.25rem; --leading-6xl:1; /* 52px */
|
|
18
|
+
--text-7xl:4rem; --leading-7xl:1; /* 64px */
|
|
19
|
+
--text-8xl:5.25rem; --leading-8xl:1; /* 84px */
|
|
20
|
+
--text-9xl:7rem; --leading-9xl:1; /* 112px */
|
|
27
21
|
|
|
28
22
|
/* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
|
|
29
23
|
Hanzo Studio address the ramp as --font-size-*; both names are one value, so
|
|
@@ -88,5 +82,5 @@
|
|
|
88
82
|
form feels broken. pointer:coarse is the real signal — a desktop mouse keeps
|
|
89
83
|
the compact 13px field. */
|
|
90
84
|
@media (pointer:coarse){
|
|
91
|
-
:root{--text-control:
|
|
85
|
+
:root{--text-control:1rem}
|
|
92
86
|
}
|
package/dist/preference.d.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A person's own reading of the system: type size, density, accent.
|
|
3
|
-
*
|
|
4
|
-
* Three knobs, and each is ONE multiplier on a whole axis — never a restated
|
|
5
|
-
* ramp. The ramps live in `tokens/*.css`, authored once, and each rung carries
|
|
6
|
-
* its own `calc(<base> * var(--type-scale, 1))`. So a preference sets three
|
|
7
|
-
* numbers and every rung follows, including rungs added later and rungs this
|
|
8
|
-
* file has never heard of.
|
|
9
|
-
*
|
|
10
|
-
* That is not a style choice; it is the fix for a real bug. The first version of
|
|
11
|
-
* this module kept its own copy of the type ramp so it could recompute each
|
|
12
|
-
* rung, and the copy was WRONG — it had `lg: 1rem` and `xl: 1.125rem` (16px and
|
|
13
|
-
* 18px) while `tokens/typography.css` says `0.9375rem` and `1.0625rem` (15px and
|
|
14
|
-
* 17px). Setting a preference of 1 — "leave it alone" — would have silently
|
|
15
|
-
* resized two rungs of the published design. A second copy of a value is a
|
|
16
|
-
* second source of truth, and it drifted before anyone used it.
|
|
17
|
-
*
|
|
18
|
-
* Because the knobs are plain multipliers, any OTHER ramp can opt in the same
|
|
19
|
-
* way. @hanzo/gui compiles its own `--f-size-*` scale for the 1600-odd
|
|
20
|
-
* `fontSize="$n"` call sites in the apps; an app that redeclares those as
|
|
21
|
-
* `calc(<its px> * var(--type-scale, 1))` gets the same control with no change
|
|
22
|
-
* at scale 1.
|
|
23
|
-
*
|
|
24
|
-
* It is a pure function on purpose: it maps a preference to custom properties
|
|
25
|
-
* and returns them, touching no document. That is what lets an app, an embedded
|
|
26
|
-
* preview and a server render apply it identically.
|
|
27
|
-
*/
|
|
28
|
-
export type Density = "compact" | "default" | "comfortable";
|
|
29
|
-
export interface Preference {
|
|
30
|
-
/** Multiplier on the type ramp. 1 is the published scale. */
|
|
31
|
-
type?: number;
|
|
32
|
-
density?: Density;
|
|
33
|
-
/** A CSS colour for --primary / --accent. Rejected unless it is one. */
|
|
34
|
-
accent?: string;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* The type multiplier is CLAMPED, and the bounds are not arbitrary.
|
|
38
|
-
*
|
|
39
|
-
* Below 0.85 the smallest rung (--text-xs, 11px) drops under 9.4px, which stops
|
|
40
|
-
* being small and starts being unreadable — and a preference that lets someone
|
|
41
|
-
* render their own tools illegible is a trap, not a choice. Above 1.4 the
|
|
42
|
-
* chrome stops fitting its own containers: this app's builder header already
|
|
43
|
-
* overlaps its actions below 1440px at scale 1.
|
|
44
|
-
*/
|
|
45
|
-
export declare const TYPE_MIN = 0.85;
|
|
46
|
-
export declare const TYPE_MAX = 1.4;
|
|
47
|
-
/**
|
|
48
|
-
* Is this a colour, or is it something being smuggled into a style attribute?
|
|
49
|
-
*
|
|
50
|
-
* A preference is user input and its destination is CSS. `#fff`, `rgb(...)`,
|
|
51
|
-
* `oklch(...)` and the bare keywords are colours; anything carrying a `;`, a
|
|
52
|
-
* `}`, or a `url(` is trying to be a second declaration, and the answer is to
|
|
53
|
-
* drop the axis rather than to sanitise a string into something plausible.
|
|
54
|
-
*/
|
|
55
|
-
export declare function isColor(v: string): boolean;
|
|
56
|
-
/**
|
|
57
|
-
* The custom properties a preference produces.
|
|
58
|
-
*
|
|
59
|
-
* Only the axes actually set appear, so an app can spread the result over
|
|
60
|
-
* whatever it already has without a default silently overriding a brand.
|
|
61
|
-
*/
|
|
62
|
-
export declare function vars(p: Preference): Record<string, string>;
|
|
63
|
-
/** `vars()` as a declaration block, for a <style> tag or an SSR inline. */
|
|
64
|
-
export declare function css(p: Preference, selector?: string): string;
|
|
65
|
-
//# sourceMappingURL=preference.d.ts.map
|
package/dist/preference.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"preference.d.ts","sourceRoot":"","sources":["../src/preference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAE5D,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,QAAQ,MAAM,CAAC;AAqB5B;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAS1C;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmB1D;AAED,2EAA2E;AAC3E,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,SAAc,GAAG,MAAM,CAKjE"}
|
package/dist/preference.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A person's own reading of the system: type size, density, accent.
|
|
3
|
-
*
|
|
4
|
-
* Three knobs, and each is ONE multiplier on a whole axis — never a restated
|
|
5
|
-
* ramp. The ramps live in `tokens/*.css`, authored once, and each rung carries
|
|
6
|
-
* its own `calc(<base> * var(--type-scale, 1))`. So a preference sets three
|
|
7
|
-
* numbers and every rung follows, including rungs added later and rungs this
|
|
8
|
-
* file has never heard of.
|
|
9
|
-
*
|
|
10
|
-
* That is not a style choice; it is the fix for a real bug. The first version of
|
|
11
|
-
* this module kept its own copy of the type ramp so it could recompute each
|
|
12
|
-
* rung, and the copy was WRONG — it had `lg: 1rem` and `xl: 1.125rem` (16px and
|
|
13
|
-
* 18px) while `tokens/typography.css` says `0.9375rem` and `1.0625rem` (15px and
|
|
14
|
-
* 17px). Setting a preference of 1 — "leave it alone" — would have silently
|
|
15
|
-
* resized two rungs of the published design. A second copy of a value is a
|
|
16
|
-
* second source of truth, and it drifted before anyone used it.
|
|
17
|
-
*
|
|
18
|
-
* Because the knobs are plain multipliers, any OTHER ramp can opt in the same
|
|
19
|
-
* way. @hanzo/gui compiles its own `--f-size-*` scale for the 1600-odd
|
|
20
|
-
* `fontSize="$n"` call sites in the apps; an app that redeclares those as
|
|
21
|
-
* `calc(<its px> * var(--type-scale, 1))` gets the same control with no change
|
|
22
|
-
* at scale 1.
|
|
23
|
-
*
|
|
24
|
-
* It is a pure function on purpose: it maps a preference to custom properties
|
|
25
|
-
* and returns them, touching no document. That is what lets an app, an embedded
|
|
26
|
-
* preview and a server render apply it identically.
|
|
27
|
-
*/
|
|
28
|
-
/**
|
|
29
|
-
* The type multiplier is CLAMPED, and the bounds are not arbitrary.
|
|
30
|
-
*
|
|
31
|
-
* Below 0.85 the smallest rung (--text-xs, 11px) drops under 9.4px, which stops
|
|
32
|
-
* being small and starts being unreadable — and a preference that lets someone
|
|
33
|
-
* render their own tools illegible is a trap, not a choice. Above 1.4 the
|
|
34
|
-
* chrome stops fitting its own containers: this app's builder header already
|
|
35
|
-
* overlaps its actions below 1440px at scale 1.
|
|
36
|
-
*/
|
|
37
|
-
export const TYPE_MIN = 0.85;
|
|
38
|
-
export const TYPE_MAX = 1.4;
|
|
39
|
-
/**
|
|
40
|
-
* Density moves SPACING only, and its range is much tighter than type's.
|
|
41
|
-
*
|
|
42
|
-
* Spacing compounds: a page nests padding inside gap inside margin, so a 0.75
|
|
43
|
-
* multiplier is already three-quarters of every one of those in sequence. Below
|
|
44
|
-
* that, touch targets fall under the 44px floor `base.css` sets for coarse
|
|
45
|
-
* pointers, and the control that promised comfort takes it away.
|
|
46
|
-
*/
|
|
47
|
-
const DENSITY = {
|
|
48
|
-
compact: 0.85,
|
|
49
|
-
default: 1,
|
|
50
|
-
comfortable: 1.15,
|
|
51
|
-
};
|
|
52
|
-
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
53
|
-
/** Trim to 4dp so a multiplier cannot emit a 17-digit float into a stylesheet. */
|
|
54
|
-
const round = (n) => String(Math.round(n * 10000) / 10000);
|
|
55
|
-
/**
|
|
56
|
-
* Is this a colour, or is it something being smuggled into a style attribute?
|
|
57
|
-
*
|
|
58
|
-
* A preference is user input and its destination is CSS. `#fff`, `rgb(...)`,
|
|
59
|
-
* `oklch(...)` and the bare keywords are colours; anything carrying a `;`, a
|
|
60
|
-
* `}`, or a `url(` is trying to be a second declaration, and the answer is to
|
|
61
|
-
* drop the axis rather than to sanitise a string into something plausible.
|
|
62
|
-
*/
|
|
63
|
-
export function isColor(v) {
|
|
64
|
-
const s = v.trim();
|
|
65
|
-
if (!s || s.length > 64)
|
|
66
|
-
return false;
|
|
67
|
-
if (/[;{}()]/.test(s) && !/^(rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|color)\([^;{}]*\)$/i.test(s))
|
|
68
|
-
return false;
|
|
69
|
-
return (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(s) ||
|
|
70
|
-
/^(rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|color)\([^;{}]*\)$/i.test(s) ||
|
|
71
|
-
/^[a-z]{3,20}$/i.test(s));
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* The custom properties a preference produces.
|
|
75
|
-
*
|
|
76
|
-
* Only the axes actually set appear, so an app can spread the result over
|
|
77
|
-
* whatever it already has without a default silently overriding a brand.
|
|
78
|
-
*/
|
|
79
|
-
export function vars(p) {
|
|
80
|
-
const out = {};
|
|
81
|
-
if (typeof p.type === "number" && Number.isFinite(p.type)) {
|
|
82
|
-
out["--type-scale"] = round(clamp(p.type, TYPE_MIN, TYPE_MAX));
|
|
83
|
-
}
|
|
84
|
-
if (p.density && p.density in DENSITY) {
|
|
85
|
-
out["--density"] = round(DENSITY[p.density]);
|
|
86
|
-
}
|
|
87
|
-
if (p.accent && isColor(p.accent)) {
|
|
88
|
-
// Both names, because the ramp uses --primary for action surfaces and
|
|
89
|
-
// --accent for selection. One hue, stated once, landing on both.
|
|
90
|
-
out["--primary"] = p.accent.trim();
|
|
91
|
-
out["--accent"] = p.accent.trim();
|
|
92
|
-
}
|
|
93
|
-
return out;
|
|
94
|
-
}
|
|
95
|
-
/** `vars()` as a declaration block, for a <style> tag or an SSR inline. */
|
|
96
|
-
export function css(p, selector = "html:root") {
|
|
97
|
-
const v = vars(p);
|
|
98
|
-
const keys = Object.keys(v);
|
|
99
|
-
if (!keys.length)
|
|
100
|
-
return "";
|
|
101
|
-
return `${selector}{${keys.map((k) => `${k}:${v[k]}`).join(";")}}`;
|
|
102
|
-
}
|
package/scripts/check-dist.mjs
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Import the BUILT package the way a consumer does.
|
|
3
|
-
*
|
|
4
|
-
* Everything else in this repo checks the sources: the token files, the
|
|
5
|
-
* generator's output, the preference contract. All of it passed while
|
|
6
|
-
* `dist/index.js` was unimportable — 0.4.10 and 0.4.11 both shipped a barrel
|
|
7
|
-
* that re-exported `'./preference'` with no extension, which TypeScript emits
|
|
8
|
-
* verbatim and Node ESM refuses to resolve. `pnpm build` was green, `pnpm test`
|
|
9
|
-
* was green, and `import '@hanzo/design'` threw ERR_MODULE_NOT_FOUND for every
|
|
10
|
-
* consumer.
|
|
11
|
-
*
|
|
12
|
-
* A test that reads source cannot see that. This one loads the artifact.
|
|
13
|
-
*/
|
|
14
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
15
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
16
|
-
import { dirname, join } from 'node:path'
|
|
17
|
-
|
|
18
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
19
|
-
const root = join(here, '..')
|
|
20
|
-
|
|
21
|
-
let failed = 0
|
|
22
|
-
const check = async (name, fn) => {
|
|
23
|
-
try {
|
|
24
|
-
await fn()
|
|
25
|
-
console.log(` ok ${name}`)
|
|
26
|
-
} catch (e) {
|
|
27
|
-
failed++
|
|
28
|
-
console.error(` FAIL ${name}\n ${e.message}`)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
console.log('dist:')
|
|
33
|
-
|
|
34
|
-
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
35
|
-
|
|
36
|
-
await check('every export the package advertises actually exists on disk', () => {
|
|
37
|
-
const missing = []
|
|
38
|
-
for (const [name, entry] of Object.entries(pkg.exports ?? {})) {
|
|
39
|
-
for (const target of typeof entry === 'string' ? [entry] : Object.values(entry)) {
|
|
40
|
-
if (typeof target !== 'string' || target.includes('*')) continue
|
|
41
|
-
if (!existsSync(join(root, target))) missing.push(`${name} -> ${target}`)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (missing.length) throw new Error(`exports pointing at nothing:\n ${missing.join('\n ')}`)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
await check('the barrel IMPORTS — the check that 0.4.10 and 0.4.11 needed', async () => {
|
|
48
|
-
const entry = join(root, 'dist/index.js')
|
|
49
|
-
if (!existsSync(entry)) throw new Error('dist/index.js is missing — run the build first')
|
|
50
|
-
const mod = await import(pathToFileURL(entry).href)
|
|
51
|
-
// The preference API is the part that was unreachable; naming it here means a
|
|
52
|
-
// future re-export losing its extension fails by name rather than by silence.
|
|
53
|
-
for (const name of ['vars', 'css', 'isColor', 'TYPE_MIN', 'TYPE_MAX', 'cssVar', 'tokenValue']) {
|
|
54
|
-
if (mod[name] === undefined) throw new Error(`dist/index.js does not export ${name}`)
|
|
55
|
-
}
|
|
56
|
-
if (typeof mod.vars !== 'function') throw new Error('vars is not callable from the built package')
|
|
57
|
-
const v = mod.vars({ type: 1.2, density: 'compact' })
|
|
58
|
-
if (v['--type-scale'] !== '1.2') throw new Error(`built vars() returned ${JSON.stringify(v)}`)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
await check('no emitted module uses an extensionless relative specifier', () => {
|
|
62
|
-
// The class of bug, not just the one instance: Node ESM resolves relative
|
|
63
|
-
// specifiers literally, so a missing `.js` is always a runtime failure.
|
|
64
|
-
const bad = []
|
|
65
|
-
for (const [, entry] of Object.entries(pkg.exports ?? {})) {
|
|
66
|
-
const target = typeof entry === 'string' ? entry : entry.import
|
|
67
|
-
if (typeof target !== 'string' || target.includes('*') || !target.endsWith('.js')) continue
|
|
68
|
-
const file = join(root, target)
|
|
69
|
-
if (!existsSync(file)) continue
|
|
70
|
-
for (const m of readFileSync(file, 'utf8').matchAll(/from\s+['"](\.[^'"]*)['"]/g)) {
|
|
71
|
-
if (!/\.(js|mjs|cjs|json|css)$/.test(m[1])) bad.push(`${target}: ${m[1]}`)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (bad.length) throw new Error(`extensionless relative imports:\n ${bad.join('\n ')}`)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
if (failed) {
|
|
78
|
-
console.error(`\n${failed} dist check(s) failed`)
|
|
79
|
-
process.exit(1)
|
|
80
|
-
}
|
|
81
|
-
console.log('dist: all checks passed')
|