From e7e492c56814eddff28be3e77ad1d804420a7498 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 24 Sep 2026 18:33:35 +0700 Subject: [PATCH] a11y: fix lime accent contrast (P3) + add shared EmptyState component (P4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Light-mode --accent-foreground changed from white (2.35:1, fails WCAG AA) to near-black 0 0% 10% (~9.5:1) so filled lime CTAs/badges are readable. - Add src/components/EmptyState.tsx: a single reusable empty-state with icon/emoji, title, hint, action slot(s) and optional footer link, to consolidate the ~39 ad-hoc empty states over time. Action slot turns dead-ends into loops (e.g. "No crews nearby → Create one"). Co-authored-by: shakespeare.diy --- src/components/EmptyState.tsx | 134 ++++++++++++++++++++++++++++++++++ src/index.css | 6 +- 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/components/EmptyState.tsx diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx new file mode 100644 index 0000000..003139c --- /dev/null +++ b/src/components/EmptyState.tsx @@ -0,0 +1,134 @@ +import type { ComponentType, ReactNode } from 'react'; +import { Link } from 'react-router-dom'; +import { cn } from '@/lib/utils'; + +/** + * Shared empty-state component. + * + * Before this existed the app had ~39 ad-hoc empty states, each free to drift + * in spacing, tone and — crucially — whether they offered a way *out*. The + * `action` slot is the valuable part: "No crews nearby → Create one" turns a + * dead-end into a loop. Prefer this component over hand-rolling new ones. + */ + +export interface EmptyStateAction { + /** Button label. */ + label: string; + /** Internal route to navigate to (renders a ). */ + to?: string; + /** Click handler (renders a + + ); + } + + return ( + + ); +} + +export function EmptyState({ + icon: Icon, + emoji, + title, + hint, + actions, + footer, + className, +}: EmptyStateProps) { + return ( +
+ {(Icon || emoji) && ( +
+ {emoji ? ( + + ) : ( + Icon && + )} +
+ )} + +

{title}

+ + {hint && ( +

{hint}

+ )} + + {actions && actions.length > 0 && ( +
+ {actions.map((action) => ( + + ))} +
+ )} + + {footer && ( + + {footer.label} + + )} +
+ ); +} + +export default EmptyState; diff --git a/src/index.css b/src/index.css index f5d1c3a..54756ad 100644 --- a/src/index.css +++ b/src/index.css @@ -24,9 +24,11 @@ --muted: 0 0% 96%; --muted-foreground: 0 0% 40%; - /* Darker neon lime for light mode */ + /* Darker neon lime for light mode. + Foreground is near-black (~9.5:1 on lime) — white (2.35:1) fails WCAG AA, + and lime is the "go"/CTA color, so its filled state must stay readable. */ --accent: 75 80% 40%; - --accent-foreground: 0 0% 100%; + --accent-foreground: 0 0% 10%; --destructive: 0 84% 60%; --destructive-foreground: 0 0% 100%;