mirror of
https://github.com/hanzoai/gui.git
synced 2026-11-03 00:13:54 +00:00
The kit shipped ten typefaces. One is ours. Deleted: font-inter, font-silkscreen, font-cherry-bomb, font-nohemi, font-dm-sans, font-dm-serif-display, font-fira-mono, font-glusp, font-munro. Six of those had no dependent in the workspace at all. font-zen stays, and so do font-size, get-font-sized and font, which size and register rather than name a face. Published versions remain on the registry, so anything pinned to one keeps resolving. @hanzogui/config's DEFAULT entry built heading and body from Inter — v4 was already system fonts and v5 already Zen, so an app on the default got a face nobody chose. It builds from Zen now. dev-config, which drives gui.hanzo.ai, was worse: Inter for text, Berkeley Mono for code, plus Cherry Bomb and Silkscreen. Every size, weight, letter-spacing and transform value is unchanged — only the faces moved. The pixel ROLE was worth keeping and only its face was borrowed, so font-zen gained Zen Pixel Square (createZenPixelFont, --font-zen-pixel-*) and the token is `$pixel`. `$silkscreen` and `$cherryBomb` are gone across ~35 call sites. Cherry Bomb had no role of its own and no Zen counterpart, so it is deleted rather than substituted. gui.hanzo.ai now self-hosts three Zen files in place of four foreign ones — one variable file per family instead of a weight per request — and its OG-image route renders cards in the same faces as the pages they link to. The Inter, Berkeley, Silkscreen and glusp binaries are gone from public/fonts, including an otf directory of 20 Inter cuts. Native is honestly short: @hanzo/font ships woff2 and react-native wants otf/ttf, so the expo starter and kitchen-sink's native entry load no custom face and say why, rather than keeping Inter because it happened to be there. bun install 0 · build 161/161 · typecheck passed. Co-authored-by: Hanzo Dev <[email protected]>
342 lines
8.2 KiB
TypeScript
342 lines
8.2 KiB
TypeScript
import { LogoIcon, useTint } from '@hanzogui/logo'
|
|
import { ArrowDown, Play } from '@hanzogui/lucide-icons-2'
|
|
import React, { memo, useEffect, useRef, useState } from 'react'
|
|
import {
|
|
Button,
|
|
ListItem,
|
|
Paragraph,
|
|
Separator,
|
|
Square,
|
|
XStack,
|
|
YStack,
|
|
useControllableState,
|
|
useEvent,
|
|
} from '@hanzo/gui'
|
|
import { ContainerLarge } from '~/components/Containers'
|
|
import { Link } from '~/components/Link'
|
|
import { useIsIntersecting } from '~/hooks/useOnIntersecting'
|
|
import { CodeDemoPreParsed } from './CodeDemoPreParsed'
|
|
import { HomeH2, HomeH3 } from './HomeHeaders'
|
|
|
|
export const AnimationsDemo = (props) => {
|
|
const { tint } = useTint()
|
|
return <AnimationsDemoBase tint={tint} {...props} />
|
|
}
|
|
|
|
const animationDescriptions = [
|
|
{
|
|
name: 'Bouncy',
|
|
description: 'A bouncy spring',
|
|
animation: 'bouncy',
|
|
settings: {
|
|
damping: 10,
|
|
stiffness: 80,
|
|
mass: 0.8,
|
|
},
|
|
},
|
|
{
|
|
name: 'Lazy',
|
|
description: 'A slow, relaxed spring',
|
|
animation: 'lazy',
|
|
settings: {
|
|
damping: 15,
|
|
stiffness: 50,
|
|
mass: 2,
|
|
},
|
|
},
|
|
{
|
|
name: 'Quick',
|
|
description: 'A super fast spring',
|
|
animation: 'quick',
|
|
settings: {
|
|
damping: 10,
|
|
stiffness: 200,
|
|
mass: 1,
|
|
},
|
|
},
|
|
] as const
|
|
|
|
let hasScrolledOnce = false
|
|
|
|
export function HomeAnimations({ animationCode }: { animationCode: string }) {
|
|
const { tint } = useTint()
|
|
const [disableScrollPane, setDisableScrollPane] = useState(true)
|
|
|
|
return (
|
|
<YStack>
|
|
<ContainerLarge position="relative" gap="$8">
|
|
<YStack z={1} gap="$3">
|
|
<HomeH2 position="relative">
|
|
Universal <span className="rainbow clip-text">Animations</span>
|
|
</HomeH2>
|
|
<HomeH3>
|
|
Better platform targeting with animation drivers that can be changed without
|
|
changing code.
|
|
</HomeH3>
|
|
</YStack>
|
|
|
|
<XStack gap="$4">
|
|
<YStack
|
|
flex={2}
|
|
minW="55%"
|
|
self="flex-start"
|
|
z={100}
|
|
elevation="$4"
|
|
rounded="$4"
|
|
theme={tint as any}
|
|
justify="center"
|
|
>
|
|
<ExampleAnimations />
|
|
</YStack>
|
|
|
|
<YStack
|
|
perspective={1000}
|
|
rotateY="-5deg"
|
|
$sm={{ display: 'none' }}
|
|
position="relative"
|
|
rounded="$8"
|
|
elevation="$5"
|
|
overflow="hidden"
|
|
>
|
|
<YStack
|
|
pointerEvents={disableScrollPane ? 'auto' : 'none'}
|
|
opacity={disableScrollPane ? 1 : 0}
|
|
fullscreen
|
|
items="center"
|
|
justify="center"
|
|
>
|
|
<YStack fullscreen t="60%" opacity={0.5} />
|
|
<Button
|
|
aria-label="View more"
|
|
y={200}
|
|
iconAfter={ArrowDown}
|
|
size="$4"
|
|
theme="accent"
|
|
z={10}
|
|
onPress={() => setDisableScrollPane((prev) => !prev)}
|
|
>
|
|
View more
|
|
</Button>
|
|
</YStack>
|
|
|
|
<CodeDemoPreParsed
|
|
pointerEvents={disableScrollPane ? 'none' : 'auto'}
|
|
height={disableScrollPane ? 500 : 1250}
|
|
transition="quick"
|
|
maxW={530}
|
|
minW={530}
|
|
rounded="$8"
|
|
language="tsx"
|
|
source={animationCode}
|
|
/>
|
|
</YStack>
|
|
</XStack>
|
|
|
|
<XStack self="center" gap="$3">
|
|
<Link href="/docs/core/animations">
|
|
<Button aria-label="Animation docs">
|
|
<Button.Text fontFamily="$pixel">Docs »</Button.Text>
|
|
</Button>
|
|
</Link>
|
|
</XStack>
|
|
</ContainerLarge>
|
|
</YStack>
|
|
)
|
|
}
|
|
|
|
export const ExampleAnimations = memo(() => {
|
|
const [animationI, setAnimationI] = useState(0)
|
|
const animation = animationDescriptions[animationI]
|
|
const container = useRef(null)
|
|
const [positionI, setPositionI] = useState(2)
|
|
const isIntersecting = useIsIntersecting(container)
|
|
const next = (to = 1) => {
|
|
setPositionI((x) => (x + to) % 3)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!isIntersecting) return
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'ArrowRight') {
|
|
next()
|
|
}
|
|
if (e.key === 'ArrowLeft') {
|
|
next(-1)
|
|
}
|
|
}
|
|
if (!hasScrolledOnce) {
|
|
hasScrolledOnce = true
|
|
setTimeout(() => {
|
|
// setting a long timeout extends the total render time a lot.., just slow down animation
|
|
next()
|
|
}, 250)
|
|
}
|
|
window.addEventListener('keydown', onKey)
|
|
return () => {
|
|
window.removeEventListener('keydown', onKey)
|
|
}
|
|
}, [isIntersecting])
|
|
|
|
const settings =
|
|
typeof animation.settings === 'string'
|
|
? [['transition', animation.settings]]
|
|
: Object.entries(animation.settings)
|
|
|
|
return (
|
|
<XStack
|
|
borderWidth={1}
|
|
borderColor="$borderColor"
|
|
elevation="$1"
|
|
width="100%"
|
|
rounded="$4"
|
|
overflow="hidden"
|
|
height={305}
|
|
self="center"
|
|
x={0}
|
|
flexDirection="row-reverse"
|
|
>
|
|
<YStack
|
|
ref={container}
|
|
position="relative"
|
|
items="center"
|
|
justify="center"
|
|
width="60%"
|
|
$sm={{ width: '100%' }}
|
|
>
|
|
<YStack fullscreen z={-1} bg="$background" opacity={0.5} />
|
|
{isIntersecting ? (
|
|
<AnimationsDemo position={positionI} animation={animation.animation} />
|
|
) : null}
|
|
</YStack>
|
|
|
|
<Separator vertical />
|
|
|
|
<YStack position="relative" $sm={{ display: 'none' }} width="40%">
|
|
<YStack flex={1} bg="$color1">
|
|
{animationDescriptions.map((item, i) => {
|
|
const isActive = item === animation
|
|
return (
|
|
<ListItem
|
|
key={item.name}
|
|
theme={isActive ? 'accent' : null}
|
|
px="$4"
|
|
py="$2"
|
|
title={item.name}
|
|
bg={isActive ? '$color2' : '$color1'}
|
|
subTitle={item.description}
|
|
cursor="pointer"
|
|
onPress={() => {
|
|
setAnimationI(i)
|
|
next()
|
|
}}
|
|
/>
|
|
)
|
|
})}
|
|
</YStack>
|
|
|
|
<Separator />
|
|
|
|
<XStack bg="$background" p="$4" items="center" justify="center">
|
|
{/* @ts-ignore */}
|
|
{settings.map(([key, value], i) => {
|
|
if (key === 'type') {
|
|
return null
|
|
}
|
|
return (
|
|
<React.Fragment key={key}>
|
|
<YStack>
|
|
<Paragraph size="$2">{key}</Paragraph>
|
|
<Paragraph>{value}</Paragraph>
|
|
</YStack>
|
|
{i < settings.length - 1 && <Separator vertical mx={15} />}
|
|
</React.Fragment>
|
|
)
|
|
})}
|
|
</XStack>
|
|
</YStack>
|
|
</XStack>
|
|
)
|
|
})
|
|
|
|
export function AnimationsDemoBase(props) {
|
|
const [positionI, setPositionI] = useControllableState({
|
|
strategy: 'most-recent-wins',
|
|
prop: props.position,
|
|
defaultProp: 0,
|
|
})
|
|
const position = positions[positionI]
|
|
const onPress = useEvent(() => {
|
|
setPositionI((x) => {
|
|
return (x + 1) % positions.length
|
|
})
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<Square
|
|
transition={props.animation || 'bouncy'}
|
|
animateOnly={['transform']}
|
|
onPress={onPress}
|
|
size={104}
|
|
borderColor="$borderColor"
|
|
borderWidth={1}
|
|
rounded="$9"
|
|
bg="$color9"
|
|
hoverStyle={{
|
|
scale: 1.5,
|
|
}}
|
|
pressStyle={{
|
|
scale: 0.9,
|
|
}}
|
|
{...position}
|
|
>
|
|
{props.children || <LogoIcon downscale={0.75} />}
|
|
</Square>
|
|
|
|
<Button
|
|
position="absolute"
|
|
b={20}
|
|
l={20}
|
|
icon={Play}
|
|
theme={props.tint}
|
|
size="$5"
|
|
circular
|
|
onPress={onPress}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export const positions = [
|
|
{
|
|
x: 0,
|
|
y: 0,
|
|
scale: 1,
|
|
rotate: '0deg',
|
|
},
|
|
{
|
|
x: -50,
|
|
y: -50,
|
|
scale: 0.5,
|
|
rotate: '-45deg',
|
|
hoverStyle: {
|
|
scale: 0.6,
|
|
},
|
|
pressStyle: {
|
|
scale: 0.4,
|
|
},
|
|
},
|
|
{
|
|
x: 50,
|
|
y: 50,
|
|
scale: 1,
|
|
rotate: '180deg',
|
|
hoverStyle: {
|
|
scale: 1.1,
|
|
},
|
|
pressStyle: {
|
|
scale: 0.9,
|
|
},
|
|
},
|
|
]
|