Files
hanzo.ai/scripts/gen-gui-css.mjs
zooqueen 69dc396015 one token table for the fleet, and a thumb-sized box on every control
**The config.** This site carried 244 lines of `createGui` on
`@hanzogui/config/v4` while the fleet had moved to v5, and it disagreed with the
canon on nearly every rung it defined — `$4` 14 against 15, `$9` 32 against 26,
`$12` 64 against 48, `$14` 112 against 64, and a spacing ramp reading `$N = 4N`
against a t-shirt scale. That is not a preference two surfaces may hold
separately: every `@hanzo/ui` component asks the HOST's table for its sizes, so
one library rendered at two sizes depending on which site it was on, and nothing
said so. `lib/gui.ts` is one line onto `@hanzo/ui/gui-config`, the same object
hanzo.app mounts.

Its 63 call sites were renamed to the canon rung of the SAME pixel — `$6`→`$5`
is 24px both times, `$9`→`$10` is 32px, `borderRadius="$9"`→`"$10"` is the pill —
so the swap moved no type and no gap. Measured before and after at 390, 834 and
1440: identical H1s (32/40/40), identical shell, no overflow either side. What
moved is who decides.

MONOCHROME, which is a brand fact: this site renders no hue but the WebGL globe,
which owes gui nothing. `monochrome` is the same table minus the eight chromatic
families — a theme table is data that ships, since a theme is chosen by string at
run time — and the sheet is 88KB rather than 356KB.

`css()` from the package emits it, never `getCSS()`. gui declares `--background`,
`--black` and `--white` on its root themes, which ties design's `:root` and wins
on load order; the raw sheet shipped gui's grey over design's black and nothing
in this repo could reach it. @hanzo/ui 8.0.82 also stopped gui asking the OS for
the theme, so the 124 `prefers-color-scheme` blocks are gone — the class on
`<html>` is the one authority.

**The material.** `@hanzo/ui/glass.css`, which is the frosted surfaces, the row
separators and the PAPER ladder — `.elevation-1/2` over @hanzo/design's
`--shadow-sheet-*`, and `.fold`. Not `theme.css`: that inlines `base.css`
unlayered, and bare element selectors outrank every layered utility, which is
how the header CTA once rendered white-on-white here.

**The box.** A tap target is a BOX and the floor applies to the SMALLER SIDE.
The rule set `min-height` only, and scoped the width to `:is(header, footer)` —
so the composer's three controls, the one place on this homepage a thumb
actually goes, rendered 30×30 at 390 and 834 while the rule claimed 44. It
carries `!important` now, which is load-bearing rather than lazy: gui writes a
control's size INLINE, and an inline declaration beats a stylesheet rule at any
specificity, so those three were `min-height:30px;min-width:30px` and the polite
version lost to them. Two standalone links wear `.hz-tap`, the mark that means
"this is a control", because a link inside a paragraph is not one.
Measured: 4 controls under the floor at 390 and 5 at 834, now zero at both.

**The gate.** `pnpm lint:design` runs @hanzo/design's twelve rules — eight about
writing a value that should have been a token, four about owning a layer that
should have been imported — with `hanzo-design.allow.json` as the ratchet: what
this site still owes, per rule, shrinking only. `local-token-table` and
`second-publisher` are absent from that file, which means zero.
2026-08-15 09:52:38 -07:00

65 lines
3.3 KiB
JavaScript

// Regenerate app/gui.css — the whole @hanzo/gui atomic sheet, as a real static asset.
//
// gui builds its atomic CSS as components are constructed, so the sheet only exists
// once something has rendered. The obvious way to get it into a statically exported
// document is useServerInsertedHTML — and that was the bug: Next runs that callback
// on EVERY streaming flush and getCSS() returns the entire accumulated sheet each
// time, so hanzo.ai's homepage shipped thirteen byte-identical copies, 591,305 of
// its 638,443 bytes. Inline, it is also uncacheable and re-sent on every navigation.
//
// Generating it here instead makes it an ordinary stylesheet: Next fingerprints it,
// the browser caches it once, and every page after the first pays nothing. GuiProvider
// renders with disableInjectCSS so this file is the ONE source of gui styles.
//
// Wired to `prebuild`, so it cannot drift from lib/gui.ts.
//
// IT IS `css()`, NEVER `config.getCSS()`. gui declares every theme key as a bare
// `--<key>` on its root themes, and three of those names — background, black,
// white — are @hanzo/design's. `:root.t_dark` is (0,2,0) and beats design outright;
// plain `:root` ties it and wins on load order, because this sheet is imported
// last. So gui's grey shipped over design's black and no CSS in this repo could
// reach it. `css()` in @hanzo/ui/gui-config drops exactly those declarations from
// exactly the ROOT themes (a nested `.t_dark_Button` legitimately scopes its own
// ground and is left alone); the TOKEN survives, `$background` still compiles to
// `var(--background)`, and that var now reads design's value from the cascade —
// so the ground follows the brand, the theme and any runtime retune.
//
// The prune lives in the package because every app runs a generator of its own,
// and a copy per app is how one of them keeps shipping the shadowed sheet. This
// script's whole job is to call it and write the bytes.
import { execFileSync } from 'node:child_process'
import { writeFileSync, rmSync } from 'node:fs'
const bundle = new URL('../.guicfg.gen.mjs', import.meta.url)
// lib/gui.ts is TypeScript and pulls in the @hanzogui graph, so it has to be
// bundled before Node can import it. react-native is aliased to react-native-web
// for the same reason the Next build does it: the bare specifier resolves to the
// native implementation, which has no business running here.
execFileSync(
'esbuild',
[
'lib/gui.ts',
'--bundle',
'--format=esm',
'--platform=node',
'--external:react',
'--external:react-dom',
'--external:react-native',
'--alias:react-native=react-native-web',
`--outfile=${bundle.pathname}`,
'--log-level=error',
],
{ stdio: 'inherit', cwd: new URL('..', import.meta.url).pathname },
)
const { css } = await import(bundle)
// The banner is not decoration: `hanzo-design-lint` skips a file whose head says
// it is generated, and without it the whole gui sheet is linted as source — a
// build product reported for raw colour, which is an instruction to hand-edit
// the output of a generator.
const sheet = `/* AUTO-GENERATED by scripts/gen-gui-css.mjs from lib/gui.ts — DO NOT EDIT. */\n${css()}`
writeFileSync(new URL('../app/gui.css', import.meta.url), sheet)
rmSync(bundle)
console.log(`app/gui.css regenerated — ${sheet.length.toLocaleString()} bytes`)