From eaaf5cfebbf90b9974712ae29f3a0334b6a08696 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Tue, 26 May 2026 19:17:39 +0300 Subject: [PATCH] fix(toast): drop side-stripe ban + a11y polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per /impeccable audit: Toast.tsx had a 4px border-l accent on top of a 1px full border — the explicit 'side-stripe' absolute ban. Toast is on screen after every notify.* call, so the violation repeated constantly. Polish pass: • Replace 'border border-l-4 border-dark-700 ${style.border}' with a single tinted full border (border-success-500/40, etc). Semantic still carries through the colored icon box + matching border tint. • Drop bg-tint flood — the icon box at 40px is enough at this scale. • Reduce shadow weight (shadow-2xl shadow-black/50 → shadow-xl shadow-black/30). Floating layer needs elevation, not drama. • Soften interaction scale (1.02 hover → 1.01, 0.98 active → 0.99) to match a calmer overall feel. • Thinner progress bar (h-1 → h-0.5) — the countdown is ambient, not primary content. A11y: • Container: role='region' + aria-label + aria-live='polite' so SR announces incoming toasts without stealing focus. • Toast: role='alert' for type=error (interrupts SR — appropriate for failures), role='status' for everything else. • Toast: tabIndex=0 + Enter/Space to activate, Escape to dismiss — was click-only before, useless on keyboard. • focus-visible: 2px ring at accent-500/60 with offset for visibility on the dark backdrop. • Icon box aria-hidden — it's decorative reinforcement, the role + text content is what SR reads. • Progress bar aria-hidden — visual countdown only. shrink keyframe already used transform: scaleX (no layout thrash) and slide-in-right keyframe already used ease-out-expo — both align with motion laws as-is, kept unchanged. --- src/components/Toast.tsx | 66 ++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 19 deletions(-) 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. */} +