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