Files
rfc-app/frontend/src/components/ToastHost.jsx
T
Ben Stull f67d0aa0db Slice 6: notifications per §15
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:09:04 -07:00

55 lines
1.5 KiB
React

// §15.3 — the toast surface.
//
// Toasts fire for the user's own actions completing and for events
// landing on the exact view the user is currently looking at. The
// inbox row still lands for the same event; the toast just carries
// the "something just happened here" beat per §15.3.
//
// This component is a simple stack with a small cap (4 visible at
// once). Newer toasts queue behind the visible ones rather than
// stacking endlessly. Auto-dismiss after a short interval; click to
// dismiss early.
import { useEffect, useState } from 'react'
const MAX_VISIBLE = 4
const AUTO_DISMISS_MS = 6000
let _emit = null
export function showToast({ summary, category, link }) {
if (_emit) _emit({ id: Math.random().toString(36).slice(2), summary, category, link, ts: Date.now() })
}
export default function ToastHost() {
const [toasts, setToasts] = useState([])
useEffect(() => {
_emit = (t) => setToasts(prev => [...prev, t])
return () => { _emit = null }
}, [])
useEffect(() => {
if (toasts.length === 0) return
const t = setTimeout(() => {
setToasts(prev => prev.slice(1))
}, AUTO_DISMISS_MS)
return () => clearTimeout(t)
}, [toasts])
const visible = toasts.slice(0, MAX_VISIBLE)
return (
<div className="toast-host">
{visible.map(t => (
<div
key={t.id}
className={`toast cat-${t.category || 'unknown'}`}
onClick={() => setToasts(prev => prev.filter(x => x.id !== t.id))}
>
{t.summary}
</div>
))}
</div>
)
}