/* UI primitives ===================================================== */ const { useState, useEffect, useRef, useMemo, useCallback, Fragment } = React; /* ---------- Icons (inline SVG; no external icon lib needed) -------- */ function Icon({ name, size=16, className='', style }) { const props = { width:size, height:size, viewBox:'0 0 24 24', fill:'none', stroke:'currentColor', strokeWidth:1.6, strokeLinecap:'round', strokeLinejoin:'round', className, style }; switch (name) { case 'chev-down': return ; case 'chev-right': return ; case 'chev-left': return ; case 'plus': return ; case 'x': return ; case 'search': return ; case 'calendar': return ; case 'bell': return ; case 'sun': return ; case 'moon': return ; case 'settings': return ; case 'sliders': return ; case 'play': return ; case 'pause': return ; case 'trend-up': return ; case 'trend-down': return ; case 'alert': return ; case 'info': return ; case 'check': return ; case 'zap': return ; case 'columns': return ; case 'mediaplan': return ; case 'menu': return ; case 'mobile': return ; case 'monitor': return ; case 'tablet': return ; case 'rocket': return ; case 'arrow-up': return ; case 'arrow-down': return ; case 'arrow-right':return ; case 'filter': return ; case 'dot': return ; case 'link-break': return ; case 'menu-dots': return ; case 'eye': return ; case 'eye-off': return ; case 'sparkles': return ; case 'image': return ; case 'link': return ; case 'refresh': return ; case 'upload': return ; case 'download': return ; case 'trash': return ; case 'lock': return ; case 'palette': return ; case 'users': return ; case 'star': return ; case 'file': return ; default: return null; } } /* ---------- Live dot ----------------------------------------------- */ function LiveDot({ tone='#22c55e', size=8 }){ return ( ); } /* ---------- Card --------------------------------------------------- */ function Card({ className='', children, ...rest }){ return (
{children}
); } /* ---------- Pill / Tag -------------------------------------------- */ function Pill({ children, tone='neutral', className='' }){ const tones = { neutral: 'bg-ink-100 text-ink-700 dark:bg-white/5 dark:text-ink-200', accent: 'bg-accent-100 text-accent-700 dark:bg-accent-500/15 dark:text-accent-200', good: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300', bad: 'bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300', warn: 'bg-amber-100 text-amber-800 dark:bg-amber-500/15 dark:text-amber-300', info: 'bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300', }; return {children}; } /* ---------- Toggle ------------------------------------------------- */ function Toggle({ checked, onChange, size='md', label, tone='#10b981' }){ const W = size==='sm' ? 28 : 38, H = size==='sm' ? 16 : 22, K = size==='sm' ? 12 : 18; const off = (H-K)/2; return ( ); } /* ---------- IconButton --------------------------------------------- */ function IconButton({ children, onClick, active=false, className='', title }){ return ( ); } /* ---------- Btn ---------------------------------------------------- */ function Btn({ children, onClick, variant='ghost', size='md', className='', icon }){ const sizeCls = size==='sm' ? 'h-8 px-2.5 text-[12px]' : 'h-9 px-3 text-[13px]'; const variants = { primary: 'bg-ink-900 hover:bg-ink-800 text-white dark:bg-white dark:hover:bg-ink-100 dark:text-ink-900', accent: 'bg-accent-500 hover:bg-accent-600 text-white', ghost: 'border border-ink-200 hover:bg-ink-100 text-ink-800 dark:border-white/10 dark:hover:bg-white/5 dark:text-ink-100', subtle: 'bg-ink-100 hover:bg-ink-200 text-ink-800 dark:bg-white/5 dark:hover:bg-white/10 dark:text-ink-100', danger: 'bg-rose-600 hover:bg-rose-700 text-white', }; return ( ); } /* ---------- Dropdown ----------------------------------------------- */ function useOutsideClick(ref, onClose){ useEffect(()=>{ function handler(e){ if(!ref.current) return; if(!ref.current.contains(e.target)) onClose(); } document.addEventListener('mousedown', handler); return ()=>document.removeEventListener('mousedown', handler); }, [onClose]); } function Dropdown({ trigger, children, align='left', width=240 }){ const [open, setOpen] = useState(false); const ref = useRef(null); useOutsideClick(ref, ()=>setOpen(false)); return (
setOpen(o=>!o)}>{trigger}
{open && (
{typeof children === 'function' ? children(()=>setOpen(false)) : children}
)}
); } /* ---------- Sparkline (svg) ---------------------------------------- */ function Sparkline({ data, width, height=36, tone='#8351ff', area=true, strokeWidth=1.6 }){ if (!data || !data.length) return null; const responsive = width === undefined; const W = width || 200; const H = height; const vals = data.map(d => d.v); const min = Math.min(...vals), max = Math.max(...vals); const pad = 2; const x = i => pad + (i*(W-2*pad)/(data.length-1)); const y = v => H - pad - ((v-min)/((max-min)||1))*(H-2*pad); const path = data.map((d,i)=>(i===0?'M':'L')+x(i).toFixed(2)+','+y(d.v).toFixed(2)).join(' '); const areaPath = path + ` L ${x(data.length-1).toFixed(2)},${H-pad} L ${x(0).toFixed(2)},${H-pad} Z`; const id = useMemo(()=>'sg'+Math.random().toString(36).slice(2,8), []); const svgProps = responsive ? { viewBox: `0 0 ${W} ${H}`, preserveAspectRatio:'none', style:{width:'100%', height:H} } : { width: W, height: H }; return ( {area && } ); } /* ---------- Rolling ticker number ---------------------------------- */ function Ticker({ value, format='int', tone='neutral', className='' }){ /* Subtle: don't fade in/out — gently translate the new value into place */ const prev = useRef(value); const [bump, setBump] = useState(0); useEffect(()=>{ if (value === prev.current) return; prev.current = value; setBump(b=>b+1); }, [value]); return ( {fmt(value, format)} ); } /* ---------- Tab strip ---------------------------------------------- */ function Tabs({ items, value, onChange }){ const ref = useRef(null); const [pill, setPill] = useState({left:0, width:0}); useEffect(()=>{ if (!ref.current) return; const el = ref.current.querySelector(`[data-tab="${value}"]`); if (!el) return; const parentRect = ref.current.getBoundingClientRect(); const r = el.getBoundingClientRect(); setPill({ left: r.left - parentRect.left, width: r.width }); }, [value, items.length]); return (
{items.map(it=>( ))}
); } /* ---------- Modal -------------------------------------------------- */ function Modal({ open, onClose, children, width=560 }){ useEffect(()=>{ if (!open) return; const onKey = e => e.key==='Escape' && onClose(); document.addEventListener('keydown', onKey); return ()=>document.removeEventListener('keydown', onKey); }, [open, onClose]); if (!open) return null; return (
{children}
); } /* ---------- Drawer (right) ----------------------------------------- */ function Drawer({ open, onClose, children, width=480, title, footer }){ if (!open) return null; return (
{title && (
{title}
)}
{children}
{footer &&
{footer}
}
); } /* ---------- Platform chip ------------------------------------------ */ function PlatformChip({ p, size=18 }){ const plat = PLATFORMS.find(x=>x.id===p) || PLATFORMS[0]; if (plat.icon){ return ( ); } return ( {plat.glyph} ); } /* ---------- Export -------------------------------------------------- */ Object.assign(window, { Icon, LiveDot, Card, Pill, Toggle, IconButton, Btn, Dropdown, useOutsideClick, Sparkline, Ticker, Tabs, Modal, Drawer, PlatformChip, }); /* v2 — refresh icon added 2026-07-02 */