feat(ui): close 6 P1 audit findings + 1 bonus mobile fix (Sprint 2)

- Dashboard MealCard: title truncate -> line-clamp-2, image shrinks to
  40x40 on <md to give the title room (B6).
- MealDetail: hero reworked to normal flow with stronger gradient;
  description runs through new cleanDescription() helper that strips
  14 spoonacular SEO patterns and trims to the last full sentence.
  Raw description moved to a 'Notes from source' disclosure (B7).
- Pantry: free-text aisle/unit replaced with <Select> populated from
  the new PANTRY_AISLES canonical enum; ingredient name field marked
  required. New PANTRY_AISLES export + PantryAisle type in types (B8).
- backend: alembic 0015_normalize_pantry_aisles maps free-text
  ingredient.aisle and grocery_item.aisle to canonical labels in a
  single transaction; downgrade raises (restore from snapshot).
  backend/scripts/dry_run_aisle_migration.sql is the read-only
  preview helper.
- ShoppingList: human-readable AISLE_LABEL map replaces raw snake_case
  aisle keys; 3-col stat grid with compact mobile sizing (B9 + S3.3).
- Pantry table: role/aria-label region and a right-edge white
  gradient hint at mobile horizontal overflow (B10).
- Recipes: pending/applied filter split, Apply and Reset buttons,
  active-count chip on the Filters button, role=region + aria-label
  on the panel (B11).
- Review/sprint2-verification.md and fix-ui-audit.md updated.

Build: npm run build (tsc + vite) green. tsc emits 0 errors.

Co-located audit + plan docs kept in sync: Review/ui-nielsen-audit.md
gains a Sprint 2 status block; fix-ui-audit.md has implementation
notes for each Sprint 2 task.
This commit is contained in:
2026-06-03 17:36:26 -07:00
parent 36038bb9cb
commit ccc70aaf72
12 changed files with 728 additions and 81 deletions
+49 -17
View File
@@ -2,14 +2,20 @@ import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Plus, Search, Trash2, Package, AlertTriangle } from 'lucide-react'
import { mealPlannerApi } from '../api'
import type { HomePantryItem, Ingredient } from '../types'
import { PANTRY_AISLES, type HomePantryItem, type Ingredient } from '../types'
import { Button } from '../components/ui/Button'
import { Card, CardBody } from '../components/ui/Card'
import { Input } from '../components/ui/Input'
import { Select } from '../components/ui/Select'
import { Skeleton, SkeletonText } from '../components/ui/Skeleton'
import { EmptyState } from '../components/ui/EmptyState'
import { showToast } from '../lib/toast'
const AISLE_OPTIONS = [
{ value: '', label: 'Select aisle…' },
...PANTRY_AISLES.map(a => ({ value: a, label: a })),
]
export default function Pantry() {
const queryClient = useQueryClient()
const [showAddForm, setShowAddForm] = useState(false)
@@ -174,10 +180,11 @@ export default function Pantry() {
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="md:col-span-2">
<Input
label="Ingredient name"
label="Ingredient name *"
value={ingredientName}
onChange={(e) => setIngredientName(e.target.value)}
placeholder="e.g., Avocado"
required
/>
{matchedIngredient && (
<p className="mt-1 text-xs text-success-600">Matched existing ingredient </p>
@@ -190,17 +197,32 @@ export default function Pantry() {
onChange={(e) => setQuantity(e.target.value)}
placeholder="e.g., 5"
/>
<Input
<Select
label="Unit"
value={unit}
onChange={(e) => setUnit(e.target.value)}
placeholder="cans, lbs, etc."
options={[
{ value: '', label: 'Select unit…' },
{ value: 'each', label: 'each' },
{ value: 'g', label: 'g' },
{ value: 'kg', label: 'kg' },
{ value: 'oz', label: 'oz' },
{ value: 'lb', label: 'lb' },
{ value: 'ml', label: 'ml' },
{ value: 'l', label: 'l' },
{ value: 'cup', label: 'cup' },
{ value: 'tbsp', label: 'tbsp' },
{ value: 'tsp', label: 'tsp' },
{ value: 'can', label: 'can' },
{ value: 'bunch', label: 'bunch' },
{ value: 'clove', label: 'clove' },
]}
/>
<Input
<Select
label="Aisle"
value={aisle}
onChange={(e) => setAisle(e.target.value)}
placeholder="e.g., Produce"
options={AISLE_OPTIONS}
/>
<div className="flex items-end">
<Button
@@ -233,17 +255,22 @@ export default function Pantry() {
{/* Items List */}
{filteredItems && filteredItems.length > 0 ? (
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Item</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Aisle</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Quantity</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Expires</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-surface-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<div className="relative">
<div
className="overflow-x-auto"
role="region"
aria-label="Pantry items, scroll horizontally to see all columns"
>
<table className="w-full">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Item</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Aisle</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Quantity</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-surface-500 uppercase tracking-wider">Expires</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-surface-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-surface-200">
{filteredItems.map((item) => {
const isExpiringSoon = item.expires_at &&
@@ -302,6 +329,11 @@ export default function Pantry() {
})}
</tbody>
</table>
</div>
<div
className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-white to-transparent md:hidden"
aria-hidden="true"
/>
</div>
</Card>
) : (