mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 22:13:53 +01:00
Three independent cleanups ahead of the configurable meal schedule.
Pills: the row was a `Wrap`, so pills dropped onto a second line as soon as
they outgrew the width -- which they do even at four. Measured: a logged chip
is ~124px at Material defaults, so four need 522px against ~330px of phone
width. Now a `FittedBox(scaleDown)` over a `Row`, which scales the row down
uniformly, plus compact chip density so there is less to scale away.
A `Row` of `Expanded` children was the obvious alternative and is wrong: it
imposes a tight width, and a Chip narrower than avatar+padding+label clips its
label rather than shrinking -- measured at 10px per label, about one character,
while throwing no overflow error. The new regression test therefore asserts
both a shared baseline and a legible label width; it was confirmed to fail on
the old `Wrap` ("chip wrapped to a second line at 320.0") rather than passing
vacuously.
Card: the `Logged "<meal>".` line restated the description the user had just
typed. Removed, along with `TodayProgress.justLogged`, which nothing else read.
The kcal position, macros and streak stay.
refactor_claude_todo.md: the 250-line file-splitting refactor it tracked is
done -- `scripts/check_file_length.sh --all` exits 0 and an independent sweep
finds nothing over the cap. DESIGN_AUDIT_TODO.md is a separate, still-open
effort and stays.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159b4E5hh512jYUeaJVNxfM
138 lines
4.7 KiB
Dart
138 lines
4.7 KiB
Dart
/// Post-log feedback card for the log screen: what was just logged, plus
|
|
/// today's standing against the daily budget.
|
|
library;
|
|
|
|
import 'package:diet_guard_app/ui/theme.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Everything [TodayProgressCard] renders, computed by the caller.
|
|
///
|
|
/// A plain value object rather than a service lookup so the card stays a
|
|
/// pure function of its inputs -- the log screen already holds the freshly
|
|
/// written log when it builds one of these, so a second read would be both
|
|
/// redundant and a chance to race.
|
|
@immutable
|
|
class TodayProgress {
|
|
/// Creates a [TodayProgress].
|
|
const TodayProgress({
|
|
required this.consumedKcal,
|
|
required this.budgetKcal,
|
|
required this.proteinG,
|
|
required this.carbsG,
|
|
required this.fatG,
|
|
required this.adherenceStreak,
|
|
});
|
|
|
|
/// Today's total calories, including the meal just logged.
|
|
final double consumedKcal;
|
|
|
|
/// The budget in effect *today* (not the schedule -- the card is only ever
|
|
/// about the current day).
|
|
final int budgetKcal;
|
|
|
|
/// Today's total protein, grams.
|
|
final double proteinG;
|
|
|
|
/// Today's total carbohydrate, grams.
|
|
final double carbsG;
|
|
|
|
/// Today's total fat, grams.
|
|
final double fatG;
|
|
|
|
/// Consecutive-day budget-adherence streak, as of today.
|
|
final int adherenceStreak;
|
|
|
|
/// Calories still available today; negative once over budget.
|
|
double get remainingKcal => budgetKcal - consumedKcal;
|
|
|
|
/// Whether today's total has passed the budget.
|
|
bool get isOverBudget => remainingKcal < 0;
|
|
}
|
|
|
|
/// Shows today's running budget position after a meal is logged.
|
|
///
|
|
/// It used to open with a `Logged "<meal>".` line, removed 2026-08-16: the
|
|
/// user had just typed that description, so it restated the input instead of
|
|
/// telling them anything. What remains is what they act on.
|
|
///
|
|
/// Replaces the old one-tap-reward prompt: the same slot in the log screen,
|
|
/// but showing information the user actually acts on instead of an
|
|
/// externally-configured URL. Deliberately has no protein *target* -- that
|
|
/// derives from body weight, which only the PC stores (see
|
|
/// `diet_guard/_budget.py`'s `protein_target_g`), so the card reports
|
|
/// protein consumed and leaves targets to the gate's dashboard.
|
|
class TodayProgressCard extends StatelessWidget {
|
|
/// Creates a [TodayProgressCard].
|
|
const TodayProgressCard({required this.progress, super.key});
|
|
|
|
/// The values to display.
|
|
final TodayProgress progress;
|
|
|
|
static String _plural(int count) => count == 1 ? 'day' : 'days';
|
|
|
|
static String _grams(double value) => '${value.round()}g';
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colors = theme.colorScheme;
|
|
final remaining = progress.remainingKcal.abs().round();
|
|
final remainingLabel = progress.isOverBudget
|
|
? '$remaining over'
|
|
: '$remaining left';
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
decoration: BoxDecoration(
|
|
// Fill-only elevation: the shared design system forbids shadows in
|
|
// dark UI, and mixing a border with a fill on the same surface.
|
|
color: colors.surfaceContainerHigh,
|
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
textBaseline: TextBaseline.alphabetic,
|
|
children: [
|
|
Text(
|
|
'${progress.consumedKcal.round()} / ${progress.budgetKcal}',
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
color: progress.isOverBudget
|
|
? colors.error
|
|
: colors.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(width: AppSpacing.xs),
|
|
Text('kcal', style: theme.textTheme.labelSmall),
|
|
const Spacer(),
|
|
Text(
|
|
remainingLabel,
|
|
style: theme.textTheme.labelMedium?.copyWith(
|
|
color: progress.isOverBudget
|
|
? colors.error
|
|
: colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppSpacing.xs),
|
|
Text(
|
|
'P ${_grams(progress.proteinG)} · '
|
|
'C ${_grams(progress.carbsG)} · '
|
|
'F ${_grams(progress.fatG)}',
|
|
style: theme.textTheme.labelSmall,
|
|
),
|
|
const SizedBox(height: AppSpacing.xs),
|
|
Text(
|
|
'Adherence streak: ${progress.adherenceStreak} '
|
|
'${_plural(progress.adherenceStreak)}',
|
|
style: theme.textTheme.labelSmall,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|