Files
zeekay 69dd4723f9 landing: one type scale, a device drags 1:1, the story never stops, and an appearance chip
Every device in the hero sets its type from one scale (stage SCALE: title
11, interface 9, code 8.5, caption 7, in the stage's units); maxpower's
screen converts its own unit through useF. Measured at 2000px: code 11.1px,
interface 11.7px, caption 9.1px in the laptop, the terminal and the phone.

A drag keeps its offset on the element and writes translate once a frame
under pointer capture, with no transition and no render: zero pixels
between the pointer and the device a frame after each move, and a drop
stays where it is.

Nothing a reader does pauses the script any more: a press, a switch, a drag
or a run driven in Dev plays beside it, and it resumes what they touched.

Every control answers: sidebar New and Recents, every send (plays the beat
again), Studio's tiles (the pick becomes maxpower's icon), the phone's bar
(the places menu, Chat | Dev, New), through one Hands context.

The pointer travels each place's round of real controls by data-cue, a
tooltip with the control's name at each stop. The company's notes come
twice as often, the routine between the story's, one line each.

The laptop's title bar carries an appearance chip: theme, accent, face and
corners, applied by @hanzo/appearance to the stage element alone.
2026-10-30 00:16:27 -07:00

446 lines
17 KiB
TypeScript

