Spaces:
Sleeping
Sleeping
| /** Interruptible toast for completion and error feedback. */ | |
| import { animate } from "motion"; | |
| import { createContext } from "preact"; | |
| import { useCallback, useContext, useEffect, useRef, useState } from "preact/hooks"; | |
| import type { ComponentChildren } from "preact"; | |
| import { prefersReducedMotion, springs } from "../motion/springs"; | |
| type ToastKind = "info" | "error"; | |
| type ToastState = { | |
| id: number; | |
| message: string; | |
| kind: ToastKind; | |
| } | null; | |
| type ToastApi = { | |
| show: (message: string, kind?: ToastKind) => void; | |
| }; | |
| const ToastContext = createContext<ToastApi | null>(null); | |
| export function useToast(): ToastApi { | |
| const ctx = useContext(ToastContext); | |
| if (!ctx) throw new Error("useToast requires ToastProvider"); | |
| return ctx; | |
| } | |
| export function ToastProvider({ children }: { children: ComponentChildren }) { | |
| const [toast, setToast] = useState<ToastState>(null); | |
| const elRef = useRef<HTMLDivElement>(null); | |
| const timer = useRef<number | null>(null); | |
| const show = useCallback((message: string, kind: ToastKind = "info") => { | |
| setToast({ id: Date.now(), message, kind }); | |
| }, []); | |
| useEffect(() => { | |
| if (!toast || !elRef.current) return; | |
| const el = elRef.current; | |
| if (prefersReducedMotion()) { | |
| el.style.opacity = "1"; | |
| } else { | |
| el.style.opacity = "0"; | |
| el.style.transform = "translateY(-12px)"; | |
| animate(el, { opacity: 1, y: 0 }, springs.snap); | |
| } | |
| if (timer.current) window.clearTimeout(timer.current); | |
| timer.current = window.setTimeout(() => { | |
| if (prefersReducedMotion()) { | |
| setToast(null); | |
| return; | |
| } | |
| animate(el, { opacity: 0, y: -8 }, springs.press).finished.then(() => { | |
| setToast(null); | |
| }); | |
| }, 2200); | |
| return () => { | |
| if (timer.current) window.clearTimeout(timer.current); | |
| }; | |
| }, [toast]); | |
| return ( | |
| <ToastContext.Provider value={{ show }}> | |
| {children} | |
| <div class="toast-host" aria-live="polite" aria-atomic="true"> | |
| {toast ? ( | |
| <div | |
| ref={elRef} | |
| class={`toast ${toast.kind === "error" ? "error" : ""}`} | |
| role="status" | |
| > | |
| {toast.message} | |
| </div> | |
| ) : null} | |
| </div> | |
| </ToastContext.Provider> | |
| ); | |
| } | |