You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/packages/ui/src/toast.tsx

47 lines
1.6 KiB
TypeScript

import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react'
import { dismissToast, initialToasts, pushToast, type Toast, type ToastState } from './toast-store'
const AUTO_DISMISS_MS = 4000
interface ToastApi { ok: (msg: string) => void; err: (msg: string) => void }
const Ctx = createContext<ToastApi | null>(null)
/** Mount once at app root; renders the bottom-right stack. */
export function ToastProvider(props: { children: ReactNode }) {
const [state, setState] = useState<ToastState>(initialToasts)
const push = useCallback((tone: Toast['tone'], message: string) => {
setState((s) => {
const next = pushToast(s, tone, message)
const id = next.toasts[next.toasts.length - 1]!.id
window.setTimeout(() => setState((s2) => dismissToast(s2, id)), AUTO_DISMISS_MS)
return next
})
}, [])
const api = useMemo<ToastApi>(() => ({
ok: (msg) => push('ok', msg),
err: (msg) => push('err', msg),
}), [push])
return (
<Ctx.Provider value={api}>
{props.children}
<div className="wf-toasts" aria-live="polite">
{state.toasts.map((t) => (
<div key={t.id} className={`wf-toast ${t.tone}`}>
<span className="msg">{t.message}</span>
<button type="button" aria-label="Dismiss" onClick={() => setState((s) => dismissToast(s, t.id))}></button>
</div>
))}
</div>
</Ctx.Provider>
)
}
export function useToast(): ToastApi {
const api = useContext(Ctx)
if (api === null) throw new Error('useToast requires <ToastProvider> at the app root')
return api
}