@hanzo/design 0.4.7 → 0.4.8
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 +26 -0
- package/styles.css +14 -3
- package/tailwind.css +14 -3
- package/tokens/base.css +14 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/design",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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,6 +140,32 @@ 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. a focused element gets exactly ONE indicator ─────────────────────
|
|
144
|
+
// The field rule and the generic ring rule both compute to (0,1,0) — :where()
|
|
145
|
+
// zeroes its contents and each side keeps one pseudo-class — so the cascade
|
|
146
|
+
// falls through to SOURCE ORDER inside @layer base. The generic rule was
|
|
147
|
+
// written later, so it overrode the `outline:none` that the field rule states
|
|
148
|
+
// expressly to prevent it, and every focused input on every consumer drew the
|
|
149
|
+
// 2px ring AND the edge+halo. Both rules read as correct in isolation; only
|
|
150
|
+
// their order was wrong, which is why nobody saw it in either file.
|
|
151
|
+
//
|
|
152
|
+
// Order is not testable as intent, so this tests the property instead: a rule
|
|
153
|
+
// that paints an outline on focus must not be able to land on a field.
|
|
154
|
+
{
|
|
155
|
+
const base = strip(read(join(tokensDir, 'base.css')))
|
|
156
|
+
const FIELDS = 'input,select,textarea'
|
|
157
|
+
// `outline:none` disarms; anything else paints. `outline-offset` is a
|
|
158
|
+
// different property and never matches — the colon must follow `outline`.
|
|
159
|
+
const paints = (body) => /(?:^|[;{\s])outline\s*:\s*(?!none\b)[^;}]+/.test(body)
|
|
160
|
+
const scoped = (sel) => sel.includes(`:where(${FIELDS})`) || sel.includes(`:not(${FIELDS})`)
|
|
161
|
+
const doubled = [...base.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
|
|
162
|
+
.map(([, sel, body]) => ({ sel: sel.trim().replace(/\s+/g, ' '), body }))
|
|
163
|
+
.filter(({ sel, body }) => sel.includes(':focus-visible') && paints(body) && !scoped(sel))
|
|
164
|
+
doubled.length
|
|
165
|
+
? doubled.forEach(({ sel }) => fail(`\`${sel}\` paints an outline on a field that already draws its own edge + halo — two focus indicators`))
|
|
166
|
+
: pass('one focus indicator per element: fields brighten their edge, everything else rings')
|
|
167
|
+
}
|
|
168
|
+
|
|
143
169
|
// ── 2. every var() used inside the token layer must resolve ──────────────
|
|
144
170
|
{
|
|
145
171
|
const declared = new Set()
|
package/styles.css
CHANGED
|
@@ -838,8 +838,9 @@
|
|
|
838
838
|
Not the generic ring. A field already HAS an edge, so focus brightens that
|
|
839
839
|
edge (.15 -> .22) and adds a soft halo just outside it — which is what the
|
|
840
840
|
reference does (.composer-box:focus-within) and what separates a focused
|
|
841
|
-
field from a browser default.
|
|
842
|
-
|
|
841
|
+
field from a browser default. `outline:none` drops the UA's own focus ring,
|
|
842
|
+
which would otherwise draw a second, harder box around this one; the
|
|
843
|
+
generic ring below excludes fields for the same reason. */
|
|
843
844
|
:where(input,select,textarea):focus-visible{
|
|
844
845
|
outline:none;
|
|
845
846
|
border-color:var(--border-focus);
|
|
@@ -886,7 +887,17 @@
|
|
|
886
887
|
}
|
|
887
888
|
}
|
|
888
889
|
|
|
889
|
-
|
|
890
|
+
/* The generic ring — for everything that is NOT a field. The exclusion is
|
|
891
|
+
load-bearing, not tidiness. This rule and the control rule above both
|
|
892
|
+
compute to (0,1,0): :where() zeroes whatever it wraps, leaving one
|
|
893
|
+
pseudo-class on each side. Equal specificity inside one layer falls through
|
|
894
|
+
to source order, this rule sits 46 lines LATER, so it overrode the
|
|
895
|
+
`outline:none` written above to prevent precisely this — and every focused
|
|
896
|
+
input, select and textarea on every consumer drew BOTH the 2px ring and the
|
|
897
|
+
edge+halo. Two indicators, from the two rules that each say there is one.
|
|
898
|
+
Stating the exclusion in the selector fixes it by MEANING rather than by
|
|
899
|
+
position, so reordering either rule cannot bring it back. */
|
|
900
|
+
:where(:not(input,select,textarea)):focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
890
901
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
891
902
|
--selection, which BOTH themes define. */
|
|
892
903
|
::selection{background:var(--selection);color:var(--text-primary)}
|
package/tailwind.css
CHANGED
|
@@ -861,8 +861,9 @@
|
|
|
861
861
|
Not the generic ring. A field already HAS an edge, so focus brightens that
|
|
862
862
|
edge (.15 -> .22) and adds a soft halo just outside it — which is what the
|
|
863
863
|
reference does (.composer-box:focus-within) and what separates a focused
|
|
864
|
-
field from a browser default.
|
|
865
|
-
|
|
864
|
+
field from a browser default. `outline:none` drops the UA's own focus ring,
|
|
865
|
+
which would otherwise draw a second, harder box around this one; the
|
|
866
|
+
generic ring below excludes fields for the same reason. */
|
|
866
867
|
:where(input,select,textarea):focus-visible{
|
|
867
868
|
outline:none;
|
|
868
869
|
border-color:var(--border-focus);
|
|
@@ -909,7 +910,17 @@
|
|
|
909
910
|
}
|
|
910
911
|
}
|
|
911
912
|
|
|
912
|
-
|
|
913
|
+
/* The generic ring — for everything that is NOT a field. The exclusion is
|
|
914
|
+
load-bearing, not tidiness. This rule and the control rule above both
|
|
915
|
+
compute to (0,1,0): :where() zeroes whatever it wraps, leaving one
|
|
916
|
+
pseudo-class on each side. Equal specificity inside one layer falls through
|
|
917
|
+
to source order, this rule sits 46 lines LATER, so it overrode the
|
|
918
|
+
`outline:none` written above to prevent precisely this — and every focused
|
|
919
|
+
input, select and textarea on every consumer drew BOTH the 2px ring and the
|
|
920
|
+
edge+halo. Two indicators, from the two rules that each say there is one.
|
|
921
|
+
Stating the exclusion in the selector fixes it by MEANING rather than by
|
|
922
|
+
position, so reordering either rule cannot bring it back. */
|
|
923
|
+
:where(:not(input,select,textarea)):focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
913
924
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
914
925
|
--selection, which BOTH themes define. */
|
|
915
926
|
::selection{background:var(--selection);color:var(--text-primary)}
|
package/tokens/base.css
CHANGED
|
@@ -103,8 +103,9 @@
|
|
|
103
103
|
Not the generic ring. A field already HAS an edge, so focus brightens that
|
|
104
104
|
edge (.15 -> .22) and adds a soft halo just outside it — which is what the
|
|
105
105
|
reference does (.composer-box:focus-within) and what separates a focused
|
|
106
|
-
field from a browser default.
|
|
107
|
-
|
|
106
|
+
field from a browser default. `outline:none` drops the UA's own focus ring,
|
|
107
|
+
which would otherwise draw a second, harder box around this one; the
|
|
108
|
+
generic ring below excludes fields for the same reason. */
|
|
108
109
|
:where(input,select,textarea):focus-visible{
|
|
109
110
|
outline:none;
|
|
110
111
|
border-color:var(--border-focus);
|
|
@@ -151,7 +152,17 @@
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
|
|
155
|
+
/* The generic ring — for everything that is NOT a field. The exclusion is
|
|
156
|
+
load-bearing, not tidiness. This rule and the control rule above both
|
|
157
|
+
compute to (0,1,0): :where() zeroes whatever it wraps, leaving one
|
|
158
|
+
pseudo-class on each side. Equal specificity inside one layer falls through
|
|
159
|
+
to source order, this rule sits 46 lines LATER, so it overrode the
|
|
160
|
+
`outline:none` written above to prevent precisely this — and every focused
|
|
161
|
+
input, select and textarea on every consumer drew BOTH the 2px ring and the
|
|
162
|
+
edge+halo. Two indicators, from the two rules that each say there is one.
|
|
163
|
+
Stating the exclusion in the selector fixes it by MEANING rather than by
|
|
164
|
+
position, so reordering either rule cannot bring it back. */
|
|
165
|
+
:where(:not(input,select,textarea)):focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
155
166
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
156
167
|
--selection, which BOTH themes define. */
|
|
157
168
|
::selection{background:var(--selection);color:var(--text-primary)}
|