// ===== 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 (
{subtitle}
}