Files
zeekay 89505054f1 landing: the hero holds to the bar's measure
The row was 1440 wide and its scene ran past the bar's Try Hanzo at 1440
and wider; it takes the shell's MEASURE, as every page does.
2026-10-30 00:46:05 -07:00

221 lines
7.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useRef } from 'react'
import { useAnalytics } from '@hanzo/event/react'
import { useI18n } from '@hanzo/i18n/react'
import { useIam } from '@hanzo/iam/react'
import { Text, View, XStack, YStack, type GuiElement } from '@hanzo/ui'
import { Action, Display, Lede, TextLink } from '@hanzo/ui/marketing'
import { Panel } from '@/components/auth/panel'
import { MEASURE, ROOF } from '@/components/shell/dock'
import { STRIP } from './beta'
import { Opening, node, useStill } from './chrome'
import { Label } from './label'
import BuildPreview from './BuildPreview'
/**
* The first screen: the promise and the way in beside the scene (laptop,
* terminal and phone, ./BuildPreview), which takes the rest of the row. Below
* 1024px the scene comes first and the way in under it.
*/
/** On a phone the scene is one phone, 9:16, as wide as the first screen under the bar lets it stand whole. */
const PHONE: `calc(${string})` = `calc(min(440px, (100svh - ${ROOF} - ${STRIP}px - 48px) * 9 / 16))`
function Door() {
const { t } = useI18n()
const { isLoading, isAuthenticated } = useIam()
const member = !isLoading && isAuthenticated
if (member) {
return (
<XStack gap={12} justify="center" flexWrap="wrap" py={24}>
<Action href="/chat" fill>
{t('Open Hanzo')}
</Action>
<Action href="https://docs.hanzo.ai">{t('Read docs')}</Action>
</XStack>
)
}
return (
<>
<Panel mode="signup" />
<Text render="p" maxW={420} fontSize="$1" lineHeight="$1" color="var(--muted-foreground)" text="center">
{t("By continuing, you agree to Hanzo's {terms} and {privacy}.", {
terms: <TextLink href="/terms">{t('Terms')}</TextLink>,
privacy: <TextLink href="/privacy">{t('Privacy Policy')}</TextLink>,
})}
</Text>
</>
)
}
function Three() {
const { t } = useI18n()
const ref = useRef<HTMLSpanElement>(null)
const still = useStill()
const light = useRef({ x: -1e4, ink: 100, tx: -1e4, tink: 100, sweep: 0, frame: 0 })
const paint = () => {
const el = ref.current
const l = light.current
if (!el) return
el.style.setProperty('--x', `${l.x.toFixed(1)}px`)
el.style.setProperty('--ink', `${l.ink.toFixed(1)}%`)
}
const run = () => {
const l = light.current
if (l.frame) return
const step = (now: number) => {
const el = ref.current
if (!el) return void (l.frame = 0)
if (l.sweep) {
const w = el.getBoundingClientRect().width
const t = Math.min(1, (now - l.sweep) / 1200)
const e = t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
l.tx = -0.25 * w + e * 1.5 * w
l.tink = t < 0.85 ? 30 : 100
if (t === 1) l.sweep = 0
}
l.x += (l.tx - l.x) * (l.sweep ? 1 : 0.24)
l.ink += (l.tink - l.ink) * 0.14
paint()
const settled = !l.sweep && Math.abs(l.tx - l.x) < 0.5 && Math.abs(l.tink - l.ink) < 0.5
l.frame = settled ? 0 : requestAnimationFrame(step)
}
l.frame = requestAnimationFrame(step)
}
useEffect(() => {
const l = light.current
if (still) return
l.sweep = performance.now()
run()
return () => {
cancelAnimationFrame(l.frame)
l.frame = 0
}
}, [still])
const at = (e: any, ink: number) => {
const el = ref.current
const l = light.current
if (!el) return
l.sweep = 0
l.tx = e.clientX - el.getBoundingClientRect().left
l.tink = ink
if (still) {
l.x = l.tx
l.ink = ink
paint()
} else run()
}
return (
<span
ref={ref}
data-flash=""
onPointerEnter={(e) => at(e, 30)}
onPointerMove={(e) => at(e, 30)}
onPointerLeave={(e) => at(e, 100)}
>
{t('Pick all three.')}
</span>
)
}
function useFit(column: { current: GuiElement | null }, words: unknown) {
useEffect(() => {
const h = node(column)?.querySelector<HTMLElement>('[data-fit]')
if (!h) return
let width = -1
const fit = () => {
const w = parseFloat(getComputedStyle(h).getPropertyValue('--w'))
if (!(w > 0)) return
for (const line of h.querySelectorAll<HTMLElement>('[data-line]')) {
const range = document.createRange()
range.selectNodeContents(line)
const at = (size: number) => {
line.style.fontSize = `${size.toFixed(3)}px`
return range.getBoundingClientRect().width
}
const near = parseFloat(getComputedStyle(line).fontSize)
const drawn = range.getBoundingClientRect().width
if (!(drawn > 0)) continue
let lo = (near * w) / drawn / 1.1
let hi = (near * w) / drawn * 1.1
for (let i = 0; i < 12; i++) {
const mid = (lo + hi) / 2
if (at(mid) <= w + 0.25) lo = mid
else hi = mid
}
at(lo)
}
}
const watch = new ResizeObserver(() => {
if (h.clientWidth === width) return
width = h.clientWidth
fit()
})
watch.observe(h)
document.fonts?.ready.then(fit)
return () => watch.disconnect()
}, [column, words])
}
export default function Start() {
const { t, messages } = useI18n()
const column = useRef<GuiElement>(null)
useFit(column, messages)
const events = useAnalytics()
const seen = useRef(false)
useEffect(() => {
if (seen.current) return
seen.current = true
events.capture('home_viewed')
}, [events])
return (
<View
render="section"
data-slot="hero"
aria-labelledby="home-title"
position="relative"
width="100%"
overflow="hidden"
display="flex"
flexDirection="column"
justify="center"
minH={`calc(100svh - ${ROOF} - ${STRIP}px)`}
$platform-web={{ overflowX: 'clip' }}
>
<Opening />
<XStack
position="relative"
width="100%"
// The bar's measure: the scene's far edge stands where the bar's last control does.
maxW={MEASURE}
mx="auto"
px="var(--page-gutter)"
pt="max(40px, min(8vh, 88px))"
pb="max(40px, min(7vh, 80px))"
gap="max(32px, min(4vw, 64px))"
items="center"
// Side by side from 1280px only: narrower, the scene beside a 440px column would set its type below reading size.
$max-xl={{ flexDirection: 'column-reverse', items: 'center', gap: 40 }}
$max-sm={{ pt: 24, gap: 32 }}
>
<YStack ref={column} width={440} shrink={0} gap="$3" items="center" data-measure="" $max-xl={{ width: '100%' }}>
<Label center href="/blog/pareto-frontier">{t('A New Pareto Frontier')}</Label>
<Display id="home-title" data-fit="" width="100%" text="center" mt={4}>
<span data-line="1">{t('Smarter, Faster, Open Source.')}</span>{' '}
<span data-line="2">
<Three />
</span>
</Display>
<Lede text="center" mb={8} $platform-web={{ textWrap: 'balance', lineHeight: '1.45' }}>
{t('Frontier intelligence up to {fast} faster and {cheap} cheaper. Every model behind one key.', { fast: <span data-num="">200×</span>, cheap: <span data-num="">400×</span> })}
</Lede>
<Door />
</YStack>
<YStack flex={1} minW={0} maxW={780} position="relative" $max-xl={{ flexBasis: 'auto', width: '100%', maxW: 780 }} $max-md={{ maxW: PHONE }} $max-sm={{ maxW: PHONE }}>
<BuildPreview />
</YStack>
</XStack>
</View>
)
}