TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { CheckCircle2, AlertTriangle, Info, X, XCircle } from "lucide-react";4import { cn } from "@/lib/utils";56type ToastKind = "success" | "error" | "info" | "warning";7interface ToastItem {8 id: number;9 kind: ToastKind;10 title: string;11 description?: string;12 duration: number;13}1415type Listener = (items: ToastItem[]) => void;16let items: ToastItem[] = [];17let seq = 1;18const listeners = new Set<Listener>();1920function publish() {21 for (const l of listeners) l([...items]);22}2324function push(kind: ToastKind, title: string, description?: string, duration = 4200) {25 const id = seq++;26 items = [...items, { id, kind, title, description, duration }];27 publish();28 if (duration > 0) setTimeout(() => dismiss(id), duration);29 return id;30}3132export function dismiss(id: number) {33 items = items.filter((t) => t.id !== id);34 publish();35}3637export const toast = {38 success: (title: string, description?: string) => push("success", title, description),39 error: (title: string, description?: string) => push("error", title, description, 6000),40 info: (title: string, description?: string) => push("info", title, description),41 warning: (title: string, description?: string) => push("warning", title, description, 5000),42};4344const ICONS: Record<ToastKind, React.ReactNode> = {45 success: <CheckCircle2 className="size-4 text-success" />,46 error: <XCircle className="size-4 text-danger" />,47 info: <Info className="size-4 text-info" />,48 warning: <AlertTriangle className="size-4 text-warning" />,49};5051export function Toaster() {52 const [list, setList] = React.useState<ToastItem[]>([]);53 React.useEffect(() => {54 listeners.add(setList);55 return () => {56 listeners.delete(setList);57 };58 }, []);59 if (list.length === 0) return null;60 return (61 <div className="pointer-events-none fixed inset-x-0 bottom-[calc(env(safe-area-inset-bottom)+12px)] z-[100] flex flex-col items-center gap-2 px-4 sm:items-end sm:right-4 sm:left-auto" role="status" aria-live="polite">62 {list.map((t) => (63 <div key={t.id} className={cn("pointer-events-auto flex w-full max-w-sm items-start gap-2.5 rounded-lg border border-border bg-bg-elevated p-3 shadow-lg animate-fade-up")}>64 <span className="mt-0.5">{ICONS[t.kind]}</span>65 <div className="min-w-0 flex-1">66 <p className="text-sm font-medium leading-5">{t.title}</p>67 {t.description ? <p className="mt-0.5 text-xs text-fg-muted leading-4 break-words">{t.description}</p> : null}68 </div>69 <button onClick={() => dismiss(t.id)} className="rounded p-0.5 text-fg-subtle hover:text-fg" aria-label="Dismiss">70 <X className="size-3.5" />71 </button>72 </div>73 ))}74 </div>75 );76}77