Files
automaker/apps/ui/src/components/splash-screen.tsx
SuperComboGamer 584f5a3426 Merge main into massive-terminal-upgrade
Resolves merge conflicts:
- apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger
- apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions
- apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling)
- apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes
- apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 20:27:44 -05:00

283 lines
9.6 KiB
TypeScript

import { useEffect, useState, useMemo } from 'react';
const TOTAL_DURATION = 2300; // Total animation duration in ms (tightened from 4000)
const LOGO_ENTER_DURATION = 500; // Tightened from 1200
const PARTICLES_ENTER_DELAY = 100; // Tightened from 400
const EXIT_START = 1800; // Adjusted for shorter duration
interface Particle {
id: number;
x: number;
y: number;
size: number;
delay: number;
angle: number;
distance: number;
opacity: number;
floatDuration: number;
}
function generateParticles(count: number): Particle[] {
return Array.from({ length: count }, (_, i) => {
const angle = (i / count) * 360 + Math.random() * 30;
const distance = 60 + Math.random() * 80; // Increased spread
return {
id: i,
x: Math.cos((angle * Math.PI) / 180) * distance,
y: Math.sin((angle * Math.PI) / 180) * distance,
size: 3 + Math.random() * 6, // Slightly smaller range for more subtle look
delay: Math.random() * 400,
angle,
distance: 300 + Math.random() * 200,
opacity: 0.4 + Math.random() * 0.6,
floatDuration: 3000 + Math.random() * 4000,
};
});
}
export function SplashScreen({ onComplete }: { onComplete: () => void }) {
const [phase, setPhase] = useState<'enter' | 'hold' | 'exit' | 'done'>('enter');
const particles = useMemo(() => generateParticles(50), []);
useEffect(() => {
const timers: NodeJS.Timeout[] = [];
// Phase transitions
timers.push(setTimeout(() => setPhase('hold'), LOGO_ENTER_DURATION));
timers.push(setTimeout(() => setPhase('exit'), EXIT_START));
timers.push(
setTimeout(() => {
setPhase('done');
onComplete();
}, TOTAL_DURATION)
);
return () => timers.forEach(clearTimeout);
}, [onComplete]);
if (phase === 'done') return null;
return (
<div
className={`
fixed inset-0 z-[9999] flex items-center justify-center
bg-background
transition-opacity duration-500 ease-out
${phase === 'exit' ? 'opacity-0' : 'opacity-100'}
`}
style={{
pointerEvents: phase === 'exit' ? 'none' : 'auto',
}}
>
<style>{`
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin-slow-reverse {
from { transform: rotate(360deg); }
to { transform: rotate(0deg); }
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(6px, -6px); }
}
`}</style>
{/* Subtle gradient background */}
<div
className="absolute inset-0 opacity-30"
style={{
background: 'radial-gradient(circle at center, var(--brand-500) 0%, transparent 70%)',
}}
/>
{/* Particle container 1 - Clockwise */}
<div
className="absolute inset-0 flex items-center justify-center overflow-hidden"
style={{ animation: 'spin-slow 60s linear infinite' }}
>
{particles.slice(0, 25).map((particle) => (
<div
key={particle.id}
className="absolute"
style={{
transform:
phase === 'exit'
? `translate(${Math.cos((particle.angle * Math.PI) / 180) * particle.distance}px, ${Math.sin((particle.angle * Math.PI) / 180) * particle.distance}px)`
: `translate(${particle.x}px, ${particle.y}px)`,
transition:
phase === 'enter'
? `all 600ms ease-out ${PARTICLES_ENTER_DELAY + particle.delay}ms`
: phase === 'exit'
? `all 800ms cubic-bezier(0.4, 0, 1, 1) ${particle.delay * 0.3}ms`
: 'all 300ms ease-out',
}}
>
<div
className="rounded-full"
style={{
width: particle.size,
height: particle.size,
background: `linear-gradient(135deg, var(--brand-400), var(--brand-600))`,
boxShadow: `0 0 ${particle.size * 2}px var(--brand-500)`,
opacity: phase === 'enter' ? 0 : phase === 'hold' ? particle.opacity : 0,
transform: phase === 'exit' ? 'scale(0)' : 'scale(1)',
animation: `float ${particle.floatDuration}ms ease-in-out infinite`,
transition: 'opacity 300ms ease-out, transform 300ms ease-out',
}}
/>
</div>
))}
</div>
{/* Particle container 2 - Counter-Clockwise */}
<div
className="absolute inset-0 flex items-center justify-center overflow-hidden"
style={{ animation: 'spin-slow-reverse 75s linear infinite' }}
>
{particles.slice(25).map((particle) => (
<div
key={particle.id}
className="absolute"
style={{
transform:
phase === 'exit'
? `translate(${Math.cos((particle.angle * Math.PI) / 180) * particle.distance}px, ${Math.sin((particle.angle * Math.PI) / 180) * particle.distance}px)`
: `translate(${particle.x}px, ${particle.y}px)`,
transition:
phase === 'enter'
? `all 600ms ease-out ${PARTICLES_ENTER_DELAY + particle.delay}ms`
: phase === 'exit'
? `all 800ms cubic-bezier(0.4, 0, 1, 1) ${particle.delay * 0.3}ms`
: 'all 300ms ease-out',
}}
>
<div
className="rounded-full"
style={{
width: particle.size,
height: particle.size,
background: `linear-gradient(135deg, var(--brand-400), var(--brand-600))`,
boxShadow: `0 0 ${particle.size * 2}px var(--brand-500)`,
opacity: phase === 'enter' ? 0 : phase === 'hold' ? particle.opacity : 0,
transform: phase === 'exit' ? 'scale(0)' : 'scale(1)',
animation: `float ${particle.floatDuration}ms ease-in-out infinite`,
animationDelay: `${particle.delay}ms`,
transition: 'opacity 300ms ease-out, transform 300ms ease-out',
}}
/>
</div>
))}
</div>
{/* Logo container */}
<div
className="relative z-10"
style={{
opacity: phase === 'enter' ? 0 : phase === 'exit' ? 0 : 1,
transform:
phase === 'enter'
? 'scale(0.3) rotate(-20deg)'
: phase === 'exit'
? 'scale(2.5) translateY(-100px)'
: 'scale(1) rotate(0deg)',
transition:
phase === 'enter'
? `all ${LOGO_ENTER_DURATION}ms cubic-bezier(0.34, 1.56, 0.64, 1)`
: phase === 'exit'
? 'all 600ms cubic-bezier(0.4, 0, 1, 1)'
: 'all 300ms ease-out',
}}
>
{/* Glow effect behind logo */}
<div
className="absolute inset-0 blur-3xl"
style={{
background: 'radial-gradient(circle, var(--brand-500) 0%, transparent 70%)',
transform: 'scale(2.5)',
opacity: phase === 'hold' ? 0.6 : 0,
transition: 'opacity 500ms ease-out',
}}
/>
{/* The logo */}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
role="img"
aria-label="Automaker Logo"
className="relative z-10"
style={{
width: 120,
height: 120,
filter: 'drop-shadow(0 0 30px var(--brand-500))',
}}
>
<defs>
<linearGradient
id="splash-bg"
x1="0"
y1="0"
x2="256"
y2="256"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" style={{ stopColor: 'var(--brand-400)' }} />
<stop offset="100%" style={{ stopColor: 'var(--brand-600)' }} />
</linearGradient>
<filter id="splash-shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow
dx="0"
dy="4"
stdDeviation="4"
floodColor="#000000"
floodOpacity="0.25"
/>
</filter>
</defs>
<rect x="16" y="16" width="224" height="224" rx="56" fill="url(#splash-bg)" />
<g
fill="none"
stroke="#FFFFFF"
strokeWidth="20"
strokeLinecap="round"
strokeLinejoin="round"
filter="url(#splash-shadow)"
>
<path d="M92 92 L52 128 L92 164" />
<path d="M144 72 L116 184" />
<path d="M164 92 L204 128 L164 164" />
</g>
</svg>
</div>
{/* Automaker text that fades in below the logo */}
<div
className="absolute flex items-center gap-1"
style={{
top: 'calc(50% + 80px)',
opacity: phase === 'enter' ? 0 : phase === 'exit' ? 0 : 1,
transform:
phase === 'enter'
? 'translateY(20px)'
: phase === 'exit'
? 'translateY(-30px) scale(1.2)'
: 'translateY(0)',
transition:
phase === 'enter'
? `all 600ms ease-out ${LOGO_ENTER_DURATION - 200}ms`
: phase === 'exit'
? 'all 500ms cubic-bezier(0.4, 0, 1, 1)'
: 'all 300ms ease-out',
}}
>
<span className="font-bold text-foreground text-4xl tracking-tight leading-none">
automaker<span className="text-brand-500">.</span>
</span>
</div>
</div>
);
}