@hanzo/design 0.4.8 → 0.4.9
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/package.json +1 -1
- package/scripts/check-tokens.mjs +28 -20
- package/styles.css +34 -25
- package/tailwind.css +34 -25
- package/tokens/base.css +34 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/design",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"packageManager": "pnpm@11.17.0",
|
|
5
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.",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
package/scripts/check-tokens.mjs
CHANGED
|
@@ -140,30 +140,38 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
140
140
|
: fail('base.css is UNLAYERED — its element rules outrank every utility an app writes')
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
// ── 1d.
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
143
|
+
// ── 1d. exactly ONE rule decides focus ───────────────────────────────────
|
|
144
|
+
// Until 0.4.9 there were two: a field rule that suppressed the outline and drew
|
|
145
|
+
// a brightened edge + halo, and the generic ring. Both computed to (0,1,0) —
|
|
146
|
+
// :where() zeroes its contents and each side keeps one pseudo-class — so the
|
|
147
|
+
// cascade fell through to SOURCE ORDER inside @layer base, the generic rule was
|
|
148
|
+
// written later, and it overrode the `outline:none` the field rule stated
|
|
149
|
+
// expressly to prevent it. Every focused input on every consumer drew BOTH.
|
|
150
|
+
// Each rule read as correct alone; the defect existed only in their order,
|
|
151
|
+
// which is why it survived review in both files.
|
|
151
152
|
//
|
|
152
|
-
// Order is not testable as intent, so this tests the property
|
|
153
|
-
//
|
|
153
|
+
// Order is not testable as intent, so this tests the property that replaced it:
|
|
154
|
+
// one rule paints the indicator, and nothing else touches focus. A second rule
|
|
155
|
+
// is how they disagree, `outline:none` is how an indicator disappears, and
|
|
156
|
+
// box-shadow is how a second one appears — none of the three can return quietly.
|
|
154
157
|
{
|
|
155
158
|
const base = strip(read(join(tokensDir, 'base.css')))
|
|
156
|
-
|
|
157
|
-
// `outline
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const scoped = (sel) => sel.includes(`:where(${FIELDS})`) || sel.includes(`:not(${FIELDS})`)
|
|
161
|
-
const doubled = [...base.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
|
|
159
|
+
// `outline-offset` is a different property and never matches: the colon must
|
|
160
|
+
// follow `outline` itself.
|
|
161
|
+
const OUTLINE = /(?:^|[;{\s])outline\s*:\s*([^;}]+)/
|
|
162
|
+
const rules = [...base.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
|
|
162
163
|
.map(([, sel, body]) => ({ sel: sel.trim().replace(/\s+/g, ' '), body }))
|
|
163
|
-
.filter(({ sel
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
.filter(({ sel }) => sel.includes(':focus-visible'))
|
|
165
|
+
|
|
166
|
+
const paints = rules.filter(({ body }) => { const m = body.match(OUTLINE); return m && m[1].trim() !== 'none' })
|
|
167
|
+
const mutes = rules.filter(({ body }) => { const m = body.match(OUTLINE); return m && m[1].trim() === 'none' })
|
|
168
|
+
const halos = rules.filter(({ body }) => /(?:^|[;{\s])box-shadow\s*:/.test(body))
|
|
169
|
+
|
|
170
|
+
paints.length === 1
|
|
171
|
+
? pass(`one focus indicator, one rule: \`${paints[0].sel}\``)
|
|
172
|
+
: fail(`${paints.length} rules paint a focus outline (${paints.map((r) => r.sel).join(' / ')}) — equal specificity inside @layer base, so source order decides which one a user actually sees`)
|
|
173
|
+
mutes.forEach(({ sel }) => fail(`\`${sel}\` sets outline:none — it removes the focus indicator instead of replacing it`))
|
|
174
|
+
halos.forEach(({ sel }) => fail(`\`${sel}\` adds a box-shadow on focus — a second indicator beside the ring`))
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
// ── 2. every var() used inside the token layer must resolve ──────────────
|
package/styles.css
CHANGED
|
@@ -834,20 +834,26 @@
|
|
|
834
834
|
padding:0 var(--space-3);
|
|
835
835
|
}
|
|
836
836
|
:where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
|
|
837
|
-
/*
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
837
|
+
/* A field has NO focus rule of its own — see the ring at the bottom of this
|
|
838
|
+
layer, which is the one focus indicator for everything.
|
|
839
|
+
|
|
840
|
+
There used to be one here: `outline:none` plus a brightened edge (.15 ->
|
|
841
|
+
.22) plus a soft halo, the composer look. It was removed because it could
|
|
842
|
+
not do the job in either of the two ways that matter.
|
|
843
|
+
|
|
844
|
+
It was never VISIBLE ENOUGH. Composited on --background the brightened edge
|
|
845
|
+
measures 1.91:1 and the halo 1.25:1, against the 3:1 that WCAG 1.4.11 asks
|
|
846
|
+
of a focus indicator and that this package already gates --ring on. The
|
|
847
|
+
budget was documented as being spent entirely on --ring "because that is
|
|
848
|
+
what a keyboard user navigates by" — true for a button, and false for a
|
|
849
|
+
field for exactly as long as this rule told fields not to use it.
|
|
850
|
+
|
|
851
|
+
And it was SUPPRESSIBLE. It carried the indicator on `border-color`, so any
|
|
852
|
+
app that states `border` on its own fields overrode it — @hanzo/id does,
|
|
853
|
+
unlayered, which beats this layer whatever its specificity, and its focused
|
|
854
|
+
fields sat at the resting .15 while both files read as correct. An outline
|
|
855
|
+
is not a border: nothing in an app's field styling reaches it, so the ring
|
|
856
|
+
paints whether or not the app has opinions about edges. */
|
|
851
857
|
:where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
|
|
852
858
|
:where(input,textarea)::placeholder{color:var(--text-disabled)}
|
|
853
859
|
:where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
|
|
@@ -887,17 +893,20 @@
|
|
|
887
893
|
}
|
|
888
894
|
}
|
|
889
895
|
|
|
890
|
-
/*
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
896
|
+
/* THE focus indicator. One rule, every focusable thing, no exceptions — a
|
|
897
|
+
button, a link, a summary, a field. 2px at --ring is 3.77:1 on the darkest
|
|
898
|
+
canvas and clears the 2px perimeter WCAG 2.4.13 asks for; the gate in
|
|
899
|
+
check-tokens holds --ring to that and nothing else here may weaken it.
|
|
900
|
+
|
|
901
|
+
There were two rules until 0.4.9, and they collided invisibly. Both computed
|
|
902
|
+
to (0,1,0) — :where() zeroes whatever it wraps, leaving one pseudo-class on
|
|
903
|
+
each side — so the cascade fell through to SOURCE ORDER inside this layer,
|
|
904
|
+
this rule was written later, and it overrode the `outline:none` the field
|
|
905
|
+
rule stated expressly to prevent it. Every focused input on every consumer
|
|
906
|
+
drew BOTH the ring and the edge+halo. Each rule read as correct alone, which
|
|
907
|
+
is why it survived review in both files; the defect existed only in their
|
|
908
|
+
order. One rule cannot disagree with itself. */
|
|
909
|
+
:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
901
910
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
902
911
|
--selection, which BOTH themes define. */
|
|
903
912
|
::selection{background:var(--selection);color:var(--text-primary)}
|
package/tailwind.css
CHANGED
|
@@ -857,20 +857,26 @@
|
|
|
857
857
|
padding:0 var(--space-3);
|
|
858
858
|
}
|
|
859
859
|
:where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
|
|
860
|
-
/*
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
860
|
+
/* A field has NO focus rule of its own — see the ring at the bottom of this
|
|
861
|
+
layer, which is the one focus indicator for everything.
|
|
862
|
+
|
|
863
|
+
There used to be one here: `outline:none` plus a brightened edge (.15 ->
|
|
864
|
+
.22) plus a soft halo, the composer look. It was removed because it could
|
|
865
|
+
not do the job in either of the two ways that matter.
|
|
866
|
+
|
|
867
|
+
It was never VISIBLE ENOUGH. Composited on --background the brightened edge
|
|
868
|
+
measures 1.91:1 and the halo 1.25:1, against the 3:1 that WCAG 1.4.11 asks
|
|
869
|
+
of a focus indicator and that this package already gates --ring on. The
|
|
870
|
+
budget was documented as being spent entirely on --ring "because that is
|
|
871
|
+
what a keyboard user navigates by" — true for a button, and false for a
|
|
872
|
+
field for exactly as long as this rule told fields not to use it.
|
|
873
|
+
|
|
874
|
+
And it was SUPPRESSIBLE. It carried the indicator on `border-color`, so any
|
|
875
|
+
app that states `border` on its own fields overrode it — @hanzo/id does,
|
|
876
|
+
unlayered, which beats this layer whatever its specificity, and its focused
|
|
877
|
+
fields sat at the resting .15 while both files read as correct. An outline
|
|
878
|
+
is not a border: nothing in an app's field styling reaches it, so the ring
|
|
879
|
+
paints whether or not the app has opinions about edges. */
|
|
874
880
|
:where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
|
|
875
881
|
:where(input,textarea)::placeholder{color:var(--text-disabled)}
|
|
876
882
|
:where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
|
|
@@ -910,17 +916,20 @@
|
|
|
910
916
|
}
|
|
911
917
|
}
|
|
912
918
|
|
|
913
|
-
/*
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
919
|
+
/* THE focus indicator. One rule, every focusable thing, no exceptions — a
|
|
920
|
+
button, a link, a summary, a field. 2px at --ring is 3.77:1 on the darkest
|
|
921
|
+
canvas and clears the 2px perimeter WCAG 2.4.13 asks for; the gate in
|
|
922
|
+
check-tokens holds --ring to that and nothing else here may weaken it.
|
|
923
|
+
|
|
924
|
+
There were two rules until 0.4.9, and they collided invisibly. Both computed
|
|
925
|
+
to (0,1,0) — :where() zeroes whatever it wraps, leaving one pseudo-class on
|
|
926
|
+
each side — so the cascade fell through to SOURCE ORDER inside this layer,
|
|
927
|
+
this rule was written later, and it overrode the `outline:none` the field
|
|
928
|
+
rule stated expressly to prevent it. Every focused input on every consumer
|
|
929
|
+
drew BOTH the ring and the edge+halo. Each rule read as correct alone, which
|
|
930
|
+
is why it survived review in both files; the defect existed only in their
|
|
931
|
+
order. One rule cannot disagree with itself. */
|
|
932
|
+
:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
924
933
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
925
934
|
--selection, which BOTH themes define. */
|
|
926
935
|
::selection{background:var(--selection);color:var(--text-primary)}
|
package/tokens/base.css
CHANGED
|
@@ -99,20 +99,26 @@
|
|
|
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
|
-
|
|
102
|
+
/* A field has NO focus rule of its own — see the ring at the bottom of this
|
|
103
|
+
layer, which is the one focus indicator for everything.
|
|
104
|
+
|
|
105
|
+
There used to be one here: `outline:none` plus a brightened edge (.15 ->
|
|
106
|
+
.22) plus a soft halo, the composer look. It was removed because it could
|
|
107
|
+
not do the job in either of the two ways that matter.
|
|
108
|
+
|
|
109
|
+
It was never VISIBLE ENOUGH. Composited on --background the brightened edge
|
|
110
|
+
measures 1.91:1 and the halo 1.25:1, against the 3:1 that WCAG 1.4.11 asks
|
|
111
|
+
of a focus indicator and that this package already gates --ring on. The
|
|
112
|
+
budget was documented as being spent entirely on --ring "because that is
|
|
113
|
+
what a keyboard user navigates by" — true for a button, and false for a
|
|
114
|
+
field for exactly as long as this rule told fields not to use it.
|
|
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. */
|
|
116
122
|
:where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
|
|
117
123
|
:where(input,textarea)::placeholder{color:var(--text-disabled)}
|
|
118
124
|
:where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
|
|
@@ -152,17 +158,20 @@
|
|
|
152
158
|
}
|
|
153
159
|
}
|
|
154
160
|
|
|
155
|
-
/*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
166
175
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
167
176
|
--selection, which BOTH themes define. */
|
|
168
177
|
::selection{background:var(--selection);color:var(--text-primary)}
|