feat(ui): close 3 P2 audit findings + a11y sweep (Sprint 3)

- lib/toast.tsx (renamed from .ts for JSX): new showToast.undo(message,
  onUndo, ms=5000) helper. Inline 'Undo' button dismisses the toast and
  fires onUndo. Note: react-hot-toast 2.6 lacks onClose/onDismiss, so
  expiry is silent — same effective behavior as confirm() declined.

- Dashboard.handleDelete: captures the full MealPlanItem before the
  DELETE so Undo can re-fire meals.generateItem(planId, dayOfWeek,
  mealType) and refill the slot (recipe may differ — see plan R4).

- Pantry.handleRemove: fully reversible — Undo re-fires pantry.add with
  the original ingredient_id, quantity, and unit. New removeId state
  scopes the spinner to the clicked row.

- Both confirm() call sites removed.

- App.tsx Navigation: whitespace-nowrap + px-2 sm:px-3 so all 4 links fit
  on one line down to 360 px. aria-current='page' on the active link.
  <nav aria-label='Primary'>, <main id='main-content'>.

- components/ui/Badge: optional icon and aria-label props. Dashboard
  approval-status Badge passes aria-label='Approval status: approved'
  (or the current value) so screen readers don't rely on color alone.

- ErrorBoundary already mounted at App.tsx:42 — verified, no code change.

- Review/sprint3-verification.md (new) + Review/ui-nielsen-audit.md and
  fix-ui-audit.md updated with Sprint 3 status and deploy steps.

Build: npm run build (tsc + vite) green. tsc 0 errors.
This commit is contained in:
2026-06-03 18:09:35 -07:00
parent f5fb7558c4
commit e90a9d6683
8 changed files with 199 additions and 39 deletions
+7 -3
View File
@@ -1,12 +1,15 @@
import { cn } from '../../lib/utils';
import type { ReactNode } from 'react';
interface BadgeProps {
variant?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral';
children: React.ReactNode;
children: ReactNode;
className?: string;
icon?: ReactNode;
'aria-label'?: string;
}
export function Badge({ variant = 'neutral', children, className }: BadgeProps) {
export function Badge({ variant = 'neutral', children, className, icon, 'aria-label': ariaLabel }: BadgeProps) {
const variants = {
primary: 'bg-primary-50 text-primary-700 border border-primary-200',
success: 'bg-success-50 text-success-700 border border-success-200',
@@ -17,7 +20,8 @@ export function Badge({ variant = 'neutral', children, className }: BadgeProps)
};
return (
<span className={cn('badge', variants[variant], className)}>
<span className={cn('badge', variants[variant], className)} aria-label={ariaLabel}>
{icon}
{children}
</span>
);