diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 48abd30..06f551f 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -95,8 +95,14 @@ export function ToastProvider({ children }: { children: ReactNode }) { {children} - {/* Toast Container — safe area aware, adaptive width */} -
+ {/* Toast region — safe area aware, adaptive width. role+aria-live lets + screen readers announce arriving toasts without stealing focus. */} +
{toasts.map((toast) => ( removeToast(toast.id)} /> ))} @@ -111,35 +117,51 @@ function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) { onClose(); }; + // Semantic carries through the icon box + a full tinted border. No side + // stripe (was a 4px border-l accent — impeccable absolute ban) and no + // background tint flood — the contained icon is enough at this size. const typeStyles = { success: { - border: 'border-l-success-500', + border: 'border-success-500/40', icon: 'text-success-400', - iconBg: 'bg-success-500/20', - progress: 'bg-success-400', + iconBg: 'bg-success-500/15', + progress: 'bg-success-500', }, error: { - border: 'border-l-error-500', + border: 'border-error-500/40', icon: 'text-error-400', - iconBg: 'bg-error-500/20', - progress: 'bg-error-400', + iconBg: 'bg-error-500/15', + progress: 'bg-error-500', }, warning: { - border: 'border-l-warning-500', + border: 'border-warning-500/40', icon: 'text-warning-400', - iconBg: 'bg-warning-500/20', - progress: 'bg-warning-400', + iconBg: 'bg-warning-500/15', + progress: 'bg-warning-500', }, info: { - border: 'border-l-accent-500', + border: 'border-accent-500/40', icon: 'text-accent-400', - iconBg: 'bg-accent-500/20', - progress: 'bg-accent-400', + iconBg: 'bg-accent-500/15', + progress: 'bg-accent-500', }, }; const style = typeStyles[toast.type || 'info']; + // Errors interrupt the screen reader; everything else announces politely. + const role = toast.type === 'error' ? 'alert' : 'status'; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleClick(); + } else if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + }; + const defaultIcons = { success: ( void }) { return (
- {/* Icon */} + {/* Icon — carries the semantic by itself; the border is a soft echo */}
- {/* Progress bar */} -
+ {/* Progress bar — visual countdown until auto-dismiss. scaleX animates + on the compositor, no layout reflow. aria-hidden because the visual + timer doesn't carry meaning beyond the toast lifetime. */} +