'use client' import Image from 'next/image' import { useEffect } from 'react' import { useTranslations } from 'next-intl' import { APP_VERSION } from '@/lib/version' interface InitStep { key: string label: string status: 'pending' | 'done' } interface LoaderProps { variant?: 'page' | 'panel' | 'inline' label?: string steps?: InitStep[] } const LOADER_AGENTS = [ { key: 'claude', name: 'Claude', src: '/brand/claude-logo.png', wrapperClass: 'absolute left-1/2 top-0 -translate-x-1/2 opacity-0 animate-converge-top', labelClass: 'absolute left-1/2 -top-5 -translate-x-1/2', }, { key: 'openclaw', name: 'OpenClaw', src: '/brand/openclaw-logo.png', wrapperClass: 'absolute left-0 top-1/2 -translate-y-1/2 opacity-0 animate-converge-left', labelClass: 'absolute -left-9 top-1/2 -translate-y-1/2', }, { key: 'codex', name: 'Codex', src: '/brand/codex-logo.png', wrapperClass: 'absolute right-0 top-1/2 -translate-y-1/2 opacity-0 animate-converge-right', labelClass: 'absolute -right-7 top-1/2 -translate-y-1/2', }, { key: 'hermes', name: 'Hermes', src: '/brand/hermes-logo.png', wrapperClass: 'absolute left-1/2 bottom-0 -translate-x-1/2 opacity-0 animate-converge-bottom', labelClass: 'absolute left-1/2 -bottom-5 -translate-x-1/2', }, ] as const const LOADER_IMAGE_SOURCES = [ ...LOADER_AGENTS.map((agent) => agent.src), '/brand/mc-logo-128.png', ] as const function LoaderDots({ size = 'md' }: { size?: 'sm' | 'md' }) { const dotSize = size === 'sm' ? 'w-1 h-1' : 'w-1.5 h-1.5' return (
) } function PageLoader({ steps }: { steps?: InitStep[] }) { const t = useTranslations('boot') useEffect(() => { const createdLinks: HTMLLinkElement[] = [] for (const href of LOADER_IMAGE_SOURCES) { const existing = document.head.querySelector(`link[rel="preload"][href="${href}"]`) if (!existing) { const link = document.createElement('link') link.rel = 'preload' link.as = 'image' link.href = href document.head.appendChild(link) createdLinks.push(link) } const img = new window.Image() img.src = href } return () => { for (const link of createdLinks) { link.remove() } } }, []) const doneCount = steps?.filter(s => s.status === 'done').length ?? 0 const totalCount = steps?.length ?? 1 const progress = steps ? (doneCount / totalCount) * 100 : 0 const allDone = steps ? doneCount === totalCount : false const activeStep = steps?.find(s => s.status === 'pending') return (
{/* Animated logo sequence: OpenClaw + Claude converge → morph into MC mark */}
{/* Ambient glow */}
{/* Phase 1: Four logos converge from cardinal directions (fades out at 1.8s) */}
{LOADER_AGENTS.map((agent) => (
{agent.name} {agent.name}
))} {/* Center burst */}
{/* Phase 2: MC mark emerges (fades in at 2.0s) */}
Mission Control
{/* Title */}

{t('missionControl')}

{t('agentOrchestration')}

{/* Progress section — appears after logo animation, only while loading */} {steps ? (
{/* Progress bar */}
{/* Active step label — crossfades on step change */}
{activeStep && (
{activeStep.label}
)}
) : ( /* SSR fallback — no progress data yet */ )} {/* Version */} v{APP_VERSION}
) } export function Loader({ variant = 'panel', label, steps }: LoaderProps) { if (variant === 'page') { return } if (variant === 'inline') { return (
{label && {label}}
) } // panel (default) return (
{label && {label}}
) }