Slice 6: notifications per §15
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
// §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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user