Files
zeekay 44626a40c3 chrome: the bar is glass on every page, and the beta strip is blade steel
The bar stood in the page's flow, clear at rest, so a first screen whose
ground is a picture or a light started below it, and the bar read as a
black band over the html's #000 ground: above the creative models'
paintings, the kit pages' glow and the lit product heroes. It is glass now
at rest and scrolled ($background/72 under the chrome blur; the scroll
listener is gone), and such a first screen spreads `under(pt)` from
components/shell/dock.ts: up by the bar's height, the same back on its top
padding, so its ground starts at the top of the window behind the glass and
no word moves. On Splash (the six creative models and Zen), PageHero, /edge,
/identity, /functions, /playground, /operative, /realtime and /enso. On
/beta the site's bar at rest keeps the glass and paints the focused pages'
fixed glow on it (components/shell/focus.tsx), so the light runs on
unbroken. 72% is kept: over Zen's painting the nav's ink holds 5.1:1 at .72
and falls to 3.7:1 at .6.

The beta strip is polished curved steel in CSS gradients: a faint top line,
a ridge 5px down, the flat dark under the words, a temper line and the
bright 1px edge at the bottom; one `/` light at 70 degrees, resting
mid-strip, moved by transform, brighter where it crosses the ridge and the
edge than under the words. The 1px/3px grain that read as stripes is gone,
and so is the root script that set --beta-tilt on <html> every pointer
frame on every page, restyling the whole document and ignoring reduced
motion. White words hold 5.4:1 against the brightest steel under them,
where the live strip measured 4.35:1 at 1440 and 3.45:1 on a phone.

Gates: e2e/gates/glass.spec.ts and e2e/gates/steel.spec.ts. The composer
gate reads its rest once the theme's colours have eased in: read straight
after the input appeared, the pill's border was a value on its way to its
token (rgba(137…), rgba(175…) on two CI runs) and failed the check that the
border keeps one value through focus.

Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
2026-10-22 12:02:12 -07:00

64 lines
3.1 KiB
TypeScript

/**
* The chrome writes its own measurements to `--header` and `--dock`.
*
* @hanzo/design leaves both at 0 for a surface to state, "usually from a
* measurement", and its base pads every scroll the browser makes for the page —
* an anchor jump, focus moving, `scrollIntoView` — by them
* (`scroll-padding-block`), so a target lands clear of the bar and the band.
*
* `--header` is the bar's height without the notch above it. `--dock` is the
* composer's band: it is `position: fixed` and takes no room in the page, and its
* height is not one number — a coarse pointer draws 44px controls, the apex
* composer sets its own control size, and the safe-area inset is part of the
* band's foot — so it is read off the element and follows it.
*
* Ref callbacks, so a measurement attaches and detaches with its element; a
* page that draws no band states no dock.
*/
/** The bar's height, under the notch. */
export const HEADER = 60
/** What the bar takes off the top of the window: its row and the notch above it. */
export const ROOF = `calc(${HEADER}px + env(safe-area-inset-top))` as const
/**
* A first screen whose ground runs up behind the bar. The bar is glass, but it
* stands in the page's flow, so a picture or a glow drawn from the top of the
* first section started below it and the bar read as a black band above the
* light. Spread on that section, these pull it up under the bar by `ROOF` and
* give the same back to its top padding: the ground starts at the top of the
* window and shows through the glass, and every word stays where it was.
* `pt` is the section's own top padding: px, a CSS length, or a gui space
* token, which gui compiles to `var(--c-space-N)` (@hanzo/ui/gui-config).
*/
export const under = (pt: number | string): { mt: `calc(${string})`; pt: `calc(${string})` } => ({
mt: `calc(-1 * ${ROOF})`,
pt: `calc(${typeof pt === 'number' ? `${pt}px` : pt.startsWith('$') ? `var(--c-space-${pt.slice(1)})` : pt} + ${ROOF})`,
})
/** The band's padding above the pill, in px. */
export const DOCK_TOP = 3
/** Its padding below: the safe-area inset, or 5px on a flat screen. */
export const DOCK_FOOT = 'max(5px, var(--safe-bottom))'
/** The fade the band draws above itself, so a line passing under it dissolves. */
export const DOCK_FADE = '56px'
/** Publish one of an element's heights to `name` on the document for as long as the element is mounted. */
function publish(name: '--header' | '--dock', box: 'content' | 'border', el: HTMLElement | null) {
if (!el) return
const root = document.documentElement.style
const size = new ResizeObserver(([entry]) => {
const height = box === 'content' ? entry.contentRect.height : el.offsetHeight
root.setProperty(name, `${height}px`)
})
size.observe(el)
return () => {
size.disconnect()
root.removeProperty(name)
}
}
/** The bar: its content box, because the notch above it is padding. */
export const header = (bar: HTMLElement | null) => publish('--header', 'content', bar)
/** The composer's band, paddings and all. */
export const dock = (band: HTMLElement | null) => publish('--dock', 'border', band)