mirror of
https://github.com/hanzoai/design.git
synced 2026-10-30 14:58:37 +00:00
A CSS custom property that nothing declares paints NOTHING, and it does it
silently: no error, no warning, no failed build. Every defect below was live
under that cover.
THE DEFECT. @hanzo/iam's UserMenu painted with var(--surface-1)/var(--shadow-lg).
Neither was a token. Reproduced in Chromium against published 0.2.0: the menu's
computed background-color is rgba(0, 0, 0, 0) — transparent. Both names are
@hanzo/brand's vocabulary, so the component was addressing the wrong layer.
- --surface-0..3 now alias --background/--card/--muted/--secondary, so a brand
fork that retunes its canvases retunes the ladder with it and light theme
inverts for free.
- --shadow-sm/--shadow/--shadow-md/--shadow-lg/--shadow-xl/--shadow-2xl.
Alphas are heavier than Tailwind's: on a near-black ground a 10% black drop
is invisible. --shadow-2xl and --shadow-floating are one rung.
- --font-size-* alias --text-* (identical values, @hanzo/gui's spelling) and
--z-tooltip/--z-notification alias the top two rungs (@hanzo/brand's).
Both vocabularies now resolve to one set of values.
ROOT CAUSE. cssVar's signature was `CssVarName | (string & {})` — the union arm
opted out of the check, so cssVar('surface-1') type-checked clean. Removed: a
name that is not a token is now a compile error. cssVar also emits the token's
authored literal as a fallback, so a reference still paints where the CSS layer
was never loaded. injectDesignCss no longer defaults to esm.sh, which quietly
made a third-party CDN the origin of the whole token layer.
ACCESSIBILITY, fixed at the token layer so no surface has to invent a value
again (hanzoai/id and hanzoai/console each already had). --ring was #333333 =
1.66:1 on --background; base.css ships :focus-visible{outline:2px solid
var(--ring)} to every consumer, so the system shipped a non-conformant focus
indicator by default. --border-strong was 2.03:1 and could not carry a control
boundary. Both are now --neutral-500, the only rung clearing 3:1 on every
surface in BOTH themes (solved across all 8 canvases x 3 brands; worst case
3.59:1). Boundaries are now documented as two kinds: decorative (--border, no
ratio required) and perceivable (--border-strong, >= 3:1, WCAG 1.4.11).
WHITE-LABEL. Measured on each fork's own palette, both themes, 1440 and 390:
10 pairings FIXED, 2 unchanged, 0 regressions. Zoo declares its own magenta
--ring in both themes and keeps it (4.61:1 dark / 4.29:1 light); it inherits the
--border-strong fix (1.64 -> 3.59:1). Lux tracks Hanzo exactly.
GEIST IS NOW SELF-HOSTED — two variable woff2 in assets/fonts, 141 KB, SIL
OFL-1.1. tokens/fonts.css @import-ed fonts.googleapis.com, which is why
hanzoai/id refused to load it on the sign-in path and took the colours without
the typeface. Verified: both faces load, zero third-party requests. base.css
also carried font-family:var(--font-sans) with no fallback, so a surface that
skipped fonts.css rendered in Times — every font-family now carries its stack
as a literal fallback.
REGRESSIONS RESTORED. This tree had dropped tokens/z.css from styles.css, so the
ladder was authored, exported, documented and never served; and had reverted the
type scale to a 16px base. Both are back to what 0.2.0 published.
ONE SOURCE OF TRUTH. Deleted tokens/{colors,radii,spacing,themes,typography}.ts
— an orphaned second token API, shipped under ./tokens/*, contradicting the CSS
(radii.ts lg = 8px vs --radius-lg 12px; colors.ts a zinc/oklch palette, not the
neutral ladder). Nothing imported them. src/tokens.gen.ts, generated from the
CSS, is the only programmatic source.
THE GATE. scripts/check-tokens.mjs runs on every build and fails if a token file
is not served by styles.css, if any internal var() does not resolve, or if
--ring/--border-strong drop below 3:1. Verified it catches both original
defects. probe.html renders the menu, a control and a tooltip from tokens alone,
for measuring resolution in a browser rather than asserting declaration.
220 tokens, up from 191. Additive except --ring and --border-strong, whose
values change by design; --border-strong is referenced in no consumer today.
Co-authored-by: Hanzo Dev <[email protected]>
56 lines
2.9 KiB
HTML
56 lines
2.9 KiB
HTML
<!doctype html>
|
|
<html><head><meta charset="utf-8">
|
|
<link rel="stylesheet" href="./styles.css">
|
|
<style>
|
|
main{padding:24px;display:flex;flex-direction:column;gap:20px;max-width:900px}
|
|
.row{display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start}
|
|
/* The @hanzo/iam UserMenu shape, painted ONLY from design tokens. */
|
|
#menu{width:260px;background:var(--surface-1);border:1px solid var(--border);
|
|
border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);
|
|
z-index:var(--z-popover);overflow:hidden}
|
|
#menu .hd{padding:12px 14px;border-bottom:1px solid var(--border)}
|
|
#menu .nm{color:var(--text-primary);font-size:var(--text-sm)}
|
|
#menu .em{color:var(--text-helper);font-size:var(--text-xs)}
|
|
#menu .it{padding:10px 14px;color:var(--foreground);font-size:var(--text-sm)}
|
|
#menu .it:hover{background:var(--surface-3)}
|
|
/* A control whose boundary IS the affordance → --border-strong */
|
|
#field{width:260px;padding:9px 12px;background:var(--surface-0);
|
|
border:1px solid var(--border-strong);border-radius:var(--radius-sm);
|
|
color:var(--foreground);font-size:var(--text-sm);font-family:var(--font-sans)}
|
|
#tip{padding:6px 10px;background:var(--surface-2);border:1px solid var(--border);
|
|
border-radius:var(--radius-sm);z-index:var(--z-tooltip);font-size:var(--text-xs)}
|
|
</style></head>
|
|
<body>
|
|
<main>
|
|
<h1>@hanzo/design token probe</h1>
|
|
<div class="row">
|
|
<div id="menu">
|
|
<div class="hd"><div class="nm">Antje Worring</div><div class="em">[email protected]</div></div>
|
|
<div class="it">Account</div><div class="it">Organisation</div><div class="it">Sign out</div>
|
|
</div>
|
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
<input id="field" value="focus me">
|
|
<div id="tip">tooltip on the ladder</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<script>
|
|
window.__probe = () => {
|
|
const cs = (el, p) => getComputedStyle(el).getPropertyValue(p).trim()
|
|
const root = getComputedStyle(document.documentElement)
|
|
const tok = n => root.getPropertyValue(n).trim()
|
|
const m = document.getElementById('menu'), f = document.getElementById('field'), t = document.getElementById('tip')
|
|
return {
|
|
tokens: Object.fromEntries(['--surface-0','--surface-1','--surface-2','--surface-3','--shadow-lg',
|
|
'--z-popover','--z-tooltip','--z-dropdown','--ring','--border-strong','--border',
|
|
'--font-sans','--text-base','--font-size-base','--radius-lg'].map(n=>[n,tok(n)])),
|
|
menu: { background: cs(m,'background-color'), border: cs(m,'border-top-width')+' '+cs(m,'border-top-style')+' '+cs(m,'border-top-color'),
|
|
radius: cs(m,'border-top-left-radius'), shadow: cs(m,'box-shadow'), z: cs(m,'z-index') },
|
|
field: { border: cs(f,'border-top-width')+' '+cs(f,'border-top-style')+' '+cs(f,'border-top-color'), font: cs(f,'font-family') },
|
|
tip: { z: cs(t,'z-index') },
|
|
body: { font: cs(document.body,'font-family'), size: cs(document.body,'font-size'), bg: cs(document.body,'background-color') },
|
|
}
|
|
}
|
|
</script>
|
|
</body></html>
|