Toast.tsx
1 import * as React from 'react' 2 import * as RadixToast from '@radix-ui/react-toast' 3 import { X, CheckCircle, XCircle, AlertTriangle, Info } from 'lucide-react' 4 import { cn } from '../../lib/utils' 5 6 export type ToastVariant = 'success' | 'error' | 'warning' | 'info' 7 8 export interface ToastProps { 9 open: boolean 10 onOpenChange: (open: boolean) => void 11 variant?: ToastVariant 12 title: string 13 description?: string 14 duration?: number 15 } 16 17 const variantConfig: Record<ToastVariant, { icon: React.ReactNode; color: string; bg: string }> = { 18 success: { 19 icon: <CheckCircle size={16} aria-hidden="true" />, 20 color: 'oklch(65.4% 0.17 162)', 21 bg: 'oklch(65.4% 0.17 162 / 0.1)', 22 }, 23 error: { 24 icon: <XCircle size={16} aria-hidden="true" />, 25 color: 'oklch(60% 0.22 25)', 26 bg: 'oklch(60% 0.22 25 / 0.1)', 27 }, 28 warning: { 29 icon: <AlertTriangle size={16} aria-hidden="true" />, 30 color: 'oklch(72% 0.20 50)', 31 bg: 'oklch(72% 0.20 50 / 0.1)', 32 }, 33 info: { 34 icon: <Info size={16} aria-hidden="true" />, 35 color: 'oklch(65% 0.18 264)', 36 bg: 'oklch(52.4% 0.22 264 / 0.1)', 37 }, 38 } 39 40 export function Toast({ 41 open, 42 onOpenChange, 43 variant = 'info', 44 title, 45 description, 46 duration = 4000, 47 }: ToastProps) { 48 const cfg = variantConfig[variant] 49 return ( 50 <RadixToast.Provider> 51 <RadixToast.Root 52 open={open} 53 onOpenChange={onOpenChange} 54 duration={duration} 55 className={cn( 56 'pointer-events-auto flex items-start gap-3 rounded-[var(--radius-md,6px)]', 57 'border border-[oklch(38%_0_0)] px-4 py-3', 58 'shadow-lg backdrop-blur-sm', 59 'data-[state=open]:animate-in data-[state=closed]:animate-out', 60 'data-[state=closed]:fade-out-80 data-[state=open]:fade-in-0', 61 'data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full', 62 'min-w-[300px] max-w-[420px]' 63 )} 64 style={{ 65 backgroundColor: `color-mix(in oklch, ${cfg.bg}, oklch(20% 0 0) 65%)`, 66 borderLeft: `3px solid ${cfg.color}`, 67 }} 68 > 69 <span className="mt-0.5 shrink-0" style={{ color: cfg.color }}> 70 {cfg.icon} 71 </span> 72 <div className="flex-1"> 73 <RadixToast.Title className="text-sm font-semibold text-[oklch(95%_0_0)]"> 74 {title} 75 </RadixToast.Title> 76 {description && ( 77 <RadixToast.Description className="mt-0.5 text-xs text-[oklch(65%_0_0)]"> 78 {description} 79 </RadixToast.Description> 80 )} 81 </div> 82 <RadixToast.Close 83 className="shrink-0 rounded p-0.5 text-[oklch(55%_0_0)] opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[oklch(52.4%_0.22_264)]" 84 aria-label="Close notification" 85 > 86 <X size={14} aria-hidden="true" /> 87 </RadixToast.Close> 88 </RadixToast.Root> 89 <RadixToast.Viewport className="fixed bottom-0 right-0 z-[100] flex max-h-screen flex-col-reverse gap-2 p-4" /> 90 </RadixToast.Provider> 91 ) 92 }