SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
2.7 KB · 77 lines tsx
Raw Blame History
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