// ===== ui.jsx ===== // Shared UI primitives: Navbar, Footer, Button, Badge, AnimatedNumber, etc. const { useEffect, useRef, useState, useMemo, useCallback } = React; // ----- Animated number (rAF-based count up) ----- function AnimatedNumber({ value, duration = 1200, decimals = 0, className = '', prefix = '', suffix = '' }) { const [n, setN] = useState(0); const startRef = useRef(null); const fromRef = useRef(0); useEffect(() => { fromRef.current = n; startRef.current = null; let raf; const step = (ts) => { if (!startRef.current) startRef.current = ts; const t = Math.min(1, (ts - startRef.current) / duration); // easeOutCubic const e = 1 - Math.pow(1 - t, 3); setN(fromRef.current + (value - fromRef.current) * e); if (t < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); // eslint-disable-next-line }, [value, duration]); const formatted = decimals > 0 ? n.toFixed(decimals) : Math.round(n).toLocaleString('en-US'); return {prefix}{formatted}{suffix}; } // ----- Button ----- function Button({ as: As = 'button', kind = 'primary', size = 'md', className = '', children, icon, iconRight, ...rest }) { const base = 'inline-flex items-center justify-center gap-2 font-medium rounded-lg transition-all duration-150 select-none'; const sizes = { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4 text-[14px]', lg: 'h-12 px-6 text-[15px]' }; const kinds = { primary: 'bg-brand-600 text-white hover:bg-blue-700 active:bg-blue-800 shadow-sm', secondary: 'bg-white text-navy-900 ring-1 ring-ink-200 hover:bg-ink-50 hover:ring-ink-300', ghost: 'text-ink-700 hover:bg-ink-100', dark: 'bg-navy-900 text-white hover:bg-navy-800', danger: 'bg-bad-500 text-white hover:bg-bad-600', }; return ( {icon ? {icon} : null} {children} {iconRight ? {iconRight} : null} ); } // ----- Badge ----- function Badge({ children, tone = 'ink', className = '' }) { const tones = { ink: 'bg-ink-100 text-ink-700', brand: 'bg-brand-50 text-brand-600 ring-1 ring-brand-100', ok: 'bg-ok-50 text-ok-600 ring-1 ring-ok-100', warn: 'bg-warn-50 text-warn-600 ring-1 ring-warn-100', bad: 'bg-bad-50 text-bad-600 ring-1 ring-bad-100', navy: 'bg-navy-900 text-white', }; return {children}; } // ----- Card ----- function Card({ className = '', children, ...rest }) { return
{children}
; } // ----- Status pill (also for Order) ----- function StatusPill({ status }) { const s = MOCK.ORDER_STATUSES[status]; if (!s) return null; return ( {s.label} ); } // ----- Navbar ----- function Navbar({ route, navigate }) { const items = [ { key: 'home', label: '产品' }, { key: 'home#pricing', label: '定价' }, { key: 'home#faq', label: '帮助' }, ]; return (
); } // ----- Footer ----- function Footer({ navigate }) { return ( ); } // ----- Section helper ----- function Section({ id, eyebrow, title, subtitle, children, className = '', dark = false }) { return (
{(eyebrow || title) && (
{eyebrow &&
{eyebrow}
} {title &&

{title}

} {subtitle &&

{subtitle}

}
)} {children}
); } // Visibility-triggered fade-up function Reveal({ children, delay = 0, as: As = 'div', className = '' }) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }, { threshold: 0.12 }); io.observe(el); return () => io.disconnect(); }, []); return ( {children} ); } Object.assign(window, { AnimatedNumber, Button, Badge, Card, StatusPill, Navbar, Footer, Section, Reveal });