Skip to content

feat: pre-launch UX overhaul (mobile/Flutter) — full audit, Waves 0–3 - #155

Merged
VoMinhKhoii merged 60 commits into
mainfrom
feat/ux-wave0-mobile
Jun 28, 2026
Merged

feat: pre-launch UX overhaul (mobile/Flutter) — full audit, Waves 0–3#155
VoMinhKhoii merged 60 commits into
mainfrom
feat/ux-wave0-mobile

Conversation

@VoMinhKhoii

@VoMinhKhoii VoMinhKhoii commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Pre-launch UX overhaul — mobile / Flutter (Waves 0–3, full audit)

Implements docs/ux-audit-2026-06-11.md end-to-end for the Flutter app (apps/mobile-flutter) — 30 commits this session. Founder design direction honored: bounded estimates/confidence stay internal; voice through component design, not prose.

Gate (whole branch): flutter analyzeNo issues found (only pre-existing SPM deprecation warnings for sign_in_with_apple/flutter_secure_storage). Native build / TestFlight not run from this PR.

⚠️ This PR restructures the shell, onboarding, and several screens — please device-smoke-test before merge (you're already on vmkhoiii). Exact flows to check are listed under "Needs device sign-off".


✅ Wave 0 — compliance & cold start

  • Sign in with Apple (hashed-nonce flow) · account section (delete/export/sign-out → /api/v1/account) · forgot-password · branded cream cold-start + wordmark splash.

✅ Wave 1 — the core loop

  • Streaming-reveal morph — the streaming card transforms in place into the confirmable meal (per-row crossfade, totals count up ~600ms, Edit/Confirm slides in, lightImpact, ScrollController) instead of unmount-then-pop · never destroy typed input + failed-attempt retry card + ~20s reassurance · over-target state + consumed-fills ring unification + overflow arc · iOS swipe-to-remove + undo on persisted cards · strike-row at qty 0 · localized greeting · locale number/weekday formatting · partial-day + legacy-macro notices ported.

✅ Wave 2 — navigation

  • Bottom tab bar (Today/Log/Patterns; cream, hairline, no elevation, keyboard-hide, selectionClick, PopScope) — drawer deleted · Settings → header avatar → Cupertino swipe-back · ergonomics (44pt targets, whole-header tap, week PageView, pull-to-refresh).

✅ Wave 3 — surface depth

  • Dashboardpaged day-viewer (PageView synced to the week strip, per-day slice fetch, date line, count-up, bars retarget without remount) · first-run staging (day-0 single Lora card; "% on track" suppressed until data exists) · dead weight_trend/weight_chart_utils/progressStatus deleted.
  • Nutritiondetail route (Cupertino swipe-back; food candidates as full rows with serving/rationale/caution; honest pointMode sparkline) · re-tokened to solid dashboard_tokens cards, eyebrows collapsed · iOS-hands (bouncing scroll, pull-to-refresh, copyWithPrevious so a failed refetch keeps content, overviewToast wired).
  • Settings — flattened to grouped rows with current-value sublines + focused editors (Cupertino swipe-back) · instant-commit toggles vs save-bar-for-numeric · felt save ("Changes saved · 2,150 kcal/day", count-up, haptic) · error-state un-masquerade · fabricated 65/165/25 defaults fixed · About (version/privacy/terms).
  • Auth + onboardingpre-auth welcome screen (Lora wordmark + typing demo → result chip + Apple/Google/email, sign-in/up split collapsed) replacing the "Welcome back" login wall · real confirm-email state + 30s resend cooldown · OAuth "Finishing sign-in…" pending across the Safari app-switch · step-2 de-leak (localized strings, sex→2-segment strip, flag-emoji→monogram, flat target card) · native CountrySelect bottom sheet (grabber, pinned search, Việt Nam pinned) · /welcome counts the target up in Lora 40.

✅ Craft

  • First haptic vocabulary · SnackBarThemeData · serifSemiBold deleted (Lora never bold) · real ellipsis · full Lucide DNA — zero Material icons remain app-wide · espresso scrim.

👀 Needs device sign-off (analyze-clean; not run on hardware)

  • Shell/routing: tab-bar nav + system-back, Settings push & swipe-back, the grouped-row settings editors persisting.
  • Onboarding/auth: welcome typing-demo + result-chip spring; Apple/Google buttons; OAuth "Finishing sign-in…" across a real Safari round-trip (and cancel-resume clearing it); confirm-email resend + cooldown; CountrySelect keyboard never covering the list; /welcome count-up; language live-switch with the monogram toggle.
  • Logging: streaming-reveal morph timing + the reveal→persisted seam; swipe-to-remove + undo; yesterday-prompt appearing for an under-logged yesterday.
  • Dashboard: paged day-viewer height animation across days with different meal counts; first-run collapse on a truly-empty account.
  • Nutrition: detail route swipe-back; food-candidate row + caution box; sparkline in pointMode; solid-card re-token.

⏳ Known residuals (small, intentional)

  • Onboarding step-2 two-screen split — skipped: it's byte-coupled to the /api/v1/onboarding/screen step contract (kOnboardingTotalSteps 3→4 remap); needs care, not a blind edit.
  • CountrySelect uses the pre-existing inline overlay in a couple of spots; multi-day nutrient sparkline renders the resolved point honestly (a true series needs a new mobile backend endpoint — the DTO ships only window-averages).
  • Version is a static constant (1.0.1, keep in sync with pubspec); Privacy/Terms copy a placeholder URL (no url_launcher dep / real legal pages yet).

⚙️ Founder prerequisites (code complete; nothing committed)

  • Enable Sign in with Apple capability (Apple Developer + provisioning) + the Apple provider in Supabase Auth.
  • SUPABASE_SERVICE_ROLE_KEY on the backend (shared with web) for account deletion.

🤖 Generated with Claude Code


🔍 Adversarial audit + hardening round (all findings fixed in 8 follow-up commits)

An independent adversarial review verified the branch (analyze clean, zero Material icons confirmed) and found real defects — all fixed:

  • Localized neutral auth errors, inline (the surviving Part 2 CI/CD #6 blocker): raw English Supabase toasts are gone; wrong-password/no-account share the neutral line; email_not_confirmed routes to the confirm-email state; errors render inline in terracotta.
  • Confidence trust-line removed from Patterns (founder direction) + orphaned uncertainty strings deleted.
  • Delete heals the whole app: dashboard bundle + day-pager invalidated; undo rebuilt on a pendingRemovalIds filter — the resurrect race and double-swipe snapshot bug are structurally gone.
  • Reveal lifecycle: a second submit can no longer vaporize an unconfirmed answer (it resurfaces as a pending card); morph seam reduced (matched card background, crossfade instead of row replay); date-switch mid-stream guarded.
  • Instant-commit integrity: mid-flight edits re-queue; failures roll back the control; locale applies only after the PUT succeeds.
  • Honesty + motion: fabricated 0.5 sparkline dot removed; truthful band labels; reduced-motion gates on every new animation.
  • Design sweep: pure-red error card → terracotta; new-code Lora w500 → 400; P: 38g consistency; "412 kcal"; 3-day %-suppression; locale number + "/wk" fixes; sex no longer silently defaults to male; deep-link back affordance; first-run suggestion chips.
  • Bundled brand fonts (last open Part 2 item): local Lora + DM Sans TTFs (full in-use variant set), allowRuntimeFetching = false — cold offline launch no longer renders in Roboto. Vietnamese glyphs verified present in the Lora binaries.

📱 Device smoke-test checklist (the audit's sharpened list — run on vmkhoiii)

  1. Cold start offline (airplane mode, force-quit, relaunch): Lora vi diacritics render; no Roboto flash.
  2. Reveal seam (slow-mo screen recording): streaming→result reads as one card, no background flash, rows crossfade.
  3. Undo race: swipe-remove a meal → immediately confirm another meal within 5s → removed card must NOT pop back; ring + dashboard heal.
  4. Swipe vs scroll: trailing swipe arbitration against vertical feed scroll; snackbar position vs the tab bar.
  5. Pager feel: swipe between a 1-meal and 6-meal day — height animates, no flash; values count up.
  6. Tab bar vs keyboard: open the composer — bar hides without flicker during the keyboard curve.
  7. vi overflow: "CHẤT BÉO" macro labels, "VƯỢT MỤC TIÊU" eyebrow, tab labels, settings sublines.
  8. Auth: wrong password → localized inline error; Apple + Google round-trip; OAuth cancel → overlay clears; confirm-email resend cooldown.
  9. Reduce Motion ON (Settings → Accessibility): no count-ups, no pulses, static streaming card.
  10. Settings: instant-commit toggle while offline → rolls back with retry; language switch persists.

Summary by CodeRabbit

  • New Features
    • Added Apple sign-in, forgot-password, and confirm-email flow improvements.
    • Introduced account export, sign-out, and typed account deletion.
    • Updated nutrition and dashboard experiences with new range selector, “day summary,” suggested foods, and source citations.
  • Bug Fixes
    • Improved streaming meal-analysis handling (reveal, retry, discard) and swipe-to-remove undo behavior.
    • Better auth error messaging and session “finishing sign-in” recovery.
  • Style
    • Refreshed UI copy (Loading… ellipsis), icon set, typography, and improved skeleton/empty states across screens.
  • Other
    • Added locale-aware count formatting and “partial yesterday” nudge behavior.

🆕 Update (this session) — Nutrition tab redesign + backend 1d range

Supersedes the Wave 3 "Nutrition detail route" bullet above. The per-nutrient detail screen was removed; all info now lives in one dense view.

Mobile — Nutrition tab rebuilt as a single dense view

  • Header date toggle (Today / 7d / 30d) moved out of the card onto the burger line; equal-width iOS segmented control with sliding thumb.
  • Compact day summary — calories hero (sans, not Lora) + macro legend; top-right target chip (up/down + target kcal, hidden when no goal set); macro distribution line-area chart for 7d/30d (stacked P/C/F kcal, dashed gridlines, y-axis labels incl. 2500/3000); composition for Today.
  • 2-column nutrient grid grouped Vitamins / Minerals with inline progress bars; target-met floor cards greened (an exceeded ceiling like sodium is not greened). ~20 micronutrients render.
  • Single CTA → "Suggested foods" sheet: the most under-target nutrients + DB-derived food sources for any nutrient. Tap a chip to reserve it; "Don't have these?" cycles the rest from a pre-fetched pool (no refetch). Long names ellipsize (chip width clamped).
  • FAO/WHO source attribution caption + citations sheet (WHO/FAO 2004, Vietnam RDA 2016, NASEM DRI).
  • All toasts moved to the top globally (showTopToast, success/error variants) — every bottom SnackBar converted.
  • Drawer hierarchy clarified (avatar + name + email).

Backend (web, same branch) — 1d "Today" range + DB-derived foods

  • Added a real 1d range end-to-end (date-range, summary, mapper, contracts) so "Today" returns single-day intake. ⚠️ Must deploy before mobile "Today" works in prod (works now on the branch dev server).
  • Sex-neutral default targets: when biological sex is unknown, resolve the mean of the male/female RDA instead of "no target" (previously the Implement chat area for meal logging with message handling #1 cause of empty cards). Verified on real data (e.g. Vitamin A = (600+500)/2 = 550 µg).
  • Food candidates derived from the composition DB for any nutrient (no curated list), ranked by per-100g density, with a quality filter: excludes non-portionable/processed/fortified categories (spices, breakfast cereals, oils, sweets, beverages, baked/leavening, sauces) + fortified/formulated name markers. Verified: vitamin K → parsley/amaranth; folate → liver/beans; vitamin E → sunflower seeds (was: dried thyme, CAP'N CRUNCH, SLIMFAST).

Targets fact-check (this session)

WHO/FAO 2004 + NASEM 2019 values (the path a no-country profile resolves) verified against canonical sources — all correct (Ca 1000, Mg 260/220, Zn 7.0/4.9, Vit A 600/500, Vit C 45, Vit K 65/55, folate 400, B12 2.4; NASEM K 3400/2600, Na 2300). Caveats: the Vietnam-RDA-path vitaminA 850 / vitaminK 150 read high vs. WHO/FAO and are worth a spot-check against the 2016 tables (they only fire for VN-country profiles).

Gates

  • flutter analyzeNo issues found; web tsc clean; 89/89 nutrition tests pass; biome clean.

Known gaps / follow-ups (not blocking)

  • VI food names: ~2,200 USDA-sourced foods show English or machine-translated names ("Mứt chiết xuất men"); source-1 (Vietnamese FCT, 526 rows) all have proper VN names. Pre-existing data limitation; the quality filter removes the worst offenders.
  • Iron sex-average = 14 mg under-states a menstruating woman's 18 mg need — real fix is the onboarding sex prompt (drawer already nudges "Step 2 of 3").
  • "Today" view still shows the "Avg per complete day" label (reads oddly for a single day) — minor copy follow-up.
  • Dead code: lib/features/nutrition/logic/{format_date,verdict_logic}.dart are unused (orphaned by the refactor) — left in place; can remove on request.
  • Decisions captured in DEV-62 "DECISIONS" doc on the Tuturuuu DEV board.

VoMinhKhoii and others added 4 commits June 11, 2026 14:21
App Store Guideline 4.8 requires Sign in with Apple wherever a third-party social
login (Google) is offered. Adds the native credential flow (sign_in_with_apple +
hashed-nonce verification through supabase.auth.signInWithIdToken), the official
SignInWithAppleButton placed above Google on iOS/macOS, and the
com.apple.developer.applesignin entitlement wired into all Runner build configs.
Enabling the capability in the Apple Developer account and the Apple provider in
the Supabase dashboard remain account-side steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the launch-blocking account layer to Settings. Account deletion
(App Store Guideline 5.1.1(v)) pushes a type-to-confirm screen in the brand's
calm terracotta register and calls DELETE /api/v1/account, then signs out. Sign
out gains a Cupertino confirmation sheet (previously a one-tap drawer action),
and 'Export my data' fetches GET /api/v1/account and shares the JSON via the
system share sheet. New api_client.deleteAccount()/exportMyData() helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was no password recovery on mobile at all (the forgotPassword string was
translated but never rendered). Adds a 'Forgot password?' link on the sign-in
form that opens a recovery screen: it sends resetPasswordForEmail with a redirect
through the web /auth/callback to the web /reset-password page (reusing the web
reset surface, so no in-app deep-link handling is needed), then holds on a
persistent check-your-email state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first frames were a pure-white native launch (on a cream-paper brand) then a
bare Material spinner. Sets the native LaunchScreen background to cream so there's
no white flash, and replaces the splash spinner with the Lora 'Nhẩm' wordmark
breathing gently on the cream surface (paused under reduced motion). Offline font
bundling (allowRuntimeFetching=false) is deferred — it needs the Vietnamese-subset
static font files and an on-device verify to avoid regressing rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cc0fe144-3939-4297-8168-b6316f5263e2

📥 Commits

Reviewing files that changed from the base of the PR and between 831cd6a and e7c676a.

📒 Files selected for processing (12)
  • apps/mobile-flutter/lib/features/dashboard/widgets/compact_weight_log.dart
  • apps/mobile-flutter/lib/features/nutrition/logic/status.dart
  • apps/mobile-flutter/lib/models/nutrition.dart
  • apps/mobile-flutter/lib/shell/sidebar.dart
  • components/nutrition/rows/nutrient-detail.tsx
  • lib/nutrition/__tests__/catalog/nutrients.test.ts
  • lib/nutrition/__tests__/pattern/aggregation.test.ts
  • lib/nutrition/actions/overview/mapper.ts
  • lib/nutrition/catalog/nutrients.ts
  • lib/nutrition/catalog/reference-targets.ts
  • lib/nutrition/pattern/aggregation.ts
  • lib/nutrition/types.ts
💤 Files with no reviewable changes (1)
  • apps/mobile-flutter/lib/models/nutrition.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/mobile-flutter/lib/features/dashboard/widgets/compact_weight_log.dart

📝 Walkthrough

Walkthrough

Large Flutter mobile update adding Apple Sign-In, a unified auth surface, dashboard day paging, nutrition screen redesign, logging feed lifecycle changes, instant-commit settings editors, and app-wide localization and icon updates.

Changes

Mobile App Feature Refresh

Layer / File(s) Summary
iOS signing and app foundation
ios/Runner.xcodeproj/project.pbxproj, ios/Runner/Runner.entitlements, ios/Runner/Base.lproj/LaunchScreen.storyboard, pubspec.yaml, lib/main.dart
Apple Sign-In entitlements are added, the launch screen background is adjusted, bundled Google Fonts are configured, and shared app dependencies and startup setup change.
Shared UI primitives and theme
lib/theme/nham_colors.dart, lib/theme/nham_theme.dart, lib/theme/nham_typography.dart, lib/shared/widgets/top_toast.dart, lib/shared/widgets/nham_text.dart, lib/shared/widgets/target_progress_bar.dart, lib/features/logging/widgets/count_up.dart, lib/features/dashboard/widgets/skeleton.dart
Theme colors, typography, toast presentation, skeleton loading widgets, text styles, and progress-bar fill behavior are updated or added.
Localization copy updates
assets/l10n/en.json, assets/l10n/vi.json
English and Vietnamese localization resources are updated with new auth, dashboard, logging, nutrition, onboarding, settings, and validation strings, plus typographic ellipsis changes and revised wording.
API client and account endpoints
lib/data/api_client.dart
ApiClient refreshes near-expiry sessions and adds account export and delete endpoints used by settings and auth flows.
Auth flow and welcome surface
lib/features/auth/providers/auth_form_controller.dart, lib/features/auth/widgets/..., lib/features/auth/screens/forgot_password_screen.dart, lib/features/auth/screens/sign_in_screen.dart, lib/features/auth/screens/sign_up_screen.dart
Auth state adds Apple sign-in, pending-email handling, and mapped error messaging; the welcome, email, confirm-email, forgot-password, and OAuth completion screens are introduced or rewired around the unified auth flow.
Dashboard tokens, shell header, and skeletons
lib/features/dashboard/widgets/dashboard_tokens.dart, lib/features/dashboard/widgets/section_header.dart, lib/shell/app_header.dart, lib/shell/sidebar.dart
Dashboard tokens change typography and card styling, skeleton loading widgets are added, and shell header/sidebar controls are simplified and restyled with Lucide icons and toast-based feedback.
Dashboard day paging and metrics
lib/features/dashboard/screens/dashboard_screen.dart, lib/features/dashboard/widgets/today_section.dart, lib/features/dashboard/widgets/week_strip.dart, lib/features/dashboard/widgets/adherence_heatmap.dart, lib/features/dashboard/widgets/calorie_ring.dart, lib/features/dashboard/widgets/compact_weight_log.dart, lib/features/dashboard/widgets/weight_chart.dart, lib/features/dashboard/widgets/floating_meal_trigger.dart, lib/features/dashboard/data/dashboard_providers.dart, lib/features/dashboard/logic/dashboard_format.dart
The dashboard screen adds a paged day model, first-run detection, a loading skeleton, localized greetings, and locale-aware number formatting; day, calorie, adherence, and weight widgets switch to signed/overflow/trend visuals.
Logging date helpers and feed lifecycle
lib/features/logging/data/logging_keys.dart, lib/features/logging/data/logging_providers.dart, lib/features/logging/data/stream_analysis_controller.dart, lib/features/logging/logic/meal_utils.dart, lib/features/logging/screens/logging_screen.dart, lib/features/logging/widgets/feed_area.dart, lib/features/logging/widgets/partial_yesterday_prompt.dart
Logging adds date helpers, partial-day prompting, stream state tracking, and feed reveal/retry/swipe-remove behavior.
Logging meal, input, and timeline widgets
lib/features/logging/widgets/meal_entry.dart, lib/features/logging/widgets/meal_input.dart, lib/features/logging/widgets/streaming_entry.dart, lib/features/logging/widgets/persisted_meal_card.dart, lib/features/logging/widgets/timeline_picker.dart, lib/features/logging/widgets/empty_state.dart, lib/features/logging/widgets/manual_log_sheet.dart, lib/features/logging/widgets/meal_mode_sheet.dart, lib/features/logging/widgets/timeline_rail.dart, lib/features/logging/widgets/dashed_divider.dart
Meal entry and input widgets gain reveal/analyzing behavior, PersistedMealCard adds swipe removal, timeline selection is split into new widgets, streaming and manual logging UIs are simplified, and obsolete timeline/divider widgets are removed.
Nutrition models, screen redesign, and candidate foods
lib/models/nutrition.dart, lib/features/nutrition/providers/nutrition_overview_provider.dart, lib/features/nutrition/providers/candidates_response.dart, lib/features/nutrition/providers/food_candidates_provider.dart, lib/features/nutrition/screens/nutrition_screen.dart, lib/features/nutrition/widgets/...
Nutrition gains day-series models, per-account overview caching, and a redesigned screen with summary, range selector, grid cards, suggested foods, and source attribution; candidate foods become concrete backend-provided items.
Onboarding body metrics, country selection, and welcome target animation
lib/features/onboarding/screens/screen_body_metrics.dart, lib/features/onboarding/screens/welcome_setup_screen.dart, lib/features/onboarding/widgets/country_select.dart, lib/features/onboarding/widgets/language_toggle.dart, lib/features/onboarding/widgets/custom_select.dart, lib/features/onboarding/widgets/onboarding_wizard.dart, lib/features/onboarding/screens/screen_origin.dart
Body metrics, country selection, language toggle, onboarding wizard, and welcome setup widgets are updated to use localization, Lucide icons, better validation, modal selection, and animated calorie-target display.
Settings screen, account section, and instant-commit editors
lib/features/settings/screens/settings_screen.dart, lib/features/settings/screens/account_section.dart, lib/features/settings/widgets/instant_commit_editor.dart, lib/features/settings/widgets/region_editor.dart, lib/features/settings/widgets/profile_form.dart, lib/features/settings/widgets/profile_form_values.dart, lib/features/settings/widgets/profile_form_controller.dart, lib/features/settings/logic/profile_payload.dart, lib/features/settings/panels/body_metrics.dart, lib/features/settings/controls/...
Settings adds a nested navigator, grouped rows, account/about sections, instant-commit editors, profile payload extraction, typed account deletion, and nullable biological-sex validation, while controls gain placeholder text and semantics improvements.
Routing, shell, and test updates
lib/router.dart, lib/shell/placeholder_screen.dart, test/widget_test.dart
The router moves settings to a standalone Cupertino route, the splash screen animates, placeholder navigation is added, shell/header navigation is simplified, and the widget test now boots localization and Supabase before asserting the welcome auth UI.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • VoMinhKhoii/Nham#129: Introduces the same partial-day detection and prompt flow that this PR adds to the logging experience.
  • VoMinhKhoii/Nham#154: Relates to the account export/delete and reset-password style flows that line up with the new mobile account and forgot-password screens.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the PR’s main scope: a broad pre-launch UX overhaul across the mobile Flutter app.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ux-wave0-mobile

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

VoMinhKhoii and others added 5 commits June 11, 2026 23:01
Retire the web-ported hamburger drawer in favour of an HIG-native three-tab
cream bottom bar: Today (dashboard) / Log (logging, center) / Patterns
(nutrition). Hairline top border, no elevation, safe-area padding, hidden when
the keyboard is open. selectionClick haptic on tab switch; instant branch
swaps. PopScope returns to Today on system back from a secondary tab.

Settings/account moves to a 32px avatar disc in the header's right slot
(carrying the onboarding pulse-dot), pushing the Settings screen as a root
CupertinoPage with swipe-back. Groups + Admin stay reachable as routes but are
off the bar until built. Delete the drawer (tab_scaffold drawer) and the mobile
sidebar port.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hardcoded, unlocalized English "Hello" — the one Lora-per-screen
moment — with a localized time-of-day greeting driven by the device clock and
app locale (Chào buổi sáng / Good morning, etc.). A greeting, not an
interpretation. Adds dashboard.greeting.* keys to en + vi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e formatting

Stop censoring over-target days. The dashboard hero now shows the honest number
("184 over") with the eyebrow flipping to OVER TARGET in espresso ink — never
red, never a pill, never an icon. The clamp(0) that made a 2,340-kcal day read
as a neutral "0 / 2,000" is gone.

Unify ring semantics: both calorie rings (dashboard + logging) now fill with
CONSUMED calories (fill up as you eat), matching the week strip and heatmap
framing — instead of the hero ring draining from remaining. When over target,
the base arc completes in tan and an overflow arc continues past 12 o'clock in
~40%-alpha terracotta. The in-ring label flips "left" → "over".

Locale-aware number formatting via intl (en "2,000" / vi "2.000") across the
ring, hero, and logging summary; localize the heatmap weekday initials and the
ring "left"/"over" labels. Adds dashboard.left/over/caloriesOverTarget keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…surance

A failed analysis no longer wipes the user's words. On error the raw text is
restored into the composer AND the failed attempt renders as a feed card: the
input as a Lora quote, a terracotta one-liner, "Try again" as the primary
action (re-runs the same meal), and a quiet Discard — wiring the previously
unused logging.discard string.

Keep the composer editable during analysis (the requestId-supersede mechanism
already handles overlap): MealInput's `disabled` becomes `analyzing`, which only
swaps Submit→Stop while leaving the field live. Add a ~20s reassurance line to
the streaming card so a slow pipeline doesn't read as a stall.

Adds logging.failedAttempt.* and logging.streaming.stillWorking to en + vi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app had zero HapticFeedback calls. Define and apply the contract (iOS-first):
- selectionClick on tab switches (bottom bar, already), day selection (week
  strip + timeline picker), segment changes (nutrition range pills, settings
  tab strip), and stepper taps (meal-entry quantity).
- lightImpact on meal submit and sheet-open (sign-out action sheet).
- mediumImpact as the success cue on save (meal confirm, weight log, profile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@VoMinhKhoii VoMinhKhoii changed the title feat: pre-launch UX Wave 0 (mobile/Flutter) — compliance & cold start feat: pre-launch UX overhaul (mobile/Flutter) — Waves 0–2 Jun 11, 2026
VoMinhKhoii and others added 19 commits June 12, 2026 13:04
…sh-then-pop

On completion the streaming card no longer unmounts and waits for a refetch to
pop a fresh pending card — the emotional peak of the analysis. Instead the run
is held in state (status done + the locally-held stream.result, previously
unused) and the card morphs in place: per-row macros are already real, the
totals row counts up ~600ms (new CountUpText), and the spinner row slides out as
the Edit/Confirm CTA slides up into the slot it vacated. One lightImpact marks
the landing. A ScrollController brings the answer into view on submit and on
done (nothing scrolled it before). The reveal card carries the user's own raw
words as its Lora quote, and on confirm it tears down the stream so it hands off
to the refetched persisted card — one continuous object from typed words to
saved meal. The refetch/reset now fires only on confirm, not on done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o-refresh

- Submit button keeps its 32pt visual but gets a 44pt tap target; edit steppers
  keep their 28pt visual inside a 40pt target (under 44 so two steppers + the
  count still fit a narrow row).
- The whole persisted-card header row toggles expand, not just the ~24px
  chevron.
- Pull-to-refresh (RefreshIndicator.adaptive → Cupertino spinner on iOS) on the
  day list, refetching the day + the meal-dates strip — none existed anywhere.
- The streaming skeleton no longer pads every meal to 3 ghost dishes: anonymous
  rows fill in only before any names arrive, and just one (logging one coffee no
  longer implies over-detection).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…with PageView

The hand-rolled three-week carousel never animated: its base offset was a
constant translateX(-100%), so paging swapped content with no slide and the
200ms duration was dead. Replace it with a PageView anchored so the current week
sits at a fixed index and forward paging is capped at this week (itemCount).
Swipe and the chevrons now both slide a full week with a selection haptic on
landing; opening the strip jumps to the selected week.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web shows two data-completeness notices the Flutter port dropped (their
strings shipped translated with zero Dart refs):
- legacyMacroWarning: when any persisted meal has unknown macros the day can't
  be totalled, so the summary ring is replaced by a quiet muted note instead of
  rendering a wrong total.
- partialDayNotice: a past day with real meals but under half the calorie target
  reads as under-logged, so a Lora-italic terracotta note (ported 1:1 from the
  web threshold isLikelyPartialDay = calories < 0.5 × target) says it's set aside
  from trends and invites folding it back in.

The partialYesterdayPrompt is deferred (needs a separate yesterday-totals fetch
+ dismiss state).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ection loop

A confirmed meal was a tombstone: nothing could remove it, so one fat-finger
confirm permanently poisoned the day's totals and trends. Add an iOS
trailing-swipe on persisted cards (terracotta, never red) that removes the meal
with a 5-second undo: the day heals immediately (the meal drops out of the
ring/bars optimistically) and the DELETE /api/v1/meals/{id} fires only if the
undo window closes — undo just restores the snapshot, a server rejection
restores it too. New strings (remove/undo/mealRemoved) in both locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
weight_trend.dart and weight_chart_utils.dart had no importers anywhere (the
verdict engine was amputated in the port; weight_chart.dart only references a
separate loadingWeightTrend string). Per the founder design direction
interpretive prose verdicts stay internal, so the dashboard.progressStatus.*
strings they fed go too — both locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…asts

- Delete the serifSemiBold Lora variant (the bible's hardest type rule) and
  convert its three callers (display, numDisplay, the calorie-ring figure) to
  serifRegular so a bold-Lora regression can't recur.
- Real ellipsis … replaces literal "..." across both locales (loading/streaming
  phase strings, placeholders).
- Add a SnackBarThemeData (espresso pill, cream text, accent action, soft
  radius, floating) so sign-out errors and the undo toast render branded instead
  of the stock dark-gray Material pill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swap the Material Icons in the logging widgets for their Lucide one-for-one
equivalents (square, arrowUp, minus, plus, check, pencil, refreshCw,
circleAlert, chevronLeft/Right/Down, calendar, trash2, utensilsCrossed) — the
package was already installed and the shell uses it, so the feed no longer ships
a second icon DNA. The intended mappings were already noted in code comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping a past day used to refetch the entire dashboard bundle (profile +
90d heatmap + weight) just to swap the meal list, swap the card for a
loading line, keep "remaining" framing, show no date, and replay every
mount animation.

Now the Today card is a PageView synced to the week strip:
- tap a strip day or swipe the card to browse (selection haptic on swipe;
  the strip already fired one on tap),
- per-day slice fetch via the lightweight /api/v1/logging/day endpoint —
  today still reads the warm bundle so the first page costs no round-trip,
- a date line in the card (Today / Yesterday / localized weekday-month-day),
- the remaining figure counts up ~300ms on day-swap (reuses CountUpText),
  macro bars + ring retarget without remounting,
- the pager animates its own height to the active page so the surrounding
  ListView reflows instead of clipping taller days.

First-run: when the user has never logged anything (zero meals today AND
zero historical logged/partial heatmap cells — there's no explicit
has-logged-before flag, so this is the reliable gate; an existing user is
never mistaken for first-run), the card collapses to one Lora
"What did you eat today?" question with no ring and no "% on track".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new user saw "0% on track" over an empty heatmap grid — a percentage
computed from zero data. The header line is now suppressed entirely until
at least one logged day exists, so the consistency section reads as a
blank grid waiting to fill, not a 0% failure. Pairs with the first-run
dashboard collapse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tent on refetch failure

The nutrition screen was the only surface forced to ClampingScrollPhysics,
had no pull-to-refresh, and a failed refetch nuked the editorial stack the
user was reading (guard() replaced the value with a bare error).

- swap Clamping for AlwaysScrollable + Bouncing, matching the rest of the app
- add a RefreshIndicator (pull-to-refresh re-runs the current range)
- refetch() now uses copyWithPrevious so a failure retains the prior overview
  underneath the error — content stays put
- wire the previously-unused nutrition.errors.overviewToast key: a refetch
  failure that still holds content surfaces as a toast instead of a blank page

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping a steady/background nutrient row pushed a duplicate of the
overview bar. Push a real Cupertino detail route (swipe-back, selection
haptic) that delivers what the row hint promises:

- A point figure + target progress, re-tokened on dashboard_tokens
  (solid white card, one radius, DM Sans scale, point value only — no
  ranges or confidence surfaced).
- A 7/30/90d coverage sparkline that degrades to discrete dots when
  coverage is thin (the trend.pointMode copy was written for exactly
  this). We hold window averages, not a per-day series, so it renders
  the resolved point honestly rather than fabricating a line.
- Food candidates as FULL rows — name, serving, rationale, and caution
  per Vietnamese food — the content the overview renders as name-only
  pills. The API already ships all four keys per candidate.

Deletes the orphaned inline nutrient_detail.dart (the duplicate bar).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nutrition screen was a verbatim port of the web screen the drift
watchlist condemns — translucent stacked cards, six eyebrows, a 24/16
radius mix — sitting one tap from the re-tokened dashboard.

- Solid white cards (kCardSurface + kCardShadow), no alpha surfaces:
  daily-rhythm, steady list, more-nutrients list.
- One radius (kCardRadius) — drop the 24/16 mix.
- Collapse six eyebrows to two: keep the editorial header eyebrow and
  the steady-list eyebrow; drop rhythm / focus / more-nutrients /
  pull-quote eyebrows (the content groups now stand on structure).

The verdict sentence is untouched; nutrition.confidence.* logic is
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Felt save: on a successful profile save the floating bar now morphs into
a 'Changes saved · N kcal/day' confirmation — wiring the previously
unused settings.saved key — with the calorie target counting old→new
(CountUpText, reduced-motion aware), holding ~1.6s before dissolving.
The success haptic already fired; the morph gives it a visible payoff.

Honest error state: a flaky profile fetch previously fell through to the
re-onboarding 'Start setup' empty state, stranding a configured user in
a false 'set up your profile' dead-end. Distinguish a load error (now a
neutral message + retry) from a genuinely-absent profile (the only case
that earns the empty state).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A user who skipped onboarding had their settings profile pre-filled with
weight 65 / height 165 / age 25 — fabricated values that could be saved
as if they were real data. Start those numeric fields EMPTY when the
profile carries none; validateBodyMetrics already requires them, so the
user is forced to enter genuine values (the hint placeholders still
suggest typical numbers without persisting them).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the edit-amounts surface, stepping a count-unit dish down to 0 now
strikes the row (line-through name + dimmed macros) — a clear 'this
one's out' cue before confirm drops it. Grams floor at minDishGrams, so
only count units can reach 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings mixed Material and Lucide icon DNAs. Swap the remaining Material
glyphs one-for-one to Lucide: person_outline→user, chevron_right→
chevronRight, arrow_back→arrowLeft (settings list + back header),
keyboard_arrow_down→chevronDown and check→check (custom select),
public→globe and place→mapPin (regional panel). One icon DNA on the
surface now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the "login wall titled Welcome back" with a real pre-auth welcome
screen: Lora wordmark, a typing demo that resolves into a point result chip,
then three stacked options (Apple, Google, Continue with email). Collapse the
sign-in/sign-up tab split into one email path — a single form whose primary
action signs in, with a quiet toggle to account-creation in place.

Sign-up no longer flashes a vanishing SnackBar: it cross-fades to a "Check
your email" state that names the address and offers resend on a 30s cooldown.
Google OAuth now holds a "Finishing sign-in…" state across the Safari
app-switch instead of clearing the spinner, dropping it only if the app
resumes still signed-out. Replace the two e.toString() error leaks in the
controller with warm localized copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ip, flat card

Localize the hardcoded English that leaked the moment a user picked Tiếng Việt
("About You" + its hint, and the unlock placeholder's "this side turns into…"
desktop-two-pane leftover) into onboarding.bodyMetrics keys (en + vi).

Replace the banned flag emoji in the language toggle with a Lora language-code
monogram in a tinted disc (EN / VI), and swap its Material check for Lucide.

Sex is a binary choice, so render it as the 2-segment OptionStrip instead of a
select popover. Flatten the daily-target hero from an accent gradient to a flat
white card with a hairline border (dashboard token system: solid cards).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VoMinhKhoii and others added 7 commits June 12, 2026 22:13
…e-pinned

A second submit while an unconfirmed reveal is showing now refreshes
the origin day first, so the server-side pending analysis resurfaces as
a pending-confirmation card instead of visibly vanishing. The stream
state carries its loggedDate: the streaming/reveal cards only render on
the day they were submitted on, and _confirmReveal updates the origin
date's caches. The streaming→reveal swap comment no longer claims
there's no remount, and the two loudest discontinuities are gone — the
revealing card matches the streaming card's surface background, and
item rows crossfade in place instead of replaying FadeInLeft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cale

A resolved save now baselines exactly the payload it sent
(markSavedAs); if the values are dirty again — edited during the
in-flight PUT — another commit is queued immediately instead of
silently baselining the edit away. On failure the controls roll back
to their last-saved values, a device-side locale change is reverted
(device and server never diverge), and the error line gains a Try
again affordance that re-applies the attempted values. The
silently-skipped commit path (incomplete body metrics) also reverts
the locale and surfaces a quiet localized hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nutrient detail's sparkline band can only ever hold one point (a
single resolved average), so the fabricated 0.5 mid-rail dot for
targetless nutrients is gone, the band is omitted entirely when there
is nothing meaningful to plot, and its eyebrow now reads 'Average vs
target' instead of the untrue 'Target progress' (orphaned key removed
from both locales). The figure caption drops to meta type, bringing the
screen to max two eyebrows. Remaining new animations now honor
MediaQuery.disableAnimations: the reveal and pager count-ups land in
place, both macro-bar sweeps render at rest, and the looping pulse
skeletons and StreamingEntry spin/pulse freeze into calm static states.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Day-error card re-clothed in terracotta nham-danger on cream (no
  literal Tailwind reds)
- New-code Lora w500 (serifMedium) call sites in settings_screen drop
  to serifRegular — Lora never above 400 (pre-existing sites in
  nutrition/onboarding left as-is)
- Macro shorthand unified to 'P: 38g' (with space) on meal-entry item
  rows and persisted-card group rows, matching their totals rows
- Today-card meal rows name the unit: '412 kcal', not a bare number
- '% on track' suppressed until 3 scored days (spec), not 1
- /welcome count-up uses locale-aware formatCount (vi '2.000') instead
  of hand-rolled comma grouping
- Goal-pace subline localizes the per-week suffix (vi '/tuần') and the
  decimal separator
- biologicalSex is a real required selection: null until chosen,
  placeholder in the select, localized validation message — no silent
  male default baking a wrong BMR (consistent with the 65/165/25 fix)
- PlaceholderScreen header gains a back affordance (pop, else home) so
  a groups/admin deep link can't strand the user
- Instant-commit save-success haptic unified to mediumImpact
- _isFirstRun comment now admits the >90d-stale-user limit, and the
  first-run card gains three localized time-of-day suggestion chips
  that open the composer prefilled

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bundles the static OFL TTFs under assets/google_fonts/ — Lora
Regular/Italic/Medium and DM Sans Regular/Medium/SemiBold/Bold, the
exact weight/style set the app's type system uses — named per the
Google Fonts API convention so google_fonts resolves them from assets,
and disables runtime fetching in main.dart. A cold offline start now
renders in the brand fonts instead of falling back while a network
fetch races first paint. Lora's TTFs carry full Vietnamese coverage
(verified ẩ ở ự đ ọ in the cmap); DM Sans has no Vietnamese subset —
identical coverage to the previous runtime fetch, so no regression.

The audit listed only Lora Regular+Italic and DM Sans
Regular/Medium/Bold, but with fetching disabled the other live
variants (Lora 500, DM Sans 600) would throw and drop to the system
fallback — so the full in-use set is bundled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	apps/mobile-flutter/lib/features/logging/data/logging_providers.dart
#	apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart
#	apps/mobile-flutter/lib/features/logging/widgets/meal_input.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart (1)

77-77: ⚠️ Potential issue | 🔴 Critical

Handle the Future<bool> return value from refetch() across all callers.

The method signature changed from Future<void> to Future<bool>. Three call sites found in nutrition_screen.dart require updates:

  1. Line 96 (RefreshIndicator.onRefresh): Type mismatch—RefreshIndicator expects Future<void>, but refetch() now returns Future<bool>. Either convert the return type back to Future<void> or wrap the call appropriately.

  2. Lines 125 & 137 (InlineError.onRetry callbacks): Currently ignoring the boolean return value. Since the method is documented to return success/failure for caller feedback, these should await the result and display a toast on failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart`
at line 77, The refetch() method in nutrition_overview_provider.dart now returns
Future<bool> instead of Future<void>, but its callers in nutrition_screen.dart
have not been updated. At the RefreshIndicator.onRefresh call site (around line
96), the type mismatch must be resolved by either converting refetch() back to
return Future<void> or wrapping the call with a closure that discards the bool
return value to match the RefreshIndicator contract. At the two
InlineError.onRetry callback sites (around lines 125 and 137), update them to
await the bool result from refetch() and use the returned boolean to determine
whether to display a failure toast notification to the user, replacing the
current behavior of ignoring the return value entirely.
🧹 Nitpick comments (1)
apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart (1)

92-96: 💤 Low value

Inconsistent formatting of refetch() calls.

The onRefresh callback splits a simple method chain across five lines, while the identical pattern at lines 125 and 137 uses a single line. The multi-line format doesn't improve readability here.

♻️ Simplify to match the style at lines 125 and 137
-              onRefresh:
-                  () =>
-                      ref
-                          .read(nutritionOverviewProvider(_range).notifier)
-                          .refetch(),
+              onRefresh: () => ref.read(nutritionOverviewProvider(_range).notifier).refetch(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart`
around lines 92 - 96, The onRefresh callback at lines 92-96 is formatted across
multiple lines in a way that is inconsistent with the same refetch() pattern
used at lines 125 and 137. Consolidate the onRefresh callback to a single line
by moving the lambda arrow and the
ref.read(nutritionOverviewProvider(_range).notifier).refetch() call onto one
line to match the formatting style used elsewhere in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart`:
- Around line 37-40: The `_lastOverviewByAccount` cache dictionary grows without
bound and is never cleared, causing a memory leak over the app's lifetime as
different userId:timezone combinations accumulate. Fix this by implementing
cache eviction: either listen to session state changes (such as when
currentSessionProvider becomes null on sign-out) to call
`_lastOverviewByAccount.clear()`, or implement a bounded LRU cache using
LinkedHashMap with a maximum size limit (e.g., 10 entries) and remove the oldest
entry when adding a new one beyond the limit. If implementing the LRU approach,
replace the direct map assignments to `_lastOverviewByAccount` at the locations
where overview data is cached with a helper method that enforces the size
constraint.

In `@apps/mobile-flutter/test/widget_test.dart`:
- Around line 28-33: The EasyLocalization widget in the widget_test.dart file
lacks an explicit startLocale parameter, which causes the test to initialize in
whatever locale matches the system environment rather than English. Add the
startLocale property set to const Locale('en') in the EasyLocalization widget
configuration to ensure the app always starts with the English locale during
testing, making the text assertions on lines 39-40 reliable across different
system locales.

---

Outside diff comments:
In
`@apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart`:
- Line 77: The refetch() method in nutrition_overview_provider.dart now returns
Future<bool> instead of Future<void>, but its callers in nutrition_screen.dart
have not been updated. At the RefreshIndicator.onRefresh call site (around line
96), the type mismatch must be resolved by either converting refetch() back to
return Future<void> or wrapping the call with a closure that discards the bool
return value to match the RefreshIndicator contract. At the two
InlineError.onRetry callback sites (around lines 125 and 137), update them to
await the bool result from refetch() and use the returned boolean to determine
whether to display a failure toast notification to the user, replacing the
current behavior of ignoring the return value entirely.

---

Nitpick comments:
In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart`:
- Around line 92-96: The onRefresh callback at lines 92-96 is formatted across
multiple lines in a way that is inconsistent with the same refetch() pattern
used at lines 125 and 137. Consolidate the onRefresh callback to a single line
by moving the lambda arrow and the
ref.read(nutritionOverviewProvider(_range).notifier).refetch() call onto one
line to match the formatting style used elsewhere in the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7d3c7aa8-9953-4b0b-bc69-27b5043c882e

📥 Commits

Reviewing files that changed from the base of the PR and between 986cc57 and 96334f9.

⛔ Files ignored due to path filters (7)
  • apps/mobile-flutter/assets/google_fonts/DMSans-Bold.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/DMSans-Medium.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/DMSans-Regular.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/DMSans-SemiBold.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/Lora-Italic.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/Lora-Medium.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/Lora-Regular.ttf is excluded by !**/*.ttf
📒 Files selected for processing (55)
  • apps/mobile-flutter/assets/l10n/en.json
  • apps/mobile-flutter/assets/l10n/vi.json
  • apps/mobile-flutter/lib/data/api_client.dart
  • apps/mobile-flutter/lib/features/auth/providers/auth_form_controller.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_page.dart
  • apps/mobile-flutter/lib/features/auth/widgets/email_auth_form.dart
  • apps/mobile-flutter/lib/features/auth/widgets/welcome_demo.dart
  • apps/mobile-flutter/lib/features/dashboard/data/dashboard_providers.dart
  • apps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/today_section.dart
  • apps/mobile-flutter/lib/features/logging/data/logging_keys.dart
  • apps/mobile-flutter/lib/features/logging/data/logging_providers.dart
  • apps/mobile-flutter/lib/features/logging/data/manual_log_providers.dart
  • apps/mobile-flutter/lib/features/logging/data/stream_analysis_controller.dart
  • apps/mobile-flutter/lib/features/logging/screens/logging_screen.dart
  • apps/mobile-flutter/lib/features/logging/widgets/count_up.dart
  • apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_entry.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_input.dart
  • apps/mobile-flutter/lib/features/logging/widgets/partial_yesterday_prompt.dart
  • apps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dart
  • apps/mobile-flutter/lib/features/logging/widgets/streaming_entry.dart
  • apps/mobile-flutter/lib/features/nutrition/logic/verdict_logic.dart
  • apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart
  • apps/mobile-flutter/lib/features/nutrition/screens/nutrient_detail_screen.dart
  • apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/background_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/inline_error.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dart
  • apps/mobile-flutter/lib/features/onboarding/screens/screen_body_metrics.dart
  • apps/mobile-flutter/lib/features/onboarding/screens/welcome_setup_screen.dart
  • apps/mobile-flutter/lib/features/onboarding/widgets/language_toggle.dart
  • apps/mobile-flutter/lib/features/settings/controls/country_select.dart
  • apps/mobile-flutter/lib/features/settings/controls/custom_select.dart
  • apps/mobile-flutter/lib/features/settings/controls/option_strip.dart
  • apps/mobile-flutter/lib/features/settings/logic/profile_payload.dart
  • apps/mobile-flutter/lib/features/settings/panels/body_metrics.dart
  • apps/mobile-flutter/lib/features/settings/screens/account_section.dart
  • apps/mobile-flutter/lib/features/settings/screens/settings_screen.dart
  • apps/mobile-flutter/lib/features/settings/widgets/instant_commit_editor.dart
  • apps/mobile-flutter/lib/features/settings/widgets/profile_form_controller.dart
  • apps/mobile-flutter/lib/features/settings/widgets/profile_form_values.dart
  • apps/mobile-flutter/lib/features/settings/widgets/region_editor.dart
  • apps/mobile-flutter/lib/main.dart
  • apps/mobile-flutter/lib/router.dart
  • apps/mobile-flutter/lib/shell/placeholder_screen.dart
  • apps/mobile-flutter/lib/shell/tab_scaffold.dart
  • apps/mobile-flutter/lib/theme/nham_typography.dart
  • apps/mobile-flutter/pubspec.yaml
  • apps/mobile-flutter/test/widget_test.dart
💤 Files with no reviewable changes (1)
  • apps/mobile-flutter/lib/features/nutrition/logic/verdict_logic.dart
✅ Files skipped from review due to trivial changes (1)
  • apps/mobile-flutter/assets/l10n/vi.json
🚧 Files skipped from review as they are similar to previous changes (26)
  • apps/mobile-flutter/lib/features/dashboard/data/dashboard_providers.dart
  • apps/mobile-flutter/lib/theme/nham_typography.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/background_section.dart
  • apps/mobile-flutter/lib/features/logging/widgets/count_up.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dart
  • apps/mobile-flutter/lib/features/onboarding/widgets/language_toggle.dart
  • apps/mobile-flutter/lib/features/settings/widgets/region_editor.dart
  • apps/mobile-flutter/lib/features/settings/widgets/instant_commit_editor.dart
  • apps/mobile-flutter/lib/features/onboarding/screens/welcome_setup_screen.dart
  • apps/mobile-flutter/lib/features/settings/logic/profile_payload.dart
  • apps/mobile-flutter/lib/features/logging/data/logging_keys.dart
  • apps/mobile-flutter/lib/features/auth/widgets/welcome_demo.dart
  • apps/mobile-flutter/lib/features/settings/screens/account_section.dart
  • apps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dart
  • apps/mobile-flutter/lib/shell/tab_scaffold.dart
  • apps/mobile-flutter/lib/features/logging/screens/logging_screen.dart
  • apps/mobile-flutter/lib/features/logging/widgets/partial_yesterday_prompt.dart
  • apps/mobile-flutter/lib/features/logging/widgets/streaming_entry.dart
  • apps/mobile-flutter/lib/features/auth/providers/auth_form_controller.dart
  • apps/mobile-flutter/lib/router.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_input.dart
  • apps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/today_section.dart
  • apps/mobile-flutter/assets/l10n/en.json
  • apps/mobile-flutter/lib/features/settings/screens/settings_screen.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_entry.dart

Comment on lines +37 to +40
final Map<String, NutritionOverview> _lastOverviewByAccount = {};

String _overviewCacheKey(String? userId) =>
'${userId ?? 'signed-out'}:${nutritionTimezoneOffset()}';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Memory leak: unbounded global cache never evicted.

_lastOverviewByAccount grows without bound and is never cleared. Each unique userId:timezone combination adds an entry that persists for the app's lifetime, even after sign-out or account deletion. Over time (especially with timezone changes from travel/DST or multiple accounts), this will leak memory.

♻️ Recommended fix: add cache eviction on sign-out

Option 1: Clear the entire cache when the session ends (simple, but loses data across sign-out/sign-in):

// In a listener or dispose callback where you detect sign-out:
ref.listen(currentSessionProvider, (prev, next) {
  if (next == null && prev != null) {
    _lastOverviewByAccount.clear();
  }
});

Option 2: Implement LRU eviction with a maximum cache size (e.g., keep only the last 10 entries):

final _lastOverviewByAccount = LinkedHashMap<String, NutritionOverview>();
const _maxCacheSize = 10;

void _updateCache(String key, NutritionOverview overview) {
  if (_lastOverviewByAccount.length >= _maxCacheSize && !_lastOverviewByAccount.containsKey(key)) {
    _lastOverviewByAccount.remove(_lastOverviewByAccount.keys.first);
  }
  _lastOverviewByAccount[key] = overview;
}

Then replace direct map assignments with _updateCache(cacheKey, overview); at lines 55 and 87.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart`
around lines 37 - 40, The `_lastOverviewByAccount` cache dictionary grows
without bound and is never cleared, causing a memory leak over the app's
lifetime as different userId:timezone combinations accumulate. Fix this by
implementing cache eviction: either listen to session state changes (such as
when currentSessionProvider becomes null on sign-out) to call
`_lastOverviewByAccount.clear()`, or implement a bounded LRU cache using
LinkedHashMap with a maximum size limit (e.g., 10 entries) and remove the oldest
entry when adding a new one beyond the limit. If implementing the LRU approach,
replace the direct map assignments to `_lastOverviewByAccount` at the locations
where overview data is cached with a helper method that enforces the size
constraint.

Comment on lines +28 to +33
EasyLocalization(
supportedLocales: const [Locale('en'), Locale('vi')],
path: 'assets/l10n',
fallbackLocale: const Locale('en'),
child: const ProviderScope(child: NhamApp()),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current test coverage for explicit locale pinning in widget tests.
rg -n -C2 "EasyLocalization\\(|startLocale:|fallbackLocale:" apps/mobile-flutter/test

Repository: VoMinhKhoii/Nham

Length of output: 747


🏁 Script executed:

# Read the widget test file around the mentioned lines to see the assertion at 39-40
cat -n apps/mobile-flutter/test/widget_test.dart | sed -n '25,45p'

Repository: VoMinhKhoii/Nham

Length of output: 724


🏁 Script executed:

# Search for other EasyLocalization usages in tests to see patterns
rg -A 5 "EasyLocalization(" apps/mobile-flutter/test --type dart

Repository: VoMinhKhoii/Nham

Length of output: 145


🌐 Web query:

EasyLocalization startLocale vs fallbackLocale behavior Flutter

💡 Result:

In the EasyLocalization package for Flutter, startLocale and fallbackLocale serve distinct purposes in how the application determines which language to display [1][2]. startLocale The startLocale property is used to override the device's default locale [2][3]. If specified, it forces the application to attempt to load this locale when the app initializes, rather than automatically detecting the user's system language [2]. It is important to note that the effectiveness of startLocale depends on the saveLocale property [4]. By default, saveLocale is set to true, meaning the package stores the user's last selected locale in device storage [3][5]. If a saved locale exists, it typically takes precedence over startLocale [4]. To ensure startLocale is always used (or to ensure it takes precedence over previously saved preferences), developers often need to set saveLocale to false [4]. fallbackLocale The fallbackLocale property defines the locale that the application should use when a requested or detected locale is not present in the supportedLocales list [1][2]. It serves as a safety net to prevent the app from failing or displaying errors if the user's device locale is not supported by your application's translation files [2]. Additionally, when used in conjunction with settings like useFallbackTranslations, the fallbackLocale can also be used as a secondary source for individual missing keys, allowing the app to fetch a translation from the fallback file if the key is missing from the primary locale file [1][2]. Summary of Behavior 1. startLocale: Determines the initial language when the app first launches, overriding the device system settings (provided saveLocale does not override it) [2][4]. 2. fallbackLocale: Provides a default locale if the currently selected or device locale is not included in the list of supported locales, or serves as a source for missing translations when enabled [1][2]. Top results: [1][2][4][3]

Citations:


Add explicit startLocale to pin English locale for test assertions.

Lines 39-40 assert English text, but without startLocale in the EasyLocalization setup, the app can initialize in any supported locale matching the environment's system locale. fallbackLocale only provides a safety net for unsupported locales, not an explicit initial locale. This will cause the test to fail in environments with Vietnamese or other non-English system locale.

Suggested fix
     await tester.pumpWidget(
       EasyLocalization(
         supportedLocales: const [Locale('en'), Locale('vi')],
         path: 'assets/l10n',
         fallbackLocale: const Locale('en'),
+        startLocale: const Locale('en'),
         child: const ProviderScope(child: NhamApp()),
       ),
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/test/widget_test.dart` around lines 28 - 33, The
EasyLocalization widget in the widget_test.dart file lacks an explicit
startLocale parameter, which causes the test to initialize in whatever locale
matches the system environment rather than English. Add the startLocale property
set to const Locale('en') in the EasyLocalization widget configuration to ensure
the app always starts with the English locale during testing, making the text
assertions on lines 39-40 reliable across different system locales.

VoMinhKhoii and others added 11 commits June 26, 2026 23:23
Logging surface redesign:
- Restore left slide-in nav drawer; remove bottom tab bar so the input
  bar owns the bottom edge (sidebar.dart, tab_scaffold.dart, app_header.dart)
- Date chip morphs in place into the week strip (fixed-height cross-dissolve,
  full-width, hamburger hidden when expanded) — feed never shifts (DateMorph)
- Two-line meal input: text on line 1, mode selector + send on line 2;
  minimal Claude-Code-style mode sheet (Normal/Cheat/Manual)
- Optimistic save with keep-previous-data + top toast (no full reload)
- Streaming meal card: single loading step, streams items like web, lightning
  icon for Normal mode
- Meal time as centered divider (── 1:04 AM ──); consistent subtle borderFaint
  separators; removed left timeline rail/gutter
- Empty state: rotating "What did you eat?" prompts, no chips/subtitle
- Manual log sheet: Lucide icons, macros with units, prominent save

Font: swap DM Sans -> Be Vietnam Pro globally (native font family) to fix
broken Vietnamese diacritics; Lora serif unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mobile (apps/mobile-flutter):
- Flatten to one view: compact DaySummary, 2-col Vitamins/Minerals grid with
  inline progress bars + average/goal in g·mg, and a single "Suggested foods"
  CTA (bottom sheet over the timeline's most under-target nutrients). Drop the
  per-nutrient detail screen, spotlight/steady/background sections and the
  Vitamin-D education card.
- Date toggle moves to the header (AppHeader.trailing) as a Today/7d/30d
  segmented control; calorie hero switches from Lora to the sans hero with a
  top-right over/under target chip (hidden when no goal).
- Macro stacked line-area trend chart (fl_chart) for 7d/30d from the overview
  daySeries; kcal y-axis to ≥3000 with dashed gridlines; Today keeps the
  composition bar. Centered macro legend with longer pills + ±% vs target.
- On-target nutrient cards greened (fresh emerald); ceilings/limited unchanged.
- FAO/WHO · Vietnam RDA · NASEM DRI source attribution with a citations sheet.
- All toasts moved to the top: showTopToast gains success/error + action
  variants; 7 bottom SnackBars converted (incl. the logging undo).
- Parse daySeries into the mobile NutritionOverview model.

Web API (lib/nutrition): add a 1-day '1d' range end-to-end (schemas, types,
date-range, summary, overview mapper) + verdict-hero Record + messages, so the
mobile "Today" view resolves to a single day.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rchy

- Suggested-foods sheet: drop the verbose serving/rationale/caution cards for a
  compact per-nutrient row — name, how-much-short (−N%), and food-name chips.
  Removes the now-unused FoodCandidateRow.
- Drawer: give the header a prominent avatar + name + email identity block and
  drop the duplicate footer account card, so the hierarchy reads
  identity → nav → account actions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…known

Sex-dependent micronutrients (iron, vitamin A, zinc, magnesium…) previously
resolved to an "unsupported" null target when a profile had no biological sex,
so those cards showed "no target". Now an unknown sex falls back to the mean of
the male/female RDA (e.g. VN iron (10+24)/2 = 17 mg), and an age-unknown VN
female keeps the premenopausal iron floor (24 mg) instead of null — every
catalogued nutrient scores. Sex-independent nutrients are unaffected (mean == value).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…any nutrient

Replace the hand-curated 8-nutrient food catalog with a query over
vietnamese_food_composition: top foods by per-100g density for ANY card
nutrient (default + extended), condiments excluded, raw/cooked variants deduped.
Returns a pool (18) so clients can cycle alternatives without refetching.

- candidates action is now a DB query; input schema widened to CANDIDATE_NUTRIENTS
  (default + more); CandidatesResponse shape is now { nutrient, foods:[{id, name,
  nameEn, amount, unit}] }.
- every card nutrient sets supportsCandidates: true.
- web FoodChipRow renders DB foods (locale-aware name) for any nutrient.
- delete the curated catalog + its test; update candidates/schema tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Consume the new DB-derived candidates shape ({foods:[{name,nameEn,amount,
  unit}]}); the provider now works for ANY card nutrient (drop the supported-8
  gate + candidate_nutrients.dart).
- Foods sheet shows a few foods per nutrient; tap to reserve (pinned + check),
  "Don't have these?" cycles only the unreserved slots through the fetched pool
  — no refetch (instant). Locale-aware food names.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ranking by raw per-100g density surfaced dense-but-impractical items —
dried spices topping vitamin K, fortified breakfast cereals, oils,
sweets, shake mixes, and yeast/leavening agents. Exclude those whole
categories (USDA SR + VN FCT naming variants) plus fortified/formulated
name markers in the "Other" catch-all, while keeping whole-food groups
(meats, vegetables, fruits, legumes, fish, nuts, grains, roots).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resolveMicronutrientTargets assigned `unit` (typed as plain string) into
MicronutrientTarget.unit (`'mg' | 'mcg'`), which fails tsc. Narrow the
local to the row's own unit type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Long USDA food names overflowed the chip Wrap row. Cap each chip at 62%
of screen width with a Flexible + ellipsis label so names truncate
instead of pushing past the row edge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vi.json had only the 'other' variant for verdict.partialNote; the
singular path would fail to resolve. Vietnamese doesn't inflect plurals,
so 'one' mirrors 'other'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart (2)

572-635: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep pull-to-refresh available for empty feeds.

RefreshIndicator.adaptive only wraps the populated ListView; empty, loading, and pending-only states use plain SingleChildScrollView, so users cannot refresh when the feed has no persisted meals.

Suggested direction
+  Widget _refreshable(Widget child) => RefreshIndicator.adaptive(
+        onRefresh: _refresh,
+        color: NhamColors.accent,
+        child: child,
+      );
+
@@
-        return SingleChildScrollView(
+        return _refreshable(SingleChildScrollView(
           keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
+          physics: const AlwaysScrollableScrollPhysics(),
           child: Padding(
             padding: const EdgeInsets.symmetric(vertical: NhamSpacing.sp6),
             child: Center(child: body),
           ),
-        );
+        ));

Apply the same wrapper/physics to the other empty or footer-only scroll branches.

Also applies to: 638-641

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart` around lines
572 - 635, The empty/loading/footer-only branches in feed_area.dart currently
return plain SingleChildScrollView, so they bypass the RefreshIndicator.adaptive
used by the populated ListView. Update the scroll branches around the
persistedMeals.isEmpty logic to use the same refreshable wrapper and scroll
physics as the main feed path, including the body-only and _Footer rendering
cases, so pull-to-refresh remains available when there are no persisted meals.

273-285: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pin failed-attempt cards to the submitted day.

Streaming/reveal cards are date-guarded, but _failedText is not. If an analysis fails after the user switches dates, the retry card and restored composer text can appear on the wrong day.

Suggested fix
   String? _failedText;
+  String? _failedDate;
@@
     setState(() {
       _failedText = null;
+      _failedDate = null;
       _revealRawInput = null;
       _inFlightText = text;
     });
@@
     if (next.status == StreamStatus.error) {
       final text = _inFlightText;
+      final failedDate = next.loggedDate ?? widget.date;
       setState(() {
         _failedText = text;
+        _failedDate = failedDate;
         _inFlightText = null;
       });
-      if (text != null && _inputController.getText().trim().isEmpty) {
+      if (failedDate == widget.date &&
+          text != null &&
+          _inputController.getText().trim().isEmpty) {
         _inputController.setText(text);
       }
@@
-    final hasFailedAttempt = _failedText != null;
+    final hasFailedAttempt =
+        _failedText != null && _failedDate == widget.date;

Also applies to: 362-376

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart` around lines
273 - 285, The failed-attempt retry card is not pinned to the day it was
submitted, so it can show on a different selected date after a stream error.
Update the error handling in feed_area.dart around the StreamStatus.error path
in the feed/state flow (including the _failedText logic and the card rendering
path) to capture the active submitted day when the request starts and only
surface the failed card when the current day still matches that captured day.
Apply the same date-guarding behavior to the related retry-card rendering code
referenced by the 362-376 block so the restored composer text and failed attempt
stay tied to the original day.
apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart (1)

460-503: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Disable future-day cells before they reach logging flows.

isFuture is computed, but the tap handler still selects those dates. Because the current centered week includes days after today, users can navigate to and log against future dates.

Suggested fix
-    final labelColor = isSelected ? NhamColors.text : NhamColors.textMuted;
+    final disabled = isFuture;
+    final labelColor =
+        disabled
+            ? NhamColors.textMuted50
+            : isSelected
+                ? NhamColors.text
+                : NhamColors.textMuted;
@@
-      onTapDown: (_) => setState(() => _pressed = true),
-      onTapUp: (_) => setState(() => _pressed = false),
-      onTapCancel: () => setState(() => _pressed = false),
-      onTap: () => widget.onSelect(widget.date),
+      onTapDown: disabled ? null : (_) => setState(() => _pressed = true),
+      onTapUp: disabled ? null : (_) => setState(() => _pressed = false),
+      onTapCancel: disabled ? null : () => setState(() => _pressed = false),
+      onTap: disabled ? null : () => widget.onSelect(widget.date),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart` around
lines 460 - 503, Future-day cells in the TimelinePicker are still tappable even
though isFuture is already computed. Update the day cell logic in the widget
build/tap handling so future dates cannot be selected or sent to
widget.onSelect, and keep the existing visual treatment for isFuture consistent
with a disabled state. Use the isFuture, GestureDetector, and onSelect paths in
timeline_picker.dart to gate both tap callbacks and pressed feedback for future
dates.
🧹 Nitpick comments (1)
components/nutrition/rows/food-chip-row.tsx (1)

27-65: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Destructure the useQuery result.

Reading the whole query object opts out of TanStack Query's tracked-property optimization, so this component subscribes to more state than it uses. Please destructure the fields you read (data, error, isError, isLoading, isSuccess) and verify that this is intentional.

♻️ Proposed fix
-  const query = useQuery({
+  const { data, error, isError, isLoading, isSuccess } = useQuery({
     queryKey: ['nutrition', 'candidates', nutrient],
     queryFn: () => getFoodSourceCandidates({ nutrient }),
     enabled,
     retry: false,
     staleTime: 60_000,
   });
 
-  if (query.isLoading) {
+  if (isLoading) {
     return <ChipRowSkeleton variant={variant} />;
   }
@@
-  if (query.isError) {
+  if (isError) {
     console.error(
       '[nutrition] candidates query failed for nutrient',
       nutrient,
-      query.error
+      error
     );
@@
-  if (query.isSuccess && !query.data.foods.length) {
+  if (isSuccess && !data.foods.length) {
@@
-  if (!query.data?.foods.length) {
+  if (!data?.foods.length) {
@@
-  const chips = query.data.foods.slice(0, limit);
+  const chips = data.foods.slice(0, limit);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/nutrition/rows/food-chip-row.tsx` around lines 27 - 65, The
food-chip-row component is reading the full useQuery result object, which
disables TanStack Query’s tracked-property optimization. Update the useQuery
call in food-chip-row.tsx to destructure only the fields actually used in the
render path, namely data, error, isError, isLoading, and isSuccess, and then use
those local bindings throughout the component to keep subscriptions minimal.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart`:
- Around line 15-17: Replace the custom Apple glyph/button in AppleButton with
Apple’s official SignInWithAppleButton control so branding and platform behavior
come from the system-provided widget. Update the AppleButton widget
implementation to use the provided Apple sign-in button rather than the
hand-rolled UI, keeping the existing auth action wiring intact while preserving
the current placement within the auth flow.
- Around line 39-47: The auth CTA in AppleButton is built with GestureDetector
and Opacity, so it is not exposed as a real accessible button or disabled
control. Replace the current interaction wrapper in AppleButton’s build logic
with a proper button primitive that supports semantics, focus, and disabled
state, and keep the pressed/busy visual behavior tied to that button instead of
manual tap handlers.

In `@apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart`:
- Around line 26-42: The shimmer still runs its AnimationController even when
reduced-motion is enabled, because _ShimmerState::_c starts repeating
unconditionally and build() only skips rendering. Update Shimmer/_ShimmerState
to start and stop the controller based on
MediaQuery.disableAnimationsOf(context) (or an equivalent media-change hook) so
the ticker is paused when animations are disabled and resumed when they are
allowed.

In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart`:
- Around line 245-255: Initialize _visibleAnchor from the same anchor used by
PageController initialPage instead of _selectedAnchor, so TimelinePicker starts
with the actual page being shown. Update the setup around _visibleAnchor,
_pageController, and _canNavigateNext in timeline_picker.dart so the right
chevron reflects the real visible page when selectedDate is on the current page.
Also make the same adjustment anywhere the current-page visibility is derived in
the related logic around the page handling flow.

In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart`:
- Around line 138-142: The nutrient grouping in nutrition_screen.dart is
treating every non-vitamin item as a mineral, so NutrientGroup.other can be
rendered under the wrong header. Update the logic around the
all/vitamins/minerals lists in the nutrition screen to filter minerals
explicitly by NutrientGroup.mineral (or equivalent) and keep NutrientGroup.other
separate so it does not fall into the Minerals section.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart`:
- Around line 171-176: The week label formatting in _label currently hard-codes
day/month order, so update the week branch to use the locale-aware short date
skeleton instead of the fixed pattern. Keep the existing weekday logic for
non-week units, and make the change in macro_trend_chart.dart within the _label
helper so week labels follow each locale’s conventions.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart`:
- Around line 37-38: The active range selection in range_selector.dart is using
_ranges.indexOf(resolvedRange) directly, so unsupported model values like 90d
resolve to -1 and get clamped to the first segment. Update the RangeSelector
logic to normalize resolvedRange to one of the supported _ranges values before
calculating activeIndex, and base the selected segment on that normalized value
so backend-resolved auto values map to the intended UI range.
- Around line 42-44: The range selector touch target is too small because the
SizedBox in range_selector.dart constrains the control to 34px height. Update
the layout in the range selector widget so the tappable area reaches at least
44pt, adjusting the parent SizedBox and any internal padding/segment sizing in
the range selector implementation to preserve the visual design while meeting
the target.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart`:
- Around line 49-54: The citations bottom sheet in source_attribution.dart is
using a non-scrollable Column, which can overflow with longer localized text or
larger accessibility fonts. Update the sheet layout in the widget built by the
builder and any related citation content sections so the content is wrapped in a
scrollable container such as SingleChildScrollView or ListView, while preserving
the existing Container styling and bottom-sheet structure.

In
`@apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart`:
- Around line 235-247: The chip actions in SuggestedFoodsSheet are interactive
but their current padding can fall below the 44pt mobile touch target. Update
the chip/widget builders in suggested_foods_sheet.dart, especially the
GestureDetector/AnimatedContainer path for the chip item and the refresh row
block, so their tappable area is at least 44pt tall while preserving the
existing layout and visuals. Use the existing chip action widget logic in the
affected builder functions to add vertical padding/minimum constraints rather
than shrinking the hit area.
- Around line 176-180: The rotating slot count in suggested_foods_sheet.dart is
widened to num by clamp, which then breaks the _cursor update logic. Update the
rotatingSlots calculation in the logic that builds the rotating FoodCandidate
list so it is converted back to int before being used in the modulo/update path,
keeping the _cursor assignment type-safe. Refer to the rotatingSlots variable
and the _cursor update in the suggested_foods_sheet widget logic when making the
fix.

In `@apps/mobile-flutter/lib/shared/widgets/top_toast.dart`:
- Around line 22-48: `showTopToast` currently inserts a new `OverlayEntry` every
time, which allows multiple top toasts to overlap; update the toast flow to
serialize or replace the active entry so only one `_TopToast` can be visible at
once. Use the existing `showTopToast`, `OverlayEntry`, and `_TopToast` logic to
track the current toast and either dismiss it or queue the next toast before
calling `overlay.insert`, ensuring the undo toast in the logging feed can remain
visible and actionable.

In `@apps/mobile-flutter/lib/shell/app_header.dart`:
- Around line 41-56: The current AppHeader layout in the build method still
centers the middle content relative to only a fixed spacer, so any wider
trailing widget shifts the title off-center. Update the Row-based structure in
app_header.dart so leading and trailing are independently anchored while the
middle child is centered against the full available header width, preserving the
centering contract for variable-width trailing content like the nutrition range
selector.

In `@apps/mobile-flutter/lib/shell/sidebar.dart`:
- Around line 97-102: The sidebar filtering in sidebar.dart hardcodes isAdmin to
false, which hides the adminOnly nav item for every user. Update the logic
around the _navItems filter to use the real admin role/claim source instead of
the constant, or remove the /admin entry until an isAdmin provider is available.
Keep the fix localized to the sidebar item selection path so the admin gating
reflects actual session state.

---

Outside diff comments:
In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart`:
- Around line 572-635: The empty/loading/footer-only branches in feed_area.dart
currently return plain SingleChildScrollView, so they bypass the
RefreshIndicator.adaptive used by the populated ListView. Update the scroll
branches around the persistedMeals.isEmpty logic to use the same refreshable
wrapper and scroll physics as the main feed path, including the body-only and
_Footer rendering cases, so pull-to-refresh remains available when there are no
persisted meals.
- Around line 273-285: The failed-attempt retry card is not pinned to the day it
was submitted, so it can show on a different selected date after a stream error.
Update the error handling in feed_area.dart around the StreamStatus.error path
in the feed/state flow (including the _failedText logic and the card rendering
path) to capture the active submitted day when the request starts and only
surface the failed card when the current day still matches that captured day.
Apply the same date-guarding behavior to the related retry-card rendering code
referenced by the 362-376 block so the restored composer text and failed attempt
stay tied to the original day.

In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart`:
- Around line 460-503: Future-day cells in the TimelinePicker are still tappable
even though isFuture is already computed. Update the day cell logic in the
widget build/tap handling so future dates cannot be selected or sent to
widget.onSelect, and keep the existing visual treatment for isFuture consistent
with a disabled state. Use the isFuture, GestureDetector, and onSelect paths in
timeline_picker.dart to gate both tap callbacks and pressed feedback for future
dates.

---

Nitpick comments:
In `@components/nutrition/rows/food-chip-row.tsx`:
- Around line 27-65: The food-chip-row component is reading the full useQuery
result object, which disables TanStack Query’s tracked-property optimization.
Update the useQuery call in food-chip-row.tsx to destructure only the fields
actually used in the render path, namely data, error, isError, isLoading, and
isSuccess, and then use those local bindings throughout the component to keep
subscriptions minimal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b31d7c71-026d-49b9-8223-d87e6a6196db

📥 Commits

Reviewing files that changed from the base of the PR and between 96334f9 and 831cd6a.

⛔ Files ignored due to path filters (4)
  • apps/mobile-flutter/assets/google_fonts/BeVietnamPro-Bold.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/BeVietnamPro-Medium.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/BeVietnamPro-Regular.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/BeVietnamPro-SemiBold.ttf is excluded by !**/*.ttf
📒 Files selected for processing (77)
  • apps/mobile-flutter/assets/l10n/en.json
  • apps/mobile-flutter/assets/l10n/vi.json
  • apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_page.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_submit_button.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_text_field.dart
  • apps/mobile-flutter/lib/features/auth/widgets/confirm_email_view.dart
  • apps/mobile-flutter/lib/features/auth/widgets/google_button.dart
  • apps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/compact_weight_log.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/dashboard_tokens.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/section_header.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/today_section.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/weight_chart.dart
  • apps/mobile-flutter/lib/features/logging/screens/logging_screen.dart
  • apps/mobile-flutter/lib/features/logging/widgets/dashed_divider.dart
  • apps/mobile-flutter/lib/features/logging/widgets/empty_state.dart
  • apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart
  • apps/mobile-flutter/lib/features/logging/widgets/manual_log_sheet.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_entry.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_input.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_mode_sheet.dart
  • apps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dart
  • apps/mobile-flutter/lib/features/logging/widgets/streaming_entry.dart
  • apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart
  • apps/mobile-flutter/lib/features/logging/widgets/timeline_rail.dart
  • apps/mobile-flutter/lib/features/nutrition/logic/candidate_nutrients.dart
  • apps/mobile-flutter/lib/features/nutrition/providers/candidates_response.dart
  • apps/mobile-flutter/lib/features/nutrition/providers/food_candidates_provider.dart
  • apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/background_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/day_summary.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/focus_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/food_chip_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_grid_card.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/pull_quote.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/spotlight_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/steady_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dart
  • apps/mobile-flutter/lib/features/onboarding/widgets/onboarding_wizard.dart
  • apps/mobile-flutter/lib/features/settings/screens/account_section.dart
  • apps/mobile-flutter/lib/features/settings/screens/settings_screen.dart
  • apps/mobile-flutter/lib/main.dart
  • apps/mobile-flutter/lib/models/nutrition.dart
  • apps/mobile-flutter/lib/shared/widgets/target_progress_bar.dart
  • apps/mobile-flutter/lib/shared/widgets/top_toast.dart
  • apps/mobile-flutter/lib/shell/app_header.dart
  • apps/mobile-flutter/lib/shell/sidebar.dart
  • apps/mobile-flutter/lib/theme/nham_colors.dart
  • apps/mobile-flutter/lib/theme/nham_typography.dart
  • apps/mobile-flutter/pubspec.yaml
  • components/nutrition/rows/food-chip-row.tsx
  • components/nutrition/sections/verdict-hero.tsx
  • lib/api/contracts/nutrition.ts
  • lib/nutrition/__tests__/actions/candidates.test.ts
  • lib/nutrition/__tests__/catalog/food-source-candidates.test.ts
  • lib/nutrition/__tests__/catalog/reference-targets.test.ts
  • lib/nutrition/__tests__/schemas.test.ts
  • lib/nutrition/actions/candidates.ts
  • lib/nutrition/actions/overview/mapper.ts
  • lib/nutrition/catalog/food-source-candidates.ts
  • lib/nutrition/catalog/nutrients.ts
  • lib/nutrition/catalog/reference-targets.ts
  • lib/nutrition/pattern/date-range.ts
  • lib/nutrition/pattern/summary.ts
  • lib/nutrition/schemas.ts
  • lib/nutrition/types.ts
  • messages/en.json
💤 Files with no reviewable changes (13)
  • apps/mobile-flutter/lib/features/logging/widgets/dashed_divider.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/food_chip_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/spotlight_row.dart
  • apps/mobile-flutter/lib/features/logging/widgets/timeline_rail.dart
  • apps/mobile-flutter/lib/features/nutrition/logic/candidate_nutrients.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/background_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/steady_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/pull_quote.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/focus_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dart
✅ Files skipped from review due to trivial changes (3)
  • apps/mobile-flutter/lib/features/auth/widgets/google_button.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_submit_button.dart
  • apps/mobile-flutter/assets/l10n/en.json
🚧 Files skipped from review as they are similar to previous changes (13)
  • apps/mobile-flutter/lib/main.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_text_field.dart
  • apps/mobile-flutter/lib/features/auth/widgets/confirm_email_view.dart
  • apps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dart
  • apps/mobile-flutter/lib/features/settings/screens/account_section.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_page.dart
  • apps/mobile-flutter/lib/features/logging/screens/logging_screen.dart
  • apps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/today_section.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dart
  • apps/mobile-flutter/lib/features/settings/screens/settings_screen.dart
  • apps/mobile-flutter/assets/l10n/vi.json
  • apps/mobile-flutter/lib/features/logging/widgets/meal_entry.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart (2)

572-635: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep pull-to-refresh available for empty feeds.

RefreshIndicator.adaptive only wraps the populated ListView; empty, loading, and pending-only states use plain SingleChildScrollView, so users cannot refresh when the feed has no persisted meals.

Suggested direction
+  Widget _refreshable(Widget child) => RefreshIndicator.adaptive(
+        onRefresh: _refresh,
+        color: NhamColors.accent,
+        child: child,
+      );
+
@@
-        return SingleChildScrollView(
+        return _refreshable(SingleChildScrollView(
           keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
+          physics: const AlwaysScrollableScrollPhysics(),
           child: Padding(
             padding: const EdgeInsets.symmetric(vertical: NhamSpacing.sp6),
             child: Center(child: body),
           ),
-        );
+        ));

Apply the same wrapper/physics to the other empty or footer-only scroll branches.

Also applies to: 638-641

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart` around lines
572 - 635, The empty/loading/footer-only branches in feed_area.dart currently
return plain SingleChildScrollView, so they bypass the RefreshIndicator.adaptive
used by the populated ListView. Update the scroll branches around the
persistedMeals.isEmpty logic to use the same refreshable wrapper and scroll
physics as the main feed path, including the body-only and _Footer rendering
cases, so pull-to-refresh remains available when there are no persisted meals.

273-285: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pin failed-attempt cards to the submitted day.

Streaming/reveal cards are date-guarded, but _failedText is not. If an analysis fails after the user switches dates, the retry card and restored composer text can appear on the wrong day.

Suggested fix
   String? _failedText;
+  String? _failedDate;
@@
     setState(() {
       _failedText = null;
+      _failedDate = null;
       _revealRawInput = null;
       _inFlightText = text;
     });
@@
     if (next.status == StreamStatus.error) {
       final text = _inFlightText;
+      final failedDate = next.loggedDate ?? widget.date;
       setState(() {
         _failedText = text;
+        _failedDate = failedDate;
         _inFlightText = null;
       });
-      if (text != null && _inputController.getText().trim().isEmpty) {
+      if (failedDate == widget.date &&
+          text != null &&
+          _inputController.getText().trim().isEmpty) {
         _inputController.setText(text);
       }
@@
-    final hasFailedAttempt = _failedText != null;
+    final hasFailedAttempt =
+        _failedText != null && _failedDate == widget.date;

Also applies to: 362-376

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart` around lines
273 - 285, The failed-attempt retry card is not pinned to the day it was
submitted, so it can show on a different selected date after a stream error.
Update the error handling in feed_area.dart around the StreamStatus.error path
in the feed/state flow (including the _failedText logic and the card rendering
path) to capture the active submitted day when the request starts and only
surface the failed card when the current day still matches that captured day.
Apply the same date-guarding behavior to the related retry-card rendering code
referenced by the 362-376 block so the restored composer text and failed attempt
stay tied to the original day.
apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart (1)

460-503: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Disable future-day cells before they reach logging flows.

isFuture is computed, but the tap handler still selects those dates. Because the current centered week includes days after today, users can navigate to and log against future dates.

Suggested fix
-    final labelColor = isSelected ? NhamColors.text : NhamColors.textMuted;
+    final disabled = isFuture;
+    final labelColor =
+        disabled
+            ? NhamColors.textMuted50
+            : isSelected
+                ? NhamColors.text
+                : NhamColors.textMuted;
@@
-      onTapDown: (_) => setState(() => _pressed = true),
-      onTapUp: (_) => setState(() => _pressed = false),
-      onTapCancel: () => setState(() => _pressed = false),
-      onTap: () => widget.onSelect(widget.date),
+      onTapDown: disabled ? null : (_) => setState(() => _pressed = true),
+      onTapUp: disabled ? null : (_) => setState(() => _pressed = false),
+      onTapCancel: disabled ? null : () => setState(() => _pressed = false),
+      onTap: disabled ? null : () => widget.onSelect(widget.date),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart` around
lines 460 - 503, Future-day cells in the TimelinePicker are still tappable even
though isFuture is already computed. Update the day cell logic in the widget
build/tap handling so future dates cannot be selected or sent to
widget.onSelect, and keep the existing visual treatment for isFuture consistent
with a disabled state. Use the isFuture, GestureDetector, and onSelect paths in
timeline_picker.dart to gate both tap callbacks and pressed feedback for future
dates.
🧹 Nitpick comments (1)
components/nutrition/rows/food-chip-row.tsx (1)

27-65: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Destructure the useQuery result.

Reading the whole query object opts out of TanStack Query's tracked-property optimization, so this component subscribes to more state than it uses. Please destructure the fields you read (data, error, isError, isLoading, isSuccess) and verify that this is intentional.

♻️ Proposed fix
-  const query = useQuery({
+  const { data, error, isError, isLoading, isSuccess } = useQuery({
     queryKey: ['nutrition', 'candidates', nutrient],
     queryFn: () => getFoodSourceCandidates({ nutrient }),
     enabled,
     retry: false,
     staleTime: 60_000,
   });
 
-  if (query.isLoading) {
+  if (isLoading) {
     return <ChipRowSkeleton variant={variant} />;
   }
@@
-  if (query.isError) {
+  if (isError) {
     console.error(
       '[nutrition] candidates query failed for nutrient',
       nutrient,
-      query.error
+      error
     );
@@
-  if (query.isSuccess && !query.data.foods.length) {
+  if (isSuccess && !data.foods.length) {
@@
-  if (!query.data?.foods.length) {
+  if (!data?.foods.length) {
@@
-  const chips = query.data.foods.slice(0, limit);
+  const chips = data.foods.slice(0, limit);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/nutrition/rows/food-chip-row.tsx` around lines 27 - 65, The
food-chip-row component is reading the full useQuery result object, which
disables TanStack Query’s tracked-property optimization. Update the useQuery
call in food-chip-row.tsx to destructure only the fields actually used in the
render path, namely data, error, isError, isLoading, and isSuccess, and then use
those local bindings throughout the component to keep subscriptions minimal.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart`:
- Around line 15-17: Replace the custom Apple glyph/button in AppleButton with
Apple’s official SignInWithAppleButton control so branding and platform behavior
come from the system-provided widget. Update the AppleButton widget
implementation to use the provided Apple sign-in button rather than the
hand-rolled UI, keeping the existing auth action wiring intact while preserving
the current placement within the auth flow.
- Around line 39-47: The auth CTA in AppleButton is built with GestureDetector
and Opacity, so it is not exposed as a real accessible button or disabled
control. Replace the current interaction wrapper in AppleButton’s build logic
with a proper button primitive that supports semantics, focus, and disabled
state, and keep the pressed/busy visual behavior tied to that button instead of
manual tap handlers.

In `@apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart`:
- Around line 26-42: The shimmer still runs its AnimationController even when
reduced-motion is enabled, because _ShimmerState::_c starts repeating
unconditionally and build() only skips rendering. Update Shimmer/_ShimmerState
to start and stop the controller based on
MediaQuery.disableAnimationsOf(context) (or an equivalent media-change hook) so
the ticker is paused when animations are disabled and resumed when they are
allowed.

In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart`:
- Around line 245-255: Initialize _visibleAnchor from the same anchor used by
PageController initialPage instead of _selectedAnchor, so TimelinePicker starts
with the actual page being shown. Update the setup around _visibleAnchor,
_pageController, and _canNavigateNext in timeline_picker.dart so the right
chevron reflects the real visible page when selectedDate is on the current page.
Also make the same adjustment anywhere the current-page visibility is derived in
the related logic around the page handling flow.

In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart`:
- Around line 138-142: The nutrient grouping in nutrition_screen.dart is
treating every non-vitamin item as a mineral, so NutrientGroup.other can be
rendered under the wrong header. Update the logic around the
all/vitamins/minerals lists in the nutrition screen to filter minerals
explicitly by NutrientGroup.mineral (or equivalent) and keep NutrientGroup.other
separate so it does not fall into the Minerals section.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart`:
- Around line 171-176: The week label formatting in _label currently hard-codes
day/month order, so update the week branch to use the locale-aware short date
skeleton instead of the fixed pattern. Keep the existing weekday logic for
non-week units, and make the change in macro_trend_chart.dart within the _label
helper so week labels follow each locale’s conventions.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart`:
- Around line 37-38: The active range selection in range_selector.dart is using
_ranges.indexOf(resolvedRange) directly, so unsupported model values like 90d
resolve to -1 and get clamped to the first segment. Update the RangeSelector
logic to normalize resolvedRange to one of the supported _ranges values before
calculating activeIndex, and base the selected segment on that normalized value
so backend-resolved auto values map to the intended UI range.
- Around line 42-44: The range selector touch target is too small because the
SizedBox in range_selector.dart constrains the control to 34px height. Update
the layout in the range selector widget so the tappable area reaches at least
44pt, adjusting the parent SizedBox and any internal padding/segment sizing in
the range selector implementation to preserve the visual design while meeting
the target.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart`:
- Around line 49-54: The citations bottom sheet in source_attribution.dart is
using a non-scrollable Column, which can overflow with longer localized text or
larger accessibility fonts. Update the sheet layout in the widget built by the
builder and any related citation content sections so the content is wrapped in a
scrollable container such as SingleChildScrollView or ListView, while preserving
the existing Container styling and bottom-sheet structure.

In
`@apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart`:
- Around line 235-247: The chip actions in SuggestedFoodsSheet are interactive
but their current padding can fall below the 44pt mobile touch target. Update
the chip/widget builders in suggested_foods_sheet.dart, especially the
GestureDetector/AnimatedContainer path for the chip item and the refresh row
block, so their tappable area is at least 44pt tall while preserving the
existing layout and visuals. Use the existing chip action widget logic in the
affected builder functions to add vertical padding/minimum constraints rather
than shrinking the hit area.
- Around line 176-180: The rotating slot count in suggested_foods_sheet.dart is
widened to num by clamp, which then breaks the _cursor update logic. Update the
rotatingSlots calculation in the logic that builds the rotating FoodCandidate
list so it is converted back to int before being used in the modulo/update path,
keeping the _cursor assignment type-safe. Refer to the rotatingSlots variable
and the _cursor update in the suggested_foods_sheet widget logic when making the
fix.

In `@apps/mobile-flutter/lib/shared/widgets/top_toast.dart`:
- Around line 22-48: `showTopToast` currently inserts a new `OverlayEntry` every
time, which allows multiple top toasts to overlap; update the toast flow to
serialize or replace the active entry so only one `_TopToast` can be visible at
once. Use the existing `showTopToast`, `OverlayEntry`, and `_TopToast` logic to
track the current toast and either dismiss it or queue the next toast before
calling `overlay.insert`, ensuring the undo toast in the logging feed can remain
visible and actionable.

In `@apps/mobile-flutter/lib/shell/app_header.dart`:
- Around line 41-56: The current AppHeader layout in the build method still
centers the middle content relative to only a fixed spacer, so any wider
trailing widget shifts the title off-center. Update the Row-based structure in
app_header.dart so leading and trailing are independently anchored while the
middle child is centered against the full available header width, preserving the
centering contract for variable-width trailing content like the nutrition range
selector.

In `@apps/mobile-flutter/lib/shell/sidebar.dart`:
- Around line 97-102: The sidebar filtering in sidebar.dart hardcodes isAdmin to
false, which hides the adminOnly nav item for every user. Update the logic
around the _navItems filter to use the real admin role/claim source instead of
the constant, or remove the /admin entry until an isAdmin provider is available.
Keep the fix localized to the sidebar item selection path so the admin gating
reflects actual session state.

---

Outside diff comments:
In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart`:
- Around line 572-635: The empty/loading/footer-only branches in feed_area.dart
currently return plain SingleChildScrollView, so they bypass the
RefreshIndicator.adaptive used by the populated ListView. Update the scroll
branches around the persistedMeals.isEmpty logic to use the same refreshable
wrapper and scroll physics as the main feed path, including the body-only and
_Footer rendering cases, so pull-to-refresh remains available when there are no
persisted meals.
- Around line 273-285: The failed-attempt retry card is not pinned to the day it
was submitted, so it can show on a different selected date after a stream error.
Update the error handling in feed_area.dart around the StreamStatus.error path
in the feed/state flow (including the _failedText logic and the card rendering
path) to capture the active submitted day when the request starts and only
surface the failed card when the current day still matches that captured day.
Apply the same date-guarding behavior to the related retry-card rendering code
referenced by the 362-376 block so the restored composer text and failed attempt
stay tied to the original day.

In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart`:
- Around line 460-503: Future-day cells in the TimelinePicker are still tappable
even though isFuture is already computed. Update the day cell logic in the
widget build/tap handling so future dates cannot be selected or sent to
widget.onSelect, and keep the existing visual treatment for isFuture consistent
with a disabled state. Use the isFuture, GestureDetector, and onSelect paths in
timeline_picker.dart to gate both tap callbacks and pressed feedback for future
dates.

---

Nitpick comments:
In `@components/nutrition/rows/food-chip-row.tsx`:
- Around line 27-65: The food-chip-row component is reading the full useQuery
result object, which disables TanStack Query’s tracked-property optimization.
Update the useQuery call in food-chip-row.tsx to destructure only the fields
actually used in the render path, namely data, error, isError, isLoading, and
isSuccess, and then use those local bindings throughout the component to keep
subscriptions minimal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b31d7c71-026d-49b9-8223-d87e6a6196db

📥 Commits

Reviewing files that changed from the base of the PR and between 96334f9 and 831cd6a.

⛔ Files ignored due to path filters (4)
  • apps/mobile-flutter/assets/google_fonts/BeVietnamPro-Bold.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/BeVietnamPro-Medium.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/BeVietnamPro-Regular.ttf is excluded by !**/*.ttf
  • apps/mobile-flutter/assets/google_fonts/BeVietnamPro-SemiBold.ttf is excluded by !**/*.ttf
📒 Files selected for processing (77)
  • apps/mobile-flutter/assets/l10n/en.json
  • apps/mobile-flutter/assets/l10n/vi.json
  • apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_page.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_submit_button.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_text_field.dart
  • apps/mobile-flutter/lib/features/auth/widgets/confirm_email_view.dart
  • apps/mobile-flutter/lib/features/auth/widgets/google_button.dart
  • apps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/compact_weight_log.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/dashboard_tokens.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/section_header.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/today_section.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/weight_chart.dart
  • apps/mobile-flutter/lib/features/logging/screens/logging_screen.dart
  • apps/mobile-flutter/lib/features/logging/widgets/dashed_divider.dart
  • apps/mobile-flutter/lib/features/logging/widgets/empty_state.dart
  • apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart
  • apps/mobile-flutter/lib/features/logging/widgets/manual_log_sheet.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_entry.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_input.dart
  • apps/mobile-flutter/lib/features/logging/widgets/meal_mode_sheet.dart
  • apps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dart
  • apps/mobile-flutter/lib/features/logging/widgets/streaming_entry.dart
  • apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart
  • apps/mobile-flutter/lib/features/logging/widgets/timeline_rail.dart
  • apps/mobile-flutter/lib/features/nutrition/logic/candidate_nutrients.dart
  • apps/mobile-flutter/lib/features/nutrition/providers/candidates_response.dart
  • apps/mobile-flutter/lib/features/nutrition/providers/food_candidates_provider.dart
  • apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/background_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/day_summary.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/focus_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/food_chip_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_grid_card.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/pull_quote.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/spotlight_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/steady_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dart
  • apps/mobile-flutter/lib/features/onboarding/widgets/onboarding_wizard.dart
  • apps/mobile-flutter/lib/features/settings/screens/account_section.dart
  • apps/mobile-flutter/lib/features/settings/screens/settings_screen.dart
  • apps/mobile-flutter/lib/main.dart
  • apps/mobile-flutter/lib/models/nutrition.dart
  • apps/mobile-flutter/lib/shared/widgets/target_progress_bar.dart
  • apps/mobile-flutter/lib/shared/widgets/top_toast.dart
  • apps/mobile-flutter/lib/shell/app_header.dart
  • apps/mobile-flutter/lib/shell/sidebar.dart
  • apps/mobile-flutter/lib/theme/nham_colors.dart
  • apps/mobile-flutter/lib/theme/nham_typography.dart
  • apps/mobile-flutter/pubspec.yaml
  • components/nutrition/rows/food-chip-row.tsx
  • components/nutrition/sections/verdict-hero.tsx
  • lib/api/contracts/nutrition.ts
  • lib/nutrition/__tests__/actions/candidates.test.ts
  • lib/nutrition/__tests__/catalog/food-source-candidates.test.ts
  • lib/nutrition/__tests__/catalog/reference-targets.test.ts
  • lib/nutrition/__tests__/schemas.test.ts
  • lib/nutrition/actions/candidates.ts
  • lib/nutrition/actions/overview/mapper.ts
  • lib/nutrition/catalog/food-source-candidates.ts
  • lib/nutrition/catalog/nutrients.ts
  • lib/nutrition/catalog/reference-targets.ts
  • lib/nutrition/pattern/date-range.ts
  • lib/nutrition/pattern/summary.ts
  • lib/nutrition/schemas.ts
  • lib/nutrition/types.ts
  • messages/en.json
💤 Files with no reviewable changes (13)
  • apps/mobile-flutter/lib/features/logging/widgets/dashed_divider.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/food_chip_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/spotlight_row.dart
  • apps/mobile-flutter/lib/features/logging/widgets/timeline_rail.dart
  • apps/mobile-flutter/lib/features/nutrition/logic/candidate_nutrients.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/background_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/steady_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/pull_quote.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/focus_section.dart
  • apps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dart
✅ Files skipped from review due to trivial changes (3)
  • apps/mobile-flutter/lib/features/auth/widgets/google_button.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_submit_button.dart
  • apps/mobile-flutter/assets/l10n/en.json
🚧 Files skipped from review as they are similar to previous changes (13)
  • apps/mobile-flutter/lib/main.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_text_field.dart
  • apps/mobile-flutter/lib/features/auth/widgets/confirm_email_view.dart
  • apps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dart
  • apps/mobile-flutter/lib/features/settings/screens/account_section.dart
  • apps/mobile-flutter/lib/features/auth/widgets/auth_page.dart
  • apps/mobile-flutter/lib/features/logging/screens/logging_screen.dart
  • apps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/today_section.dart
  • apps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dart
  • apps/mobile-flutter/lib/features/settings/screens/settings_screen.dart
  • apps/mobile-flutter/assets/l10n/vi.json
  • apps/mobile-flutter/lib/features/logging/widgets/meal_entry.dart
🛑 Comments failed to post (14)
apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart (2)

15-17: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AppleButton call sites =="
rg -n -C3 '\bAppleButton\s*\(' apps/mobile-flutter/lib

echo
echo "== Platform gating near auth flow =="
rg -n -C3 'Platform\.isIOS|TargetPlatform\.iOS|defaultTargetPlatform|kIsWeb' \
  apps/mobile-flutter/lib/features/auth

Repository: VoMinhKhoii/Nham

Length of output: 2489


🌐 Web query:

What are Apple's current Sign in with Apple branding requirements for custom buttons, and do they allow replacing the provided button with a custom-rendered Apple logo glyph?

💡 Result:

Apple's branding requirements for Sign in with Apple prioritize the use of system-provided buttons, which are specifically designed to ensure a consistent user experience and maintain brand integrity [1][2]. Apple provides dedicated APIs for native apps (e.g., ASAuthorizationAppleIDButton) and web integrations (Sign in with Apple JS) that allow for standardized customization, such as adjusting button styles, text labels (e.g., "Sign in with Apple" vs "Sign up with Apple"), and alignment [3][4]. Regarding your specific questions: 1. Branding Requirements for Custom Buttons: Apple strongly recommends and typically requires the use of their provided button components [1][5][6]. While developers sometimes attempt to build custom buttons, they must strictly adhere to the Human Interface Guidelines (HIG) to avoid App Store rejection [7]. Official documentation consistently emphasizes that you should use the provided Apple-generated button artwork and assets rather than creating your own custom interpretations [8]. 2. Replacing the Button with a Custom-Rendered Apple Logo Glyph: You are not permitted to create your own custom-rendered Apple logo or replace the provided button with a custom implementation [8]. Apple requires you to use the logo artwork downloaded from their official Design Resources [8]. If you need a minimalist button, Apple provides an official "logo-only" mode (available in both web and native API implementations) that displays the Apple logo without accompanying text [3]. You should use this official configuration rather than attempting to render a custom glyph or button [3][8]. In summary, for compliance and to avoid rejection during app review, you should use the official API-provided buttons and choose from the supported configuration modes (such as "logo-only" or standard text-based buttons) provided by Apple's development frameworks [1][4][7].

Citations:


Use Apple’s provided Sign in with Apple button here. This custom Apple glyph/button is shown on both iOS and macOS, and Apple’s branding rules call for the official control rather than a hand-rolled logo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart` around lines
15 - 17, Replace the custom Apple glyph/button in AppleButton with Apple’s
official SignInWithAppleButton control so branding and platform behavior come
from the system-provided widget. Update the AppleButton widget implementation to
use the provided Apple sign-in button rather than the hand-rolled UI, keeping
the existing auth action wiring intact while preserving the current placement
within the auth flow.

39-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use a real button primitive here.

GestureDetector + Opacity does not expose the auth CTA as a proper button/disabled control to accessibility services. That makes the Apple sign-in entry point unreliable for VoiceOver/Switch Control users.

Suggested direction
-      child: GestureDetector(
-        onTapDown:
-            widget.busy ? null : (_) => setState(() => _pressed = true),
-        onTapUp: widget.busy ? null : (_) => setState(() => _pressed = false),
-        onTapCancel:
-            widget.busy ? null : () => setState(() => _pressed = false),
-        onTap: widget.busy ? null : widget.onPressed,
+      child: Semantics(
+        button: true,
+        enabled: !widget.busy,
+        label: tr('auth.dialog.continueWithApple'),
+        child: Material(
+          color: Colors.transparent,
+          child: InkWell(
+            onTap: widget.busy ? null : widget.onPressed,
+            onHighlightChanged: (pressed) {
+              if (_pressed != pressed) {
+                setState(() => _pressed = pressed);
+              }
+            },
+            borderRadius: BorderRadius.circular(NhamRadii.buttonXl),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    return Opacity(
      opacity: widget.busy ? 0.6 : 1.0,
      child: Semantics(
        button: true,
        enabled: !widget.busy,
        label: tr('auth.dialog.continueWithApple'),
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: widget.busy ? null : widget.onPressed,
            onHighlightChanged: (pressed) {
              if (_pressed != pressed) {
                setState(() => _pressed = pressed);
              }
            },
            borderRadius: BorderRadius.circular(NhamRadii.buttonXl),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart` around lines
39 - 47, The auth CTA in AppleButton is built with GestureDetector and Opacity,
so it is not exposed as a real accessible button or disabled control. Replace
the current interaction wrapper in AppleButton’s build logic with a proper
button primitive that supports semantics, focus, and disabled state, and keep
the pressed/busy visual behavior tied to that button instead of manual tap
handlers.
apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart (1)

26-42: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Stop the shimmer ticker when animations are disabled.

_c starts repeating in initState(), and build() only skips the ShaderMask. Reduced-motion users still pay for a live animation controller on every frame. Start/stop the controller from the current media setting instead of only bypassing the paint effect.

Suggested direction
 class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
   late final AnimationController _c = AnimationController(
     vsync: this,
     duration: const Duration(milliseconds: 1300),
-  )..repeat();
+  );
+
+  bool? _animationsDisabled;
+
+  `@override`
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    final disabled = MediaQuery.disableAnimationsOf(context);
+    if (_animationsDisabled == disabled) return;
+    _animationsDisabled = disabled;
+    if (disabled) {
+      _c.stop();
+    } else {
+      _c.repeat();
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
  late final AnimationController _c = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 1300),
  );

  bool? _animationsDisabled;

  `@override`
  void didChangeDependencies() {
    super.didChangeDependencies();
    final disabled = MediaQuery.disableAnimationsOf(context);
    if (_animationsDisabled == disabled) return;
    _animationsDisabled = disabled;
    if (disabled) {
      _c.stop();
    } else {
      _c.repeat();
    }
  }

  `@override`
  void dispose() {
    _c.dispose();
    super.dispose();
  }

  `@override`
  Widget build(BuildContext context) {
    // Honor reduced-motion: hold a static base tint, no sweep.
    if (MediaQuery.disableAnimationsOf(context)) return widget.child;
    return AnimatedBuilder(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart` around
lines 26 - 42, The shimmer still runs its AnimationController even when
reduced-motion is enabled, because _ShimmerState::_c starts repeating
unconditionally and build() only skips rendering. Update Shimmer/_ShimmerState
to start and stop the controller based on
MediaQuery.disableAnimationsOf(context) (or an equivalent media-change hook) so
the ticker is paused when animations are disabled and resumed when they are
allowed.
apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart (1)

245-255: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Initialize _visibleAnchor from the actual initial page.

For selected dates still rendered on the current page, _visibleAnchor can be a past date while the PageView is already on today’s page. That leaves the right chevron enabled even though there is no future page.

Suggested fix
 class _TimelineStripState extends State<TimelineStrip> {
-  late String _visibleAnchor = _selectedAnchor;
+  late final int _initialPage = _pageForAnchor(_selectedAnchor);
+  late String _visibleAnchor = _anchorForPage(_initialPage);
   late final PageController _pageController =
-      PageController(initialPage: _pageForAnchor(_selectedAnchor));
+      PageController(initialPage: _initialPage);

Also applies to: 309-344

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart` around
lines 245 - 255, Initialize _visibleAnchor from the same anchor used by
PageController initialPage instead of _selectedAnchor, so TimelinePicker starts
with the actual page being shown. Update the setup around _visibleAnchor,
_pageController, and _canNavigateNext in timeline_picker.dart so the right
chevron reflects the real visible page when selectedDate is on the current page.
Also make the same adjustment anywhere the current-page visibility is derived in
the related logic around the page handling flow.
apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart (1)

138-142: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't group NutrientGroup.other under “Minerals”.

Line 142 treats every non-vitamin as a mineral, but the model also has NutrientGroup.other. Those cards will render under the wrong header as soon as that enum value appears.

Proposed fix
 final all = [...overview.micronutrients, ...overview.moreNutrients];
 final vitamins =
     all.where((c) => c.group == NutrientGroup.vitamin).toList();
-final minerals =
-    all.where((c) => c.group != NutrientGroup.vitamin).toList();
+final minerals =
+    all.where((c) => c.group == NutrientGroup.mineral).toList();
+final other =
+    all.where((c) => c.group == NutrientGroup.other).toList();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart`
around lines 138 - 142, The nutrient grouping in nutrition_screen.dart is
treating every non-vitamin item as a mineral, so NutrientGroup.other can be
rendered under the wrong header. Update the logic around the
all/vitamins/minerals lists in the nutrition screen to filter minerals
explicitly by NutrientGroup.mineral (or equivalent) and keep NutrientGroup.other
separate so it does not fall into the Minerals section.
apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart (1)

171-176: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Dart intl 0.20.2, is DateFormat.Md(locale)the correct locale-aware replacement forDateFormat('d/M', locale) when formatting short month/day labels?

💡 Result:

Yes, DateFormat.Md(locale) is the recommended and correct locale-aware replacement for a manually defined pattern like DateFormat('d/M', locale) [1]. In the Dart intl package, DateFormat.Md is a named constructor that uses a predefined "skeleton" format [2][1]. When you use a skeleton constructor, the package automatically looks up the appropriate date format pattern for the specified locale [1]. Conversely, when you provide an explicit string pattern—such as 'd/M'—to the DateFormat constructor, the package treats it as a static format string, meaning it will not be adapted to suit local conventions [1]. By using DateFormat.Md(locale), you ensure that the date is displayed according to the formatting rules defined for that specific locale in the CLDR (Common Locale Data Repository), rather than forcing a specific 'd/M' structure that may be incorrect or unnatural for certain cultures [1].

Citations:


Use the locale-aware month/day skeleton for week labels.
DateFormat('d/M', locale) hard-codes the order, so locales that expect month/day formatting won’t get their local short date pattern. DateFormat.Md(locale) will adapt the label to the locale’s conventions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart`
around lines 171 - 176, The week label formatting in _label currently hard-codes
day/month order, so update the week branch to use the locale-aware short date
skeleton instead of the fixed pattern. Keep the existing weekday logic for
non-week units, and make the change in macro_trend_chart.dart within the _label
helper so week labels follow each locale’s conventions.
apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart (2)

37-38: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Normalize unsupported resolved ranges before clamping.

resolvedRange can still be 90d at the model layer, but _ranges.indexOf('90d') is -1, so this highlights the 1d segment. If the backend resolves auto to 90d, the control will show the wrong active range.

Proposed fix
-    final activeIndex =
-        _ranges.indexOf(resolvedRange).clamp(0, _ranges.length - 1);
+    final normalizedRange =
+        _ranges.contains(resolvedRange) ? resolvedRange : '30d';
+    final activeIndex = _ranges.indexOf(normalizedRange);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    final normalizedRange =
        _ranges.contains(resolvedRange) ? resolvedRange : '30d';
    final activeIndex = _ranges.indexOf(normalizedRange);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart`
around lines 37 - 38, The active range selection in range_selector.dart is using
_ranges.indexOf(resolvedRange) directly, so unsupported model values like 90d
resolve to -1 and get clamped to the first segment. Update the RangeSelector
logic to normalize resolvedRange to one of the supported _ranges values before
calculating activeIndex, and base the selected segment on that normalized value
so backend-resolved auto values map to the intended UI range.

42-44: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

This control is still below the 44pt touch-target goal.

The selector is rendered at 34px tall here, so each segment falls short of the 44pt target called out in the PR objectives. That makes the primary range switch harder to hit than the surrounding header controls.

Proposed fix
       child: SizedBox(
         width: 168,
-        height: 34,
+        height: 44,
         child: Container(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      child: SizedBox(
        width: 168,
        height: 44,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart`
around lines 42 - 44, The range selector touch target is too small because the
SizedBox in range_selector.dart constrains the control to 34px height. Update
the layout in the range selector widget so the tappable area reaches at least
44pt, adjusting the parent SizedBox and any internal padding/segment sizing in
the range selector implementation to preserve the visual design while meeting
the target.
apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart (1)

49-54: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the citations sheet scrollable.

The localized citation copy is in a non-scrollable Column, so longer translations or large text settings can overflow the bottom sheet.

Proposed fix
     context: context,
     backgroundColor: Colors.transparent,
     builder: (_) => Container(
+      constraints: BoxConstraints(
+        maxHeight: MediaQuery.of(context).size.height * 0.8,
+      ),
       decoration: const BoxDecoration(
         color: NhamColors.surface,
         borderRadius: BorderRadius.vertical(top: Radius.circular(NhamRadii.xxl)),
       ),
@@
-          Padding(
-            padding: EdgeInsets.fromLTRB(
-              NhamSpacing.sp5,
-              NhamSpacing.sp1,
-              NhamSpacing.sp5,
-              bottomInset + NhamSpacing.sp5,
-            ),
-            child: Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                Text(tr('nutrition.sources.intro'), style: dashMeta()),
-                const SizedBox(height: NhamSpacing.sp4),
-                _Citation(
-                  label: tr('nutrition.targetSources.whoFao'),
-                  detail: tr('nutrition.sources.whoFao'),
-                ),
-                _Citation(
-                  label: tr('nutrition.targetSources.vietnamRda'),
-                  detail: tr('nutrition.sources.vietnamRda'),
-                ),
-                _Citation(
-                  label: tr('nutrition.targetSources.nasem'),
-                  detail: tr('nutrition.sources.nasem'),
-                ),
-              ],
-            ),
+          Flexible(
+            child: SingleChildScrollView(
+              padding: EdgeInsets.fromLTRB(
+                NhamSpacing.sp5,
+                NhamSpacing.sp1,
+                NhamSpacing.sp5,
+                bottomInset + NhamSpacing.sp5,
+              ),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(tr('nutrition.sources.intro'), style: dashMeta()),
+                  const SizedBox(height: NhamSpacing.sp4),
+                  _Citation(
+                    label: tr('nutrition.targetSources.whoFao'),
+                    detail: tr('nutrition.sources.whoFao'),
+                  ),
+                  _Citation(
+                    label: tr('nutrition.targetSources.vietnamRda'),
+                    detail: tr('nutrition.sources.vietnamRda'),
+                  ),
+                  _Citation(
+                    label: tr('nutrition.targetSources.nasem'),
+                    detail: tr('nutrition.sources.nasem'),
+                  ),
+                ],
+              ),
+            ),
           ),

Also applies to: 84-110

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart`
around lines 49 - 54, The citations bottom sheet in source_attribution.dart is
using a non-scrollable Column, which can overflow with longer localized text or
larger accessibility fonts. Update the sheet layout in the widget built by the
builder and any related citation content sections so the content is wrapped in a
scrollable container such as SingleChildScrollView or ListView, while preserving
the existing Container styling and bottom-sheet structure.
apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart (2)

176-180: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Mobile docs ==\n'
for f in apps/docs/mobile/development.md apps/docs/mobile/releasing.md apps/docs/mobile/architecture.md; do
  echo "--- $f"
  wc -l "$f"
done

printf '\n== Suggested foods sheet outline ==\n'
ast-grep outline apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart --view expanded || true

printf '\n== Relevant lines around 160-220 ==\n'
sed -n '160,220p' apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart

printf '\n== Search for rotatingSlots/cursor usage ==\n'
rg -n "rotatingSlots|_cursor|_visibleCount|reserved.length|clamp\\(" apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart

Repository: VoMinhKhoii/Nham

Length of output: 2849


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Suggested foods sheet (lines 220-360) ==\n'
sed -n '220,360p' apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart

printf '\n== Find widget definitions in file ==\n'
rg -n "class _FoodChip|class _RefreshButton|GestureDetector|InkWell|SizedBox|Container|padding|constraints|minimumSize|hit" apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart

printf '\n== Search for any explicit tap target guidance in mobile docs ==\n'
rg -n "44|touch target|hit area|minimum size|interactive" apps/docs/mobile/development.md apps/docs/mobile/releasing.md apps/docs/mobile/architecture.md

Repository: VoMinhKhoii/Nham

Length of output: 4202


Cast rotatingSlots to int before the _cursor update.

clamp(...) widens this value to num, so the modulo assignment to _cursor at apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart:207-210 does not type-check unless rotatingSlots is converted first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart`
around lines 176 - 180, The rotating slot count in suggested_foods_sheet.dart is
widened to num by clamp, which then breaks the _cursor update logic. Update the
rotatingSlots calculation in the logic that builds the rotating FoodCandidate
list so it is converted back to int before being used in the modulo/update path,
keeping the _cursor assignment type-safe. Refer to the rotatingSlots variable
and the _cursor update in the suggested_foods_sheet widget logic when making the
fix.

235-247: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve 44pt touch targets for the chip actions.

The food chips and refresh row are interactive but their current padding can render below the mobile 44pt target size called out by the PR’s ergonomics goals.

Proposed fix
       child: AnimatedContainer(
         duration: const Duration(milliseconds: 120),
-        constraints: BoxConstraints(maxWidth: maxWidth),
+        constraints: BoxConstraints(maxWidth: maxWidth, minHeight: 44),
         padding: EdgeInsets.fromLTRB(
           reserved ? 8 : 10,
           6,
@@
       behavior: HitTestBehavior.opaque,
       onTap: onTap,
-      child: Padding(
-        padding: const EdgeInsets.symmetric(vertical: 2),
+      child: ConstrainedBox(
+        constraints: const BoxConstraints(minHeight: 44),
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 2),

Also applies to: 283-287

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart`
around lines 235 - 247, The chip actions in SuggestedFoodsSheet are interactive
but their current padding can fall below the 44pt mobile touch target. Update
the chip/widget builders in suggested_foods_sheet.dart, especially the
GestureDetector/AnimatedContainer path for the chip item and the refresh row
block, so their tappable area is at least 44pt tall while preserving the
existing layout and visuals. Use the existing chip action widget logic in the
affected builder functions to add vertical padding/minimum constraints rather
than shrinking the hit area.
apps/mobile-flutter/lib/shared/widgets/top_toast.dart (1)

22-48: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Serialize top toasts instead of stacking them.

Each call inserts a fresh OverlayEntry at the same top: 0 slot, so concurrent toasts overlap. That can hide the undo toast that apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart:181-206 keeps open for 5 seconds before finalizing delete, leaving the delete window running without a visible/actionable affordance. Keep a single active toast or queue replacements before inserting the next entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/shared/widgets/top_toast.dart` around lines 22 - 48,
`showTopToast` currently inserts a new `OverlayEntry` every time, which allows
multiple top toasts to overlap; update the toast flow to serialize or replace
the active entry so only one `_TopToast` can be visible at once. Use the
existing `showTopToast`, `OverlayEntry`, and `_TopToast` logic to track the
current toast and either dismiss it or queue the next toast before calling
`overlay.insert`, ensuring the undo toast in the logging feed can remain visible
and actionable.
apps/mobile-flutter/lib/shell/app_header.dart (1)

41-56: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

trailing breaks the centering contract.

This layout only keeps child centered when the right slot is 44px wide. Any wider trailing widget shifts the header content left, which already affects callers like the nutrition range selector. Use a layout that independently pins leading/trailing and centers the middle content against the full header width.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/shell/app_header.dart` around lines 41 - 56, The
current AppHeader layout in the build method still centers the middle content
relative to only a fixed spacer, so any wider trailing widget shifts the title
off-center. Update the Row-based structure in app_header.dart so leading and
trailing are independently anchored while the middle child is centered against
the full available header width, preserving the centering contract for
variable-width trailing content like the nutrition range selector.
apps/mobile-flutter/lib/shell/sidebar.dart (1)

97-102: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Admin users can never see the /admin item.

isAdmin is hardcoded to false, so the adminOnly row is filtered out for every session. That ships a missing role gate rather than an admin-only one. Please thread the real role/claim into this filter or remove the item until the role source exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile-flutter/lib/shell/sidebar.dart` around lines 97 - 102, The
sidebar filtering in sidebar.dart hardcodes isAdmin to false, which hides the
adminOnly nav item for every user. Update the logic around the _navItems filter
to use the real admin role/claim source instead of the constant, or remove the
/admin entry until an isAdmin provider is available. Keep the fix localized to
the sidebar item selection path so the admin gating reflects actual session
state.

VoMinhKhoii and others added 5 commits June 28, 2026 22:25
Iron's postmenopausal drop (24 → 10 mg at 50) was a 4-condition
special-case in the resolver with a magic 10 that wasn't in the data
table. Express it through the existing ageBands mechanism instead, so the
resolver's sex branch collapses to a plain lookup. Behavior-identical:
age-unknown still resolves to the <50 band (female 24).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bucket unit

Two thermo-review cleanups:

- supportsCandidates was always true since candidates went universal, and
  isSpotlightCandidate re-checked SUPPORTED_CANDIDATE_NUTRIENTS (identical
  to DEFAULT_NUTRIENTS) on cards already filtered to those. Remove the
  dead field + set/type across the DTO, aggregation, web detail, mobile
  model, and showChips gates. Behavior-identical.
- Replace the duplicated "'1d' || '7d' ? day : week" expression (two
  copies in the mapper) with a RANGE_BUCKET_UNIT lookup table, matching
  the other range-keyed records.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The input's filled area rendered ~5pt shorter than the Update button:
isDense sizes the InputDecorator fill to its content and centers it
inside the SizedBox(52), leaving transparent gaps. Drop the fixed heights
and use IntrinsicHeight + CrossAxisAlignment.stretch so the button
matches the field's actual filled height (now pixel-equal at 52).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the 3-stop gradient wash for a solid card + accent border
(hierarchy from the border, not alpha), drop the sparkle-chip icon, and
unify the four-size font zoo (10/11/11/12) onto the design-system scale
(eyebrow / 14 semibold title / 13 body / 13 CTA).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@VoMinhKhoii
VoMinhKhoii merged commit 355073c into main Jun 28, 2026
10 checks passed
@VoMinhKhoii
VoMinhKhoii deleted the feat/ux-wave0-mobile branch June 28, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant