mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 04:20:39 +00:00
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
193 lines
6.8 KiB
TypeScript
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>
|
|
)
|
|
}
|