Files
audio-ui/components/ui/Toast.tsx
Sebastian Krüger cbd9ad03fc fix: improve toast notification readability
Enhanced toast notifications with better contrast and visibility:
- Increased background opacity from /10 to /90 for all variants
- Changed to thicker border (border-2) for better definition
- Added backdrop-blur-sm for depth and clarity
- Improved text contrast:
  * Success/Error/Info: White text on colored backgrounds
  * Warning: Black text on yellow background
  * Default: Uses theme foreground color
- Enhanced shadow (shadow-2xl) for better separation
- Added aria-label to close button for accessibility

Toast notifications are now clearly readable in both light and dark modes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 17:15:20 +01:00

136 lines
3.6 KiB
TypeScript

'use client';
import * as React from 'react';
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils/cn';
export interface Toast {
id: string;
title?: string;
description?: string;
variant?: 'default' | 'success' | 'error' | 'warning' | 'info';
duration?: number;
}
interface ToastContextType {
toasts: Toast[];
addToast: (toast: Omit<Toast, 'id'>) => void;
removeToast: (id: string) => void;
}
const ToastContext = React.createContext<ToastContextType | undefined>(
undefined
);
export function useToast() {
const context = React.useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within ToastProvider');
}
return context;
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<Toast[]>([]);
const addToast = React.useCallback((toast: Omit<Toast, 'id'>) => {
const id = Math.random().toString(36).substring(2, 9);
const newToast: Toast = { id, ...toast };
setToasts((prev) => [...prev, newToast]);
// Auto remove after duration
const duration = toast.duration ?? 5000;
if (duration > 0) {
setTimeout(() => {
removeToast(id);
}, duration);
}
}, []);
const removeToast = React.useCallback((id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
return (
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
{children}
<ToastContainer toasts={toasts} onRemove={removeToast} />
</ToastContext.Provider>
);
}
function ToastContainer({
toasts,
onRemove,
}: {
toasts: Toast[];
onRemove: (id: string) => void;
}) {
if (toasts.length === 0) return null;
return (
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-md w-full pointer-events-none">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onRemove={onRemove} />
))}
</div>
);
}
function ToastItem({
toast,
onRemove,
}: {
toast: Toast;
onRemove: (id: string) => void;
}) {
const variant = toast.variant ?? 'default';
const icons = {
default: Info,
success: CheckCircle,
error: AlertCircle,
warning: AlertTriangle,
info: Info,
};
const Icon = icons[variant];
return (
<div
className={cn(
'flex items-start gap-3 rounded-lg border-2 p-4 shadow-2xl pointer-events-auto',
'animate-slideInFromRight backdrop-blur-sm',
{
'bg-card/95 border-border text-foreground': variant === 'default',
'bg-success/90 border-success text-white dark:text-white':
variant === 'success',
'bg-destructive/90 border-destructive text-white dark:text-white':
variant === 'error',
'bg-warning/90 border-warning text-black dark:text-black':
variant === 'warning',
'bg-info/90 border-info text-white dark:text-white': variant === 'info',
}
)}
>
<Icon className="h-5 w-5 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
{toast.title && (
<div className="font-semibold text-sm">{toast.title}</div>
)}
{toast.description && (
<div className="text-sm opacity-90 mt-1">{toast.description}</div>
)}
</div>
<button
onClick={() => onRemove(toast.id)}
className="flex-shrink-0 rounded-md p-1 hover:bg-black/20 dark:hover:bg-white/20 transition-colors"
aria-label="Close"
>
<X className="h-4 w-4" />
</button>
</div>
);
}