Files

213 lines
6.8 KiB
TypeScript

'use client'
/**
* The lists under the hero.
*
* Requests walks the same three calls the figure routes. Walk carries the
* learning argument. SecondPass is Ultra's two outcomes. Every line is on
* the page either way; motion only moves the emphasis, and only while the
* section is showing.
*/
import { useEffect, useLayoutEffect, useState } from 'react'
import { Text, View, XStack, YStack } from '@hanzo/ui'
import { useCycle } from './cycle'
import { EASE, TIME } from './motion'
import type { Lane, Scene, Step } from './scenes'
const SHIFT = `color ${TIME.row}ms ${EASE}, border-color ${TIME.row}ms ${EASE}, background-color ${TIME.row}ms ${EASE}`
export function Requests({ scenes, lanes, rest }: { scenes: readonly Scene[]; lanes: readonly Lane[]; rest: number }) {
const { i, phase, play } = useCycle('enso-paths', scenes.length, rest)
const active = play && phase === 'pick' ? i : -1
const [mark, setMark] = useState<{ top: number; height: number } | null>(null)
useLayoutEffect(() => {
if (active < 0) return
const frame = window.requestAnimationFrame(() => {
const root = document.getElementById('enso-paths')
const row = root?.querySelectorAll('[data-row]')[active]
if (!root || !row) return
const rb = root.getBoundingClientRect()
const b = row.getBoundingClientRect()
setMark({ top: b.top - rb.top, height: b.height })
})
return () => window.cancelAnimationFrame(frame)
}, [active])
return (
<YStack id="enso-paths" render="ol" position="relative" m={0} p={0} borderTopWidth={1} borderColor="var(--border)">
{mark && play ? (
<View
position="absolute"
l={0}
width={2}
bg="$ink"
pointerEvents="none"
z="var(--z-raised)"
$platform-web={{
top: `${mark.top}px`,
height: `${mark.height}px`,
transition: `top ${TIME.row}ms ${EASE}`,
}}
/>
) : null}
{scenes.map((scene, n) => {
const on = n === active
return (
<XStack
key={scene.request}
render="li"
flexDirection="column"
gap="$2"
py="$5"
pl="$4"
borderBottomWidth={1}
borderColor={on ? 'var(--foreground)' : 'var(--border)'}
$sm={{ flexDirection: 'row', justify: 'space-between', items: 'baseline', gap: '$4' }}
$platform-web={{ transition: play ? SHIFT : undefined }}
{...{ 'data-row': '' }}
>
<YStack gap="$1" minW={0} $sm={{ flex: 1 }}>
<Text
fontSize="$6"
lineHeight="$6"
color={on ? '$ink' : 'var(--text-secondary)'}
$platform-web={{ transition: play ? `color ${TIME.row}ms ${EASE}` : undefined }}
>
{scene.request}
</Text>
<Text fontSize="$3" lineHeight="$3" color="var(--text-secondary)">
{scene.why}
</Text>
</YStack>
<Text
fontFamily="$mono"
fontSize="$2"
lineHeight="$2"
color={on ? '$ink' : 'var(--muted-foreground)'}
shrink={0}
$platform-web={{ transition: play ? `color ${TIME.row}ms ${EASE}` : undefined }}
>
{lanes.find((l) => l.id === scene.lane)?.verb} ยท {scene.name}
</Text>
</XStack>
)
})}
</YStack>
)
}
/** Emphasis walks the list once, then rests with every line at full ink. */
export function Walk({ id, steps }: { id: string; steps: readonly Step[] }) {
const [on, setOn] = useState(steps.length)
useEffect(() => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
let stop = false
let timer = 0
let n = -1
const shown = () => {
if (document.hidden) return false
const node = document.getElementById(id)
if (!node) return false
const r = node.getBoundingClientRect()
return r.bottom > 64 && r.top < window.innerHeight * 0.85
}
const step = () => {
if (stop) return
if (!shown()) {
timer = window.setTimeout(step, 400)
return
}
n += 1
setOn(n)
if (n < steps.length - 1) timer = window.setTimeout(step, 900)
}
timer = window.setTimeout(step, 280)
return () => {
stop = true
window.clearTimeout(timer)
}
}, [id, steps.length])
return (
<YStack id={id} render="ol" m={0} p={0} borderTopWidth={1} borderColor="var(--border)">
{steps.map((step, n) => {
const lit = n <= on
const here = n === on
return (
<XStack
key={step.title}
render="li"
gap="$4"
py="$5"
borderBottomWidth={1}
borderColor={here ? 'var(--foreground)' : 'var(--border)'}
$platform-web={{ transition: SHIFT }}
>
<View width={2} self="stretch" bg={here ? '$ink' : 'var(--border)'} shrink={0} />
<YStack gap="$2" flex={1} minW={0}>
<Text
render="h3"
fontSize="$5"
lineHeight="$5"
fontWeight="600"
color={lit ? '$ink' : 'var(--muted-foreground)'}
$platform-web={{ transition: `color ${TIME.row}ms ${EASE}` }}
>
{step.title}
</Text>
<Text fontSize="$3" lineHeight="$3" color="var(--text-secondary)">
{step.body}
</Text>
</YStack>
</XStack>
)
})}
</YStack>
)
}
/** Ultra's two outcomes. Both stay readable; the bar marks the one in play. */
export function SecondPass({ steps }: { steps: readonly Step[] }) {
const { i, phase, play } = useCycle('enso-pass', steps.length, 0)
const active = play && phase === 'pick' ? i : -1
return (
<YStack id="enso-pass" render="ol" m={0} p={0} borderTopWidth={1} borderColor="var(--border)">
{steps.map((step, n) => {
const on = n === active
return (
<XStack
key={step.title}
render="li"
gap="$4"
py="$5"
borderBottomWidth={1}
borderColor={on ? 'var(--foreground)' : 'var(--border)'}
$platform-web={{ transition: play ? SHIFT : undefined }}
>
<Text
fontFamily="$mono"
fontSize="$2"
lineHeight="$2"
color={on ? '$ink' : 'var(--muted-foreground)'}
shrink={0}
width={72}
>
{step.title}
</Text>
<Text fontSize="$5" lineHeight="$5" color={on || active < 0 ? '$ink' : 'var(--text-secondary)'} flex={1}>
{step.body}
</Text>
</XStack>
)
})}
</YStack>
)
}