/* Shared components and small visual primitives */ const Icon = ({ name, size = 16, stroke = 1.6 }) => { const props = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round", }; switch (name) { case "arrow-right": return ; case "chevron-right": return ; case "chevron-down": return ; case "chevron-left": return ; case "plus": return ; case "check": return ; case "x": return ; case "menu": return ; case "shield": return ; case "heart": return ; case "calendar": return ; case "file": return ; case "activity": return ; case "lock": return ; case "search": return ; case "bell": return ; case "star": return ; case "sun": return ; case "moon": return ; case "play": return ; case "users": return ; case "trending": return ; default: return null; } }; /* simple sparkline svg */ const Sparkline = ({ values, color = "currentColor", height = 36, fill = false }) => { const max = Math.max(...values), min = Math.min(...values); const w = 100, h = height; const points = values.map((v, i) => { const x = (i / (values.length - 1)) * w; const y = h - ((v - min) / (max - min || 1)) * (h - 6) - 3; return `${x},${y}`; }); return ( {fill && ( )} ); }; /* count up hook */ const useCountUp = (target, duration = 1400, start = 0, enabled = true) => { const [v, setV] = React.useState(start); React.useEffect(() => { if (!enabled) return; let raf, t0; const step = (t) => { if (!t0) t0 = t; const p = Math.min(1, (t - t0) / duration); setV(Math.round(start + (target - start) * (1 - Math.pow(1 - p, 3)))); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [target, duration, start, enabled]); return v; }; const useInView = (ref, threshold = 0.2) => { const [vis, setVis] = React.useState(false); React.useEffect(() => { if (!ref.current) return; const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setVis(true); obs.disconnect(); } }, { threshold }); obs.observe(ref.current); return () => obs.disconnect(); }, []); return vis; }; const StatNumber = ({ target, suffix = "" }) => { const ref = React.useRef(null); const inView = useInView(ref); const v = useCountUp(target, 1600, 0, inView); return {v}{suffix}; }; Object.assign(window, { Icon, Sparkline, useCountUp, useInView, StatNumber });