53 lines
1.9 KiB
TypeScript
53 lines
1.9 KiB
TypeScript
import { AnimatePresence, motion } from "framer-motion";
|
|
import { X } from "lucide-react";
|
|
import { springs } from "@/lib/motion";
|
|
import { useToastStore } from "@/stores/toast-store";
|
|
|
|
const TYPE_STYLES = {
|
|
success: "bg-pylon-accent/10 text-pylon-accent border-pylon-accent/20",
|
|
error: "bg-pylon-danger/10 text-pylon-danger border-pylon-danger/20",
|
|
info: "bg-pylon-surface text-pylon-text border-border",
|
|
} as const;
|
|
|
|
export function ToastContainer() {
|
|
const toasts = useToastStore((s) => s.toasts);
|
|
const removeToast = useToastStore((s) => s.removeToast);
|
|
const pauseToast = useToastStore((s) => s.pauseToast);
|
|
const resumeToast = useToastStore((s) => s.resumeToast);
|
|
|
|
return (
|
|
<div
|
|
className="pointer-events-none fixed bottom-4 right-4 z-[100] flex flex-col gap-2"
|
|
role="status"
|
|
aria-live="polite"
|
|
aria-atomic="true"
|
|
>
|
|
<AnimatePresence>
|
|
{toasts.map((toast) => (
|
|
<motion.div
|
|
key={toast.id}
|
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 20, scale: 0.9 }}
|
|
transition={springs.wobbly}
|
|
className={`pointer-events-auto flex items-center gap-2 rounded-lg border px-4 py-2 text-sm shadow-md ${TYPE_STYLES[toast.type]}`}
|
|
onMouseEnter={() => pauseToast(toast.id)}
|
|
onMouseLeave={() => resumeToast(toast.id)}
|
|
onFocus={() => pauseToast(toast.id)}
|
|
onBlur={() => resumeToast(toast.id)}
|
|
>
|
|
<span className="flex-1">{toast.message}</span>
|
|
<button
|
|
onClick={() => removeToast(toast.id)}
|
|
className="shrink-0 rounded p-0.5 transition-opacity hover:opacity-70"
|
|
aria-label="Dismiss notification"
|
|
>
|
|
<X className="size-3.5" />
|
|
</button>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
}
|