/* Notifications: Bell + sliding toasts ============================== */ const SEV_TONES = { info: { bg:'bg-sky-50 dark:bg-sky-500/10', text:'text-sky-700 dark:text-sky-300', border:'border-sky-200 dark:border-sky-500/30', dot:'#0ea5e9' }, warn: { bg:'bg-amber-50 dark:bg-amber-500/10', text:'text-amber-800 dark:text-amber-300', border:'border-amber-200 dark:border-amber-500/30', dot:'#f59e0b' }, error: { bg:'bg-rose-50 dark:bg-rose-500/10', text:'text-rose-700 dark:text-rose-300', border:'border-rose-200 dark:border-rose-500/30', dot:'#ef4444' }, }; const SEV_ICONS = { info:'info', warn:'alert', error:'link-break' }; function fmtAgo(ts){ const m = Math.max(0, Math.floor((Date.now()-ts)/60000)); if (m < 1) return 'just now'; if (m < 60) return m + 'm ago'; const h = Math.floor(m/60); if (h < 24) return h + 'h ago'; return Math.floor(h/24) + 'd ago'; } function ToastStack({ toasts, onDismiss }){ return (
{toasts.map(t => { const tone = SEV_TONES[t.sev]; return (
{t.title}
{t.msg}
); })}
); } function NotificationsBell({ alerts, onMarkAllRead }){ const unread = alerts.filter(a=>!a.read).length; return ( {unread>0 && ( {unread > 99 ? '99+' : unread} )} }> {(close)=>(
Notifications
{alerts.length === 0 && (
No notifications yet.
)} {alerts.map(a => { const tone = SEV_TONES[a.sev]; return (
{a.title}
{a.msg}
{fmtAgo(a.ts)}
{!a.read && }
); })}
)}
); } Object.assign(window, { ToastStack, NotificationsBell });