perf(frontend): drop the motion library for CSS keyframes and small FLIP/presence hooks

This commit is contained in:
loki5512344 2026-09-30 16:26:36 +02:00
parent 7172652ac3
commit d629ccf496
Signed by: boba
GPG key ID: 253067914055423B
13 changed files with 213 additions and 135 deletions

View file

@ -1,4 +1,3 @@
import { MotionConfig, motion } from 'motion/react'
import { lazy, Suspense, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router'
@ -9,16 +8,14 @@ import { useActiveTheme } from '@features/themes/useActiveTheme'
import { ParticleField } from '../../effects/ParticleField'
import { useInView } from '../../effects/useInView'
import { useIdleReady } from '../../effects/useIdleReady'
import '@shared/motion/motion.css'
import '@features/landing/styles/landing.css'
// The backdrop is decorative and its first draw is heavy, so it loads and paints after the headline.
const VoxelScene = lazy(() => import('../../effects/VoxelScene').then((m) => ({ default: m.VoxelScene })))
const rise = (delay: number) => ({
initial: { opacity: 0, y: 24 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.7, delay, ease: [0.22, 1, 0.36, 1] as const },
})
/** Entrance: the `lv-rise` keyframes, staggered by `delay` seconds. */
const rise = (delay: number) => ({ ['--d' as string]: `${delay}s` })
function DownloadIcon() {
return (
@ -43,7 +40,7 @@ export function Hero() {
]
return (
<MotionConfig reducedMotion="user">
<>
<section
ref={sectionRef}
aria-labelledby="hero-title"
@ -59,20 +56,18 @@ export function Hero() {
<div className="relative mx-auto grid w-full max-w-[1120px] items-center gap-12 px-4 pb-20 pt-12 sm:px-6 lg:py-16">
<div className="flex min-w-0 flex-col items-start gap-6">
<motion.span
{...rise(0)}
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-black/30 px-3 py-1 font-code text-xs text-frost backdrop-blur"
<span style={rise(0)} className="lv-rise inline-flex items-center gap-2 rounded-full border border-white/10 bg-black/30 px-3 py-1 font-code text-xs text-frost backdrop-blur"
>
<span className="lv-live-dot" aria-hidden="true" />
{t('hero.badge')}
</motion.span>
<motion.h1 {...rise(0.08)} id="hero-title" className="lv-hero-title font-display font-bold">
</span>
<h1 style={rise(0.08)} id="hero-title" className="lv-rise lv-hero-title font-display font-bold">
{t('hero.headlineLead')} <span className="lv-hero-accent">{t('hero.headlineAccent')}</span>
</motion.h1>
<motion.p {...rise(0.16)} className="max-w-[34rem] text-lg leading-relaxed text-snow/80">
</h1>
<p style={rise(0.16)} className="lv-rise max-w-[34rem] text-lg leading-relaxed text-snow/80">
{t('hero.lead')}
</motion.p>
<motion.div {...rise(0.24)} className="flex flex-wrap items-center gap-3">
</p>
<div style={rise(0.24)} className="lv-rise flex flex-wrap items-center gap-3">
<Link to="/download" className="lv-cta-primary lv-cta-lg">
<DownloadIcon />
{t('hero.download')}
@ -80,34 +75,29 @@ export function Hero() {
<a href="/register" className="lv-cta-quiet-lg">
{t('hero.register')}
</a>
</motion.div>
<motion.p {...rise(0.3)} className="-mt-2 font-code text-xs text-frost">
</div>
<p style={rise(0.3)} className="lv-rise -mt-2 font-code text-xs text-frost">
{t('hero.downloadHint')}
</motion.p>
<motion.dl {...rise(0.36)} className="mt-2 flex flex-wrap gap-x-8 gap-y-3">
</p>
<dl style={rise(0.36)} className="lv-rise mt-2 flex flex-wrap gap-x-8 gap-y-3">
{facts.map((f) => (
<div key={f.label} className="flex flex-col">
<dt className="order-2 text-sm text-frost">{f.label}</dt>
<dd className="lv-fact font-display text-3xl font-bold text-snow">{f.n}</dd>
</div>
))}
</motion.dl>
</dl>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.94, y: 30 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.2, ease: [0.22, 1, 0.36, 1] }}
className="flex min-w-0 flex-col items-center gap-4"
>
<div className="lv-rise-panel flex min-w-0 flex-col items-center gap-4">
<ClickGuiWindow paused={!inView} />
<p className="flex items-center gap-2 text-sm text-frost">
<span className="lv-live-dot" aria-hidden="true" />
{t('hero.tryIt')}
</p>
</motion.div>
</div>
</div>
</section>
</MotionConfig>
</>
)
}

View file

@ -1,8 +1,9 @@
import { AnimatePresence, MotionConfig, motion } from 'motion/react'
import { type MouseEvent, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ButtonLink } from '@shared/ui/Button'
import { prefersReducedMotion } from '@shared/motion/reducedMotion'
import { RollText } from '@shared/motion/RollText'
import '@shared/motion/motion.css'
import { themeToCssVars } from '@features/clickgui/themeVars'
import { PRESETS } from '@features/themes/presets.generated'
import { argbToCss, type ThemeColors } from '@features/themes/types'
@ -38,27 +39,22 @@ export function ThemeStrip() {
}
return (
<MotionConfig reducedMotion="user">
<>
<section
ref={sectionRef}
aria-labelledby="themes-title"
style={themeToCssVars(theme)}
className="lv-bleed lv-paint relative isolate overflow-hidden"
>
<AnimatePresence>
{wave ? (
<motion.span
key={wave.key}
aria-hidden="true"
className="lv-paint-wave"
style={{ left: wave.x, top: wave.y }}
initial={{ scale: 0, opacity: 0.8 }}
animate={{ scale: 1, opacity: [0.8, 0.6, 0] }}
transition={{ duration: 0.9, ease: 'easeOut' }}
onAnimationComplete={() => setWave((w) => (w?.key === wave.key ? null : w))}
/>
) : null}
</AnimatePresence>
{wave ? (
<span
key={wave.key}
aria-hidden="true"
className="lv-paint-wave lv-wave-run"
style={{ left: wave.x, top: wave.y }}
onAnimationEnd={() => setWave((w) => (w?.key === wave.key ? null : w))}
/>
) : null}
<div className="lv-paint-header" aria-hidden="true">
<div className="mx-auto flex h-full max-w-[1120px] items-center gap-3 px-4 sm:px-6">
@ -79,18 +75,7 @@ export function ThemeStrip() {
<div className="flex min-w-0 flex-col gap-3">
<p className="text-sm" style={{ color: 'var(--gui-text-muted)' }}>{t('themeStrip.current')}</p>
<div className="relative h-[clamp(3.6rem,9vw,7.5rem)] overflow-hidden" aria-live="polite">
<AnimatePresence mode="popLayout" initial={false}>
<motion.p
key={theme.id}
className="lv-paint-name"
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '-100%' }}
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
>
{theme.name}
</motion.p>
</AnimatePresence>
<RollText value={theme.name} className="lv-paint-name" />
</div>
<ul aria-label={t('themeStrip.palette')} className="grid grid-cols-8 overflow-hidden rounded-ctl ring-1 ring-white/10">
{PALETTE_KEYS.map((k) => (
@ -137,6 +122,6 @@ export function ThemeStrip() {
</ul>
</div>
</section>
</MotionConfig>
</>
)
}