Files
zeekay cb6db9e659 design: every page and component outside the landing and beta paths is at zero lint
Inline style objects became gui props (Box for a div, View or Text rendering the
element for the rest, since Box styles only a div), raw colours and sizes became
the design's tokens, the brand swatches read @hanzo/design's own values, and the
project and creation hues moved to palette.json, which is data. The newsletter's
subscribe button is the standard filled Action. connect-steps.tsx, which nothing
imports, is deleted with the brand constants nobody read.

Pages compared before and after at 1440x900 and 390x844: blockchain, brand,
directory, projects, settings, unsubscribe identical; founder 0.03% of pixels;
buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's
button is now filled.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 03:49:42 -07:00

193 lines
6.8 KiB
TypeScript

'use client'
/**
* A FOCUSED PAGE, drawn once: the way in (/login, /signup, /forget, /join,
* /login/mfa), its return (/auth/*), and the first run (/onboarding). Under the
* shared bar (@hanzo/app `Chrome`: the wordmark home at the start, the account
* at the end) every one of them is the same page with different words:
*
* the light one soft glow from the top of the window (`Glow`), painted on the
* bar too and fixed to the window, so bar and page are one ground
* the column the sign-in card's measure (`MEASURE`), centred across the window
* and down the part of it under the bar
* the title pay's: 32px, 26px on a phone, the regular weight, -0.015em
* (`Title`), and the line under it 14px below (`Line`)
* the mark Hanzo's, centred at the foot of the window
*
* hanzo-inc/pay draws its plan picker and checkout with the same light and the
* same title, so a person walking from sign-in to payment sees one page change
* its words. A page passes words and its card; it states no type and no spacing.
*/
import type { ReactNode } from 'react'
import { Spinner, View, XStack, YStack } from '@hanzo/ui'
import { Display, Lede } from '@hanzo/ui/marketing'
import { HanzoLogo } from '@hanzo/logo/react'
import { HEADER, ROOF, under } from './dock'
/**
* The bar over a focused page (@hanzo/app `Chrome`), all of it: HEADER tall,
* its 1px hairline under that, and the notch above. Stated, not read off
* `--header`: design sets that to 0 until the bar has mounted and measured
* itself, so a page sized from it was a bar's height too tall in the export's
* first paint, off centre by half that and scrolling, until the script ran.
*/
export const BAR_HEIGHT = `calc(${HEADER + 1}px + env(safe-area-inset-top, 0px))`
/** The sign-in card's width (components/auth/steps `Sheet`): every column of the flow stands at it. */
export const MEASURE = 420
/** Every control on these pages is pay's (hanzo-inc/pay `.pay-cta`): a 56px pill at 1rem. The fields are the card's (components/auth/steps `Sheet`). */
export const LARGE = { minH: 56, fontSize: '1rem' } as const
/**
* The light's ink at its brightest: white over the dark ground, where design's
* 8% rung is a soft lamp, and over the light ground the page's own white above
* design's #f7f7f7. `light-dark()` follows the `color-scheme` the theme sets.
*/
const LAMP = 'light-dark(var(--pure-white), var(--white-08))'
/** The glow: an ellipse from the top centre of the window, gone by 70% of its reach. */
export const GLOW = `radial-gradient(ellipse 55% 100% at 50% 0%, ${LAMP}, transparent 70%)`
/** How far down the window the glow reaches. */
export const GLOW_HEIGHT = 560
/**
* The page's ground: the design's background under the glow, behind everything
* in the section it is the first child of (a positioned section; what follows it
* is drawn over it). The section starts behind the bar (`under`), and the ground
* is fixed to the window, so the bar is the page's own light, never a band.
*/
export function Glow() {
return (
<>
<View
aria-hidden
data-slot="glow"
position="absolute"
inset={0}
pointerEvents="none"
bg="var(--background)"
backgroundImage={GLOW}
backgroundSize={`100% ${GLOW_HEIGHT}px`}
backgroundRepeat="no-repeat"
backgroundPosition="top"
backgroundAttachment="fixed"
/>
</>
)
}
/** The page's title: pay's, centred. `id` names it for the section that it heads. */
export function Title({ id, children }: { id?: string; children: ReactNode }) {
return (
<Display
id={id}
width="100%"
text="center"
fontSize="var(--text-4xl)"
lineHeight="var(--leading-4xl)"
fontWeight="400"
letterSpacing="-0.015em"
$max-sm={{ fontSize: 'var(--text-3xl)', lineHeight: 'var(--leading-3xl)' }}
>
{children}
</Display>
)
}
/** The line under the title: 15px in the quiet ink, as pay's picker says its subtitle. */
export function Line({ children }: { children: ReactNode }) {
return (
<Lede m={0} width="100%" text="center" fontSize="var(--text-lg)" lineHeight="var(--leading-lg)" textWrap="balance">
{children}
</Lede>
)
}
/**
* The page: a section at least the window under the bar tall, its ground the
* light, its column at `width` (the measure, or wider for a row of cards) in the
* middle across and down, and the mark at the foot. The padding above and below
* is equal, so the column's middle is the window's; the foot's padding is what
* keeps the column clear of the mark.
*
* `title` and `line` head the column; `slot` and `id` name the section and its
* title for a page that is found by them (the sign-in's `hero`, `hero-title`).
*/
export function Focus({
label,
title,
line,
id,
slot = 'focus',
width = MEASURE,
children,
}: {
label?: string
title?: ReactNode
line?: ReactNode
id?: string
slot?: string
width?: number
children?: ReactNode
}) {
const named = title && id ? { 'aria-labelledby': id } : { 'aria-label': label }
return (
<YStack
render="section"
data-slot={slot}
{...named}
position="relative"
width="100%"
// The ground starts at the top of the window, behind the bar (`under`); the column stays where it was.
minH={`calc(100svh - ${BAR_HEIGHT} + ${ROOF})`}
items="center"
justify="center"
px="var(--page-gutter)"
pb={88}
{...under(88)}
>
<Glow />
<YStack data-slot="column" position="relative" z={1} width="100%" maxW={width} items="stretch" gap="$6">
{title ? (
<YStack items="center" gap="$3.5">
<Title id={id}>{title}</Title>
{line ? <Line>{line}</Line> : null}
</YStack>
) : null}
{children}
</YStack>
<View data-slot="mark" position="absolute" b={36} l={0} r={0} items="center" $platform-web={{ color: 'var(--muted-foreground)' }}>
<XStack
render={<a href="https://hanzo.ai" aria-label="Hanzo" />}
items="center"
justify="center"
opacity={0.8}
cursor="pointer"
hoverStyle={{ opacity: 1, scale: 1.05 }}
$platform-web={{ display: 'inline-flex', color: 'inherit', transition: 'opacity 0.15s ease, transform 0.15s ease' }}
>
<HanzoLogo size={28} />
</XStack>
</View>
</YStack>
)
}
/** A page on its way somewhere: the spinner over the title, nothing to press. */
export function Wait({ title, line }: { title: string; line?: ReactNode }) {
return (
<Focus label={title}>
<YStack role="status" items="center" gap="$6">
<Spinner size={28} color="var(--muted-foreground)" />
<YStack items="center" gap="$3.5" width="100%">
<Title>{title}</Title>
{line ? <Line>{line}</Line> : null}
</YStack>
</YStack>
</Focus>
)
}