perf(frontend): drop the motion library for CSS keyframes and small FLIP/presence hooks
This commit is contained in:
parent
7172652ac3
commit
d629ccf496
13 changed files with 213 additions and 135 deletions
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue