f67d0aa0db
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
55 lines
1.5 KiB
React
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>
|
|
)
|
|
}
|