Files
design/probe.html
zooqueenandHanzo Dev 4e783dff7d design: complete the token layer — resolve every referenced name, clear 3:1, self-host Geist (0.3.0)
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]>
2026-07-27 17:27:14 -07:00

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>