Toast
src/components/thegridcn/toast.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
type ToastVariant = "info" | "success" | "warning" | "error";
interface ToastItem {
description?: string;
duration?: number;
id: string;
title: string;
variant?: ToastVariant;
}
interface ToastContextValue {
addToast: (toast: Omit<ToastItem, "id">) => void;
removeToast: (id: string) => void;
toasts: ToastItem[];
}
const ToastContext = React.createContext<ToastContextValue | null>(null);
export function useToast() {
const ctx = React.useContext(ToastContext);
if (!ctx) {
throw new Error("useToast must be used within ToastProvider");
}
return ctx;
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastItem[]>([]);
const addToast = React.useCallback((toast: Omit<ToastItem, "id">) => {
const id = Math.random().toString(36).slice(2, 9);
setToasts((prev) => [...prev, { ...toast, id }]);
const duration = toast.duration ?? 4000;
if (duration > 0) {
setTimeout(
() => setToasts((prev) => prev.filter((t) => t.id !== id)),
duration
);
}
}, []);
const removeToast = React.useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ addToast, removeToast, toasts }}>
{children}
<ToastContainer toasts={toasts} onRemove={removeToast} />
</ToastContext.Provider>
);
}
const variantStyles: Record<ToastVariant, string> = {
error: "border-red-500/30",
info: "border-primary/30",
success: "border-emerald-500/30",
warning: "border-amber-500/30",
};
const variantDots: Record<ToastVariant, string> = {
error: "bg-red-400",
info: "bg-primary",
success: "bg-emerald-400",
warning: "bg-amber-400",
};
function ToastContainer({
toasts,
onRemove,
}: {
toasts: ToastItem[];
onRemove: (id: string) => void;
}) {
if (toasts.length === 0) {
return null;
}
return (
<div className="fixed right-4 bottom-4 z-[100] flex flex-col gap-2">
{toasts.map((toast) => (
<div
key={toast.id}
data-slot="tron-toast"
className={cn(
"relative min-w-[280px] max-w-sm overflow-hidden rounded border bg-card/95 px-4 py-3 shadow-[0_0_20px_rgba(var(--primary-rgb,0,180,255),0.06)] backdrop-blur-md",
variantStyles[toast.variant || "info"]
)}
>
{/* Scanline */}
<div className="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(0,0,0,0.02)_2px,rgba(0,0,0,0.02)_4px)]" />
<div className="flex items-start gap-2.5">
<span
className={cn(
"mt-1 h-1.5 w-1.5 shrink-0 rounded-full",
variantDots[toast.variant || "info"]
)}
/>
<div className="flex-1">
<span className="block font-mono text-[10px] text-foreground/70 uppercase tracking-widest">
{toast.title}
</span>
{toast.description ? (
<span className="mt-0.5 block font-mono text-[9px] text-foreground/35">
{toast.description}
</span>
) : null}
</div>
<button
type="button"
onClick={() => onRemove(toast.id)}
className="shrink-0 text-foreground/20 hover:text-foreground/50"
>
<svg
aria-hidden="true"
width="8"
height="8"
viewBox="0 0 8 8"
fill="none"
>
<path
d="M1 1l6 6M7 1l-6 6"
stroke="currentColor"
strokeWidth="1.2"
strokeLinecap="round"
/>
</svg>
</button>
</div>
{/* Corner ticks */}
<div className="pointer-events-none absolute top-0 left-0 h-1.5 w-1.5 border-primary/30 border-t border-l" />
<div className="pointer-events-none absolute right-0 bottom-0 h-1.5 w-1.5 border-primary/30 border-r border-b" />
</div>
))}
</div>
);
}
/** Standalone toast display for previews (no provider needed) */
export function ToastDemo({ className }: { className?: string }) {
const toasts: ToastItem[] = [
{
description: "Grid firmware v2.7.1 installed.",
id: "1",
title: "System Updated",
variant: "success",
},
{
description: "Sector 7G response time elevated.",
id: "2",
title: "Warning",
variant: "warning",
},
{
description: "Relay node 12A unreachable.",
id: "3",
title: "Connection Lost",
variant: "error",
},
];
return (
<div className={cn("flex flex-col gap-2", className)}>
{toasts.map((toast) => (
<div
key={toast.id}
className={cn(
"relative min-w-[260px] overflow-hidden rounded border bg-card/95 px-4 py-3 backdrop-blur-md",
variantStyles[toast.variant || "info"]
)}
>
<div className="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(0,0,0,0.02)_2px,rgba(0,0,0,0.02)_4px)]" />
<div className="flex items-start gap-2.5">
<span
className={cn(
"mt-1 h-1.5 w-1.5 shrink-0 rounded-full",
variantDots[toast.variant || "info"]
)}
/>
<div>
<span className="block font-mono text-[10px] text-foreground/70 uppercase tracking-widest">
{toast.title}
</span>
{toast.description ? (
<span className="mt-0.5 block font-mono text-[9px] text-foreground/35">
{toast.description}
</span>
) : null}
</div>
</div>
<div className="pointer-events-none absolute top-0 left-0 h-1.5 w-1.5 border-primary/30 border-t border-l" />
<div className="pointer-events-none absolute right-0 bottom-0 h-1.5 w-1.5 border-primary/30 border-r border-b" />
</div>
))}
</div>
);
}
No selection tags yet — empty means no evidence, not lack of support.