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
@@ -0,0 +1,76 @@
-- Dry-run: show which rows WOULD change under the aisle normalization
-- migration 0015. Safe to run against any database; makes no changes.
--
-- Usage (from the deployment host):
-- psql "$DATABASE_URL" -f backend/scripts/dry_run_aisle_migration.sql
WITH src AS (
SELECT
'ingredient'::text AS tbl,
id::text AS id,
aisle AS old_aisle,
LOWER(COALESCE(aisle, '')) AS key
FROM ingredient
WHERE aisle IS NOT NULL
UNION ALL
SELECT
'grocery_item'::text,
id::text,
aisle,
LOWER(COALESCE(aisle, ''))
FROM grocery_item
WHERE aisle IS NOT NULL
),
mapped AS (
SELECT
tbl, id, old_aisle,
CASE key
WHEN 'canned goods' THEN 'Pantry'
WHEN 'canned' THEN 'Pantry'
WHEN 'freezer' THEN 'Frozen'
WHEN 'frozen' THEN 'Frozen'
WHEN 'produce' THEN 'Produce'
WHEN 'fruit' THEN 'Produce'
WHEN 'vegetable' THEN 'Produce'
WHEN 'dairy' THEN 'Dairy & Eggs'
WHEN 'eggs' THEN 'Dairy & Eggs'
WHEN 'cheese' THEN 'Dairy & Eggs'
WHEN 'milk' THEN 'Dairy & Eggs'
WHEN 'yogurt' THEN 'Dairy & Eggs'
WHEN 'meat' THEN 'Meat & Seafood'
WHEN 'seafood' THEN 'Meat & Seafood'
WHEN 'fish' THEN 'Meat & Seafood'
WHEN 'chicken' THEN 'Meat & Seafood'
WHEN 'beef' THEN 'Meat & Seafood'
WHEN 'pork' THEN 'Meat & Seafood'
WHEN 'meat_seafood' THEN 'Meat & Seafood'
WHEN 'bakery' THEN 'Bakery'
WHEN 'bread' THEN 'Bakery'
WHEN 'beverage' THEN 'Beverages'
WHEN 'beverages' THEN 'Beverages'
WHEN 'drinks' THEN 'Beverages'
WHEN 'spice' THEN 'Spices'
WHEN 'spices' THEN 'Spices'
WHEN 'seasoning' THEN 'Spices'
WHEN 'pantry' THEN 'Pantry'
WHEN 'dry' THEN 'Pantry'
WHEN 'snack' THEN 'Pantry'
WHEN 'snacks' THEN 'Pantry'
ELSE 'Other'
END AS new_aisle
FROM src
)
SELECT tbl,
COUNT(*) AS rows_to_change,
COUNT(DISTINCT old_aisle) AS distinct_old_values
FROM mapped
WHERE old_aisle IS DISTINCT FROM new_aisle
GROUP BY tbl
ORDER BY tbl;
-- Optional detail dump (uncomment to inspect actual rows):
-- SELECT tbl, old_aisle, new_aisle, COUNT(*)
-- FROM mapped
-- WHERE old_aisle IS DISTINCT FROM new_aisle
-- GROUP BY tbl, old_aisle, new_aisle
-- ORDER BY tbl, old_aisle;