'use client'
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'
import { useI18n } from '@hanzo/i18n/react'
import { Text, View, XStack, YStack, type GuiElement } from '@hanzo/ui'
import { Dots, type Field } from '@hanzo/ui/dots'
import { Lede, Title } from '@hanzo/ui/marketing'
import { Label } from './label'
import { INSET } from '@/components/shell/dock'
/**
* The parts every landing section is drawn from: a window, the dot field behind
* it, the section's head, and the two facts an animation asks before it moves
* (is it on screen, and does the reader want motion).
*
* MOTION MOVES MARKS, NEVER COPY. A bar, a dot, a line and a clock animate; a
* word, a figure's text and a heading are drawn where they stand from the first
* frame, so a reader the observer never reaches still reads every one
* (LLM.md "A scroll reveal may not hide the content it reveals").
*/
/** Whether `query` matches, kept current; `server` is the answer before the page knows (the server, the first paint). */
export function useMedia(query: string, server: boolean): boolean {
return useSyncExternalStore(
(on) => {
const q = window.matchMedia(query)
q.addEventListener('change', on)
return () => q.removeEventListener('change', on)
},
() => window.matchMedia(query).matches,
() => server,
)
}
/** Whether the reader asked for less motion. The server and the first paint assume still, so nothing moves before the page knows. */
export const useStill = () => useMedia('(prefers-reduced-motion: reduce)', true)
/**
* The element `ref` names has come on screen (a fifth of it, by default): true
* once and for good, so an animation plays the first time it is seen and holds
* its last frame after.
*/
export function useSeen(threshold = 0.2) {
const ref = useRef<GuiElement>(null)
const [seen, setSeen] = useState(false)
useEffect(() => {
const el = ref.current as unknown as Element | null
if (!el) return
if (typeof IntersectionObserver === 'undefined') return
const io = new IntersectionObserver(
([e]) => {
if (!e?.isIntersecting) return
setSeen(true)
io.disconnect()
},
{ threshold },
)
io.observe(el)
return () => io.disconnect()
}, [threshold])
return [ref, seen] as const
}
/** A DOM node behind a gui ref. */
export const node = (r: { current: GuiElement | null }) => r.current as unknown as HTMLElement | null
/**
* A mark that grows into place the first time it is seen: a paused animation
* from its first frame, so it waits at `from` with no style of its own, plays
* when `play` turns true, and stands at `to` for a reader who wants no motion.
*/
export function useGrow(play: boolean, still: boolean, { delay = 0, duration = 700, from = 'scaleX(0)', to = 'scaleX(1)' } = {}) {
const ref = useRef<GuiElement>(null)
const run = useRef<Animation | null>(null)
useLayoutEffect(() => {
const el = node(ref)
if (!el || typeof el.animate !== 'function') return
const a = el.animate([{ transform: from }, { transform: to }], { duration, delay, easing: 'cubic-bezier(0.2, 0.7, 0.2, 1)', fill: 'both' })
run.current = a
if (still) a.finish()
else a.pause()
return () => a.cancel()
}, [still, delay, duration, from, to])
useEffect(() => {
if (play && !still) run.current?.play()
}, [play, still])
return ref
}
/** Whether the windows may move, from 1024px: false on the server and the first paint, so nothing moves before the page knows. */
const useRoomy = () => useMedia('(min-width: 1024px)', false)
/** The window last taken hold of is drawn above the rest. One count for the page. */
let lifted = 1
/** The pixels an arrow key moves a focused title bar; with Shift, four times as far. */
const NUDGE = 12
/**
* What makes a window move: a pointer or a finger on its handle drags it, and
* the arrow keys nudge it while the handle has focus (`NUDGE`, four times as far
* with Shift). It moves by a transform from where the page put it (`at`), held
* inside the section it belongs to, and the one taken hold of comes to the
* front (`z`, one count for the page). Below 1024px nothing moves. A button
* inside the handle stays a button. Spread `handle` on the handle; put `box` on
* the window, its `z` and `translate3d(at)` on its style.
*/
export type Grip = ReturnType<typeof useMove>['handle'] & { 'aria-label': string }
export function useMove() {
const box = useRef<GuiElement>(null)
const wide = useRoomy()
const [z, setZ] = useState(0)
// The offset lives on the element, never in React state: a drag writes the
// element's own `translate` once a frame and renders nothing, so the window
// stays under the pointer (it was a re-render of the whole scene a move,
// behind a transform transition). `translate` composes with any `transform`
// the window wears, so a scale or a fold is untouched.
const at = useRef({ x: 0, y: 0 })
const grab = useRef<{ id: number; x: number; y: number; from: { x: number; y: number } } | null>(null)
const frame = useRef(0)
const place = (x: number, y: number) => {
at.current = { x, y }
const el = node(box)
if (el) el.style.translate = x || y ? `${x}px ${y}px` : ''
}
// Back where the page put it when the screen narrows past moving.
useEffect(() => {
if (!wide) place(0, 0)
}, [wide])
useEffect(() => () => cancelAnimationFrame(frame.current), [])
/** An offset held inside the section the window belongs to. */
const held = (x: number, y: number) => {
const el = node(box)
const room = el?.closest('section')
if (!el || !room) return { x, y }
const r = el.getBoundingClientRect()
const c = room.getBoundingClientRect()
// Where the window stands with no offset, so the bounds do not move with it.
const left = r.left - at.current.x
const top = r.top - at.current.y
const clamp = (v: number, lo: number, hi: number) => (lo > hi ? 0 : Math.min(hi, Math.max(lo, v)))
return { x: clamp(x, c.left - left, c.right - left - r.width), y: clamp(y, c.top - top, c.bottom - top - r.height) }
}
const lift = () => setZ(++lifted)
const down = (e: React.PointerEvent<HTMLElement>) => {
if (!wide || (e.target as HTMLElement).closest('button')) return
lift()
grab.current = { id: e.pointerId, x: e.clientX, y: e.clientY, from: at.current }
e.currentTarget.setPointerCapture(e.pointerId)
e.preventDefault()
}
const move = (e: React.PointerEvent<HTMLElement>) => {
const g = grab.current
if (!g || g.id !== e.pointerId) return
const to = held(g.from.x + e.clientX - g.x, g.from.y + e.clientY - g.y)
cancelAnimationFrame(frame.current)
frame.current = requestAnimationFrame(() => place(to.x, to.y))
}
// Dropped where it is: nothing snaps back.
const up = (e: React.PointerEvent<HTMLElement>) => {
if (grab.current?.id === e.pointerId) grab.current = null
}
// gui types the event as React Native's; on the web it is the DOM's, which carries both fields.
const key = (e: { key?: string; shiftKey?: boolean; target: unknown; currentTarget: unknown; preventDefault: () => void }) => {
if (e.target !== e.currentTarget || !wide) return
const step = e.shiftKey ? NUDGE * 4 : NUDGE
const d = ({ ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] } as Record<string, number[]>)[e.key ?? '']
if (!d) return
e.preventDefault()
lift()
const to = held(at.current.x + d[0], at.current.y + d[1])
place(to.x, to.y)
}
return {
box,
wide,
z,
/** Brings the window to the front, as a press anywhere on it does. */
lift,
handle: {
tabIndex: 0,
role: 'group',
'data-handle': '',
cursor: wide ? 'grab' : undefined,
onPointerDown: down,
onPointerMove: move,
onPointerUp: up,
onPointerCancel: up,
onKeyDown: key,
},
} as const
}
/**
* A window: a title bar in the page's ink with the page's ground for its words,
* a mark or icon, a mono label, and whatever the window holds. Smooth rounded
* corners with subtle depth. The bar carries no control of its own.
*
* ON A WIDE SCREEN A WINDOW MOVES. Its title bar is the handle: a pointer or a
* finger drags it, and the arrow keys nudge it while the bar has focus. It
* moves by a transform from where the page put it, held inside the
* section it belongs to, so none is lost off the page, and the one taken hold
* of comes to the front. Below 1024px the windows stay stacked and still.
* Everything inside a window stays a control of its own: only the bar drags.
*/
export function Win({
title,
aside,
icon,
terminal,
children,
slot,
open,
}: {
title: string
aside?: ReactNode
icon?: ReactNode
/** A line of the window's own, terminal-style: the command the window is about. */
terminal?: ReactNode
children: ReactNode
slot?: string
/** Lets a menu inside spill past the frame; the bar then rounds its own top corners. */
open?: boolean
}) {
const { t } = useI18n()
const { box, wide, z, handle } = useMove()
return (
<YStack
ref={box}
data-slot={slot}
data-win={title}
position="relative"
z={z || undefined}
minW={0}
borderWidth={1}
borderColor="var(--border-strong)"
bg="var(--background)"
rounded={20}
overflow={open ? 'visible' : 'hidden'}
boxShadow="0 28px 60px -32px color-mix(in srgb, var(--pure-black) 80%, transparent)"
$platform-web={{ borderRadius: '20px' }}
>
<XStack
items="center"
gap={10}
px={12}
py={7}
bg="var(--foreground)"
borderTopLeftRadius={open ? 19 : undefined}
borderTopRightRadius={open ? 19 : undefined}
{...handle}
aria-label={wide ? t('{title} window. Drag or use the arrow keys to move it.', { title }) : t('{title} window', { title })}
focusVisibleStyle={{ outlineWidth: 2, outlineStyle: 'solid', outlineColor: 'var(--ring)', outlineOffset: -2 }}
$platform-web={{ color: 'var(--background)', touchAction: wide ? 'none' : 'auto', userSelect: 'none' }}
>
<View aria-hidden display="flex" items="center" justify="center" shrink={0}>
{icon ?? <View width={6} height={6} rounded={9999} bg="var(--background)" $platform-web={{ borderRadius: '9999px' }} />}
</View>
<Text fontFamily="$mono" fontSize="$1" lineHeight="$1" color="var(--background)" fontWeight="600" numberOfLines={1} flex={1} minW={0}>
{title}
</Text>
{terminal ? (
<Text fontFamily="$mono" fontSize="$1" lineHeight="$1" color="var(--background)" numberOfLines={1}>
{terminal}
</Text>
) : null}
{aside ? (
<XStack px={8} py={2} rounded={9999} borderWidth={1} borderColor="var(--background)" opacity={0.75} items="center" justify="center" $platform-web={{ borderRadius: '9999px' }}>
<Text fontFamily="$mono" fontSize="$1" lineHeight="$1" color="var(--background)" numberOfLines={1}>
{aside}
</Text>
</XStack>
) : null}
</XStack>
{children}
</YStack>
)
}
/** A line of the window's mono readout: a label at the start of the line, its value at the end. */
export function Reading({ label, children, strong }: { label: string; children: ReactNode; strong?: boolean }) {
return (
<XStack justify="space-between" flexWrap="wrap" columnGap={12} py={4}>
<Text fontFamily="$mono" fontSize="$2" lineHeight="$2" color="var(--muted-foreground)">
{label}
</Text>
<Text fontFamily="$mono" fontSize="$2" lineHeight="$2" color={strong ? 'var(--foreground)' : 'var(--muted-foreground)'} text="end" $platform-web={{ fontVariantNumeric: 'tabular-nums' }}>
{children}
</Text>
</XStack>
)
}
/**
* The dot field behind a section: a halftone of soft clouds, painted once at
* the cell pitch, and drifted as one composited layer while it is on screen.
* Moving the painted canvas, rather than repainting it, is what keeps the page
* at the display's rate: a transform costs the compositor, not the CPU.
*
* The clouds are placed against the words, so a right-to-left page, whose
* headings and promise stand at the right, draws the field mirrored
* (`data-i18n-mirror` on the frame; the drift moves the layer inside it).
*/
/** Clouds as centre and radius, in the field's 0..1 units. */
type Cloud = [number, number, number]
export const clouds =
(cs: Cloud[]): Field =>
(x, y) =>
Math.min(1, cs.reduce((v, [cx, cy, r]) => v + Math.exp(-((x - cx) ** 2 + (y - cy) ** 2) / (r * r)), 0)) ** 1.4
/** The sections': heaviest at the right and the foot, where no heading sits. */
const CLOUD = clouds([
[0.95, 0.35, 0.34],
[0.78, 0.98, 0.4],
[0.2, 1.05, 0.32],
])
export function Dither({ field = CLOUD, cell = 7, strength = 0.2 }: { field?: Field; cell?: number; strength?: number }) {
const ref = useRef<GuiElement>(null)
const still = useStill()
useEffect(() => {
const el = node(ref)
if (!el || still || typeof el.animate !== 'function') return
const drift = el.animate([{ transform: 'translate3d(0, 0, 0)' }, { transform: 'translate3d(-2.5%, 1.5%, 0)' }], {
duration: 26000,
direction: 'alternate',
iterations: Infinity,
easing: 'ease-in-out',
})
drift.pause()
const io = new IntersectionObserver(([e]) => (e?.isIntersecting ? drift.play() : drift.pause()))
io.observe(el)
return () => {
io.disconnect()
drift.cancel()
}
}, [still])
return (
<View aria-hidden position="absolute" t={0} l={0} r={0} b={0} overflow="hidden" pointerEvents="none" data-i18n-mirror>
<View ref={ref} position="absolute" t="-4%" l="-4%" width="108%" height="108%" opacity={strength} $platform-web={{ color: 'var(--foreground)' }}>
<Dots field={field} cell={cell} animate={false} fade={{ top: 0.12, bottom: 0.18, left: 0.06, right: 0.06 }} />
</View>
</View>
)
}
/** The first screen's field: clouds behind the figure and at the corners, clear behind the promise. */
const OPENING = clouds([
[0.98, 0.1, 0.36],
[0.7, 1.02, 0.34],
[0.02, 0.98, 0.26],
[0.62, 0.0, 0.18],
])
/**
* The dot field behind a page's first screen, denser than a section's. / opens
* on it, and so does /enso, the promise on the left and the figure on the right.
*/
export function Opening() {
return <Dither field={OPENING} strength={0.34} />
}
/**
* A section of the page: its anchor, a label in mono above the heading, the
* heading, a line under it, and the figure. Every section is a band of the
* page's measure, with the dot field behind it when it asks for one. Its id is
* also its `data-hz-view`, so @hanzo/observe counts a `$view` the first time
* half of it is on screen, and every click inside names it as its `$section`.
*/
export function Section({
id,
label,
title,
lede,
dither,
children,
}: {
id: string
label: string
title: ReactNode
lede?: ReactNode
dither?: boolean
children: ReactNode
}) {
return (
<View render="section" id={id} aria-labelledby={`${id}-title`} data-hz-view={id} position="relative" width="100%" py={56} $md={{ py: 80 }}>
{dither ? <Dither /> : null}
<YStack position="relative" width="100%" px={INSET} gap={40}>
<YStack gap={14} maxW={760}>
<Label>{label}</Label>
<Title id={`${id}-title`}>{title}</Title>
{lede ? <Lede>{lede}</Lede> : null}
</YStack>
{children}
</YStack>
</View>
)
}
/** The room a figure keeps for itself while its read is on its way, so nothing below it moves when it arrives. */
export function Hold({ height, children }: { height: number; children: ReactNode }) {
return (
<YStack minH={height}>
{children}
</YStack>
)
}
/**
* A card leans toward a mouse and a light follows it (`[data-tilt]` in
* app/layout.tsx), the app cards and the model cards alike: the pointer's place on the card's face sets four properties
* on it, and leaving clears them. A finger, a pen and reduced motion get none.
*/
export function useLean(still: boolean) {
const face = useRef<GuiElement>(null)
useEffect(() => {
const el = node(face)
if (!el || still) return
const move = (e: PointerEvent) => {
if (e.pointerType !== 'mouse') return
const r = el.getBoundingClientRect()
const x = (e.clientX - r.left) / r.width
const y = (e.clientY - r.top) / r.height
el.style.setProperty('--ry', `${((x - 0.5) * 6).toFixed(2)}deg`)
el.style.setProperty('--rx', `${((0.5 - y) * 6).toFixed(2)}deg`)
el.style.setProperty('--mx', `${(x * 100).toFixed(1)}%`)
el.style.setProperty('--my', `${(y * 100).toFixed(1)}%`)
el.style.setProperty('--lit', '1')
}
const leave = () => ['--rx', '--ry', '--lit'].forEach((k) => el.style.removeProperty(k))
el.addEventListener('pointermove', move)
el.addEventListener('pointerleave', leave)
return () => {
el.removeEventListener('pointermove', move)
el.removeEventListener('pointerleave', leave)
leave()
}
}, [still])
return face
}