Skip to content

mobile: hall menu v2 — plate selection, bottom summary bar, expanded sheet, nutrition label #91

Closed

Description

@TSVRN9

Part of #87 (wave 3). Blocked by: #90.

Rebuild halls/[slug] per the canvas:

  • Dish rows gain a + button → stepper when in the plate; info affordance → nutrition label screen (full FDA-style label per the canvas — requires extending shared's parseCategoryItems to also capture the data-*-dv attributes, which it currently drops; shared change with red-first tests, additive so web is unaffected).
  • Plate: in-memory selection state; bottom bar (items · cal + macro line + LOG button, canvas styling) appears when non-empty; tap bar → expanded sheet (steppers, per-item cal, totals grid, LOG N ITEMS); one tap writes one LogEntry per item at loggedAt=now via existing SqliteLogStorage.
  • OpenFoodFacts entry point: the sheet's "Add something else" row → search UI using shared's existing openFoodFacts client (SEARCH only; barcode scanning needs a native dep — explicitly out of scope, note it).
  • Post-log comparison prompt parity is NOT in scope (web-only today; keep mobile's flow unchanged apart from the plate).

TDD: plate math (totals, add/remove/step) red-first in jest as pure functions; emulator verification with screenshots (bar collapsed/expanded, label screen, OFF search) — the plate-vs-content occlusion class of bug (see PR #78/#84 history) must be checked: last dish row reachable with the bar up. Tokens/components only; jest + tsc + shared tests. Standing policy per #87; coordinate emulator use.

Activity

  1. TSVRN9 commented on Aug 19, 2026

    @TSVRN9
    OwnerAuthor

    Implemented in PR #106 (branch mobile/plate-logging, stacked on mobile/swipe-shell / PR #104 per standing policy). Not merged — left for review.

    What changed

    • shared (umassDining.ts, types.ts): parseCategoryItems now also captures the data-*-dv attributes it was dropping — 8 new optional NutritionFacts fields (totalFatDv, satFatDv, cholesterolDv, sodiumDv, totalCarbDv, dietaryFiberDv, sugarsDv, proteinDv, all number | null). Additive/optional, so web is unaffected (verified: no existing constructor sets them, no web spec asserts exact nutrition-object shape). Includes the real feed's data-cholesterol_dv underscore quirk (every other -dv attribute is hyphenated).
    • shared (openFoodFacts.ts): the issue named "shared's existing openFoodFacts client" as SEARCH-capable, but only lookupBarcode (barcode-only) existed. Added searchProducts (OFF's cgi/search.pl, field-slimmed) and extracted the nutriments→NutritionFacts mapper so both functions share the same per-serving/100g-fallback logic. This file is outside the issue's enumerated file ownership — touched because the premise required it; called out for review.
    • mobile/src/lib/plate.ts: pure in-memory plate math — add/increment, stepper set/step, totalItemCount, toLogEntries, and listBottomPadding (the occlusion-fix helper, see below).
    • mobile/src/lib/nutritionLabel.ts: FDA-label row order/formatting (buildLabelRows, DV% formatting, blank-DV vs. no-DV-column distinction).
    • mobile/src/components/{NutritionLabel,PlateBar,PlateSheet}.tsx: new RN <Modal>-based components (not routed screens — see structural note below), each with a render smoke test.
    • mobile/src/app/halls/[slug].tsx: rebuilt — dish rows get a +/stepper and an info affordance; bottom bar shown once the plate is non-empty, opens the expanded sheet; LOG writes one LogEntry per plate row (servings = that row's count, not one row per unit — matches today.tsx's existing "Dish × N" display and rank.tsx's dedupe-by-dish-key comparison pool). Favorite-star and filter/section logic left untouched. Post-log comparison-prompt parity intentionally not added (out of scope per issue).
    • _layout.tsx untouched — NutritionLabel/PlateSheet are RN Modals, not routed Stack.Screens, since MenuItem doesn't need to survive Expo Router's string-only search-param serialization and neither needs its own back-stack entry.
    • Occlusion fix (bug class from PR web: rank-informed surfaces -- post-log compare prompt + top dishes on home #78/web: close the compare-prompt cluster (#80, #81, #82) #84): the dish list's contentContainerStyle.paddingBottom tracks the plate bar's onLayout-measured height, and listBottomPadding collapses it to 0 once the plate empties — even though the bar's last-measured height is still sitting in state. PlateBar also pads for the bottom safe-area inset itself (useSafeAreaInsets, already provided by expo-router's root — no _layout.tsx change).

    Red/green (TDD)

    All red evidence captured by writing the test first, running it against pre-implementation code, confirming failure, then implementing:

    • shared/src/umassDining.test.ts (%DV parsing): red — AssertionError: undefined !== 4 (captured via git stash of the implementation, re-run, git stash pop) → green after implementing.
    • shared/src/openFoodFacts.test.ts (searchProducts): red — SyntaxError: ... does not provide an export named 'searchProducts' → green after implementing (existing lookupBarcode tests unaffected, confirmed unchanged output shape).
    • mobile/src/lib/plate.test.ts: red — Cannot find module './plate' → green (18 tests, including listBottomPadding's stale-height case).
    • mobile/src/lib/nutritionLabel.test.ts: red — Cannot find module './nutritionLabel' → green (8 tests).
    • Component smoke tests (PlateBar, NutritionLabel, PlateSheet) written against the finished components (render + interaction wiring, not red-first, since they're integration-style checks of already-implemented UI).

    Verification

    • pnpm --filter @udine/shared test — 116/116 pass.
    • cd mobile && npx tsc --noEmit — clean.
    • pnpm --filter mobile test — 19 suites / 101 tests pass.
    • pnpm --filter web check — pre-existing failures only (missing web/.env in this worktree; confirmed unrelated by stashing the diff and re-running against identical worktree state).
    • Emulator/visual pass not done. The shared main checkout was on fix/oauth-cold-start for most of this session and only moved to main near the end; couldn't independently confirm it was clean/free (worktree isolation blocks git calls against the shared checkout), and reviewers were noted as occupying the emulator. Flagging for the orchestrator rather than risking a conflict — screenshots (bar collapsed/expanded, label screen, OFF search, last-row reachability) are still outstanding.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions