From 0e9acb9e18c59e261f5854261d88ebd1fdbaae9b Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Tue, 21 Jul 2026 21:50:16 +0200 Subject: [PATCH] Wire theme.dart into the app; replace remaining raw Colors.* literals main.dart now builds MaterialApp's theme from buildAppTheme() instead of ColorScheme.fromSeed(Colors.teal) -- theme.dart existed but was never actually used until this commit. photo_viewer_screen.dart, settings_screen.dart, and day_status_calendar.dart's raw Colors.* literals (Colors.black, Colors.red, Colors.white38, Colors.grey.shade800, ...) now read from Theme.of(context) instead. Also rewrites CLAUDE.md's Visual Design & Theming section, which previously contradicted the unified-design-system skill (noise texture, multi-layered shadows, ColorScheme.fromSeed -- all things that skill explicitly forbids). This was the other prerequisite the on-fill/warm-palette fixes actually needed: theme.dart existing but unwired meant the app was still running Flutter's default seeded theme. --- CLAUDE.md | 26 ++++++--- app/lib/main.dart | 3 +- app/lib/screens/photo_viewer_screen.dart | 9 +-- app/lib/screens/settings_screen.dart | 5 +- app/lib/widgets/day_status_calendar.dart | 74 +++++++++++++++--------- 5 files changed, 75 insertions(+), 42 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9163c33..6fc81e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -535,15 +535,20 @@ linter: provide an intuitive and easy navigation bar or controls. * **Typography:** Stress and emphasize font sizes to ease understanding, e.g., hero text, section headlines, list headlines, keywords in paragraphs. -* **Background:** Apply subtle noise texture to the main background to add a - premium, tactile feel. -* **Shadows:** Multi-layered drop shadows create a strong sense of depth; cards - have a soft, deep shadow to look "lifted." +* **Background:** Keep backgrounds flat and simple — no noise texture or + gradients. Follow the `unified-design-system` skill (safe-design-rules + rule 17: simple-on-complex); let content and typography carry visual + interest instead of background texture. +* **Shadows:** Follow the `unified-design-system` skill's shadow policy: no + shadows at all in dark UI (differentiate elevation with lighter surface + fills, not shadows); in light UI, shadows are for floating/overlay + elements only (menus, dialogs), never for inline cards, and never mixed + with a border on the same surface. * **Icons:** Incorporate icons to enhance the user’s understanding and the logical navigation of the app. -* **Interactive Elements:** Buttons, checkboxes, sliders, lists, charts, graphs, - and other interactive elements have a shadow with elegant use of color to - create a "glow" effect. +* **Interactive Elements:** Buttons/controls signal state via color and fill + changes, not a "glow" shadow — one depth technique per surface, per + `unified-design-system`. #### Theming * **Centralized Theme:** Define a centralized `ThemeData` object to ensure a @@ -551,8 +556,11 @@ linter: * **Light and Dark Themes:** Implement support for both light and dark themes, ideal for a user-facing theme toggle (`ThemeMode.light`, `ThemeMode.dark`, `ThemeMode.system`). -* **Color Scheme Generation:** Generate harmonious color palettes from a single - color using `ColorScheme.fromSeed`. +* **Color Scheme Generation:** Do NOT use `ColorScheme.fromSeed` for this + app's palette — it can't reproduce hand-picked values exactly. Build + `ThemeData` from explicit `ColorScheme.dark(...)`/`.light(...)` using the + frozen, identical-everywhere tokens in the `unified-design-system` skill + (`~/.claude/skills/unified-design-system/references/tokens.md`). ```dart final ThemeData lightTheme = ThemeData( diff --git a/app/lib/main.dart b/app/lib/main.dart index f1965c7..5f092a0 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -11,6 +11,7 @@ import 'package:diet_guard_app/services/foodbank_service.dart'; import 'package:diet_guard_app/services/frame_stats.dart'; import 'package:diet_guard_app/services/log_storage_service.dart'; import 'package:diet_guard_app/services/notification_service.dart'; +import 'package:diet_guard_app/ui/theme.dart'; import 'package:flutter/material.dart'; Future main() async { @@ -49,7 +50,7 @@ class DietGuardApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Diet Guard', - theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true), + theme: buildAppTheme(), home: const LogMealScreen(), ); } diff --git a/app/lib/screens/photo_viewer_screen.dart b/app/lib/screens/photo_viewer_screen.dart index 06e1941..0ccf324 100644 --- a/app/lib/screens/photo_viewer_screen.dart +++ b/app/lib/screens/photo_viewer_screen.dart @@ -16,18 +16,19 @@ class PhotoViewerScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: Colors.black, + backgroundColor: scheme.surface, appBar: AppBar( - backgroundColor: Colors.black, - foregroundColor: Colors.white, + backgroundColor: scheme.surface, + foregroundColor: scheme.onSurface, ), body: Center( child: InteractiveViewer( child: AttachedImage( path: path, errorIconSize: 64, - errorIconColor: Colors.white, + errorIconColor: scheme.onSurface, ), ), ), diff --git a/app/lib/screens/settings_screen.dart b/app/lib/screens/settings_screen.dart index 189b918..dfc1ef5 100644 --- a/app/lib/screens/settings_screen.dart +++ b/app/lib/screens/settings_screen.dart @@ -564,7 +564,10 @@ class _DeviceCodeDialogState extends State<_DeviceCodeDialog> { ], ) else - Text(_error!, style: const TextStyle(color: Colors.red)), + Text( + _error!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), ], ), actions: [ diff --git a/app/lib/widgets/day_status_calendar.dart b/app/lib/widgets/day_status_calendar.dart index 3181335..20bd817 100644 --- a/app/lib/widgets/day_status_calendar.dart +++ b/app/lib/widgets/day_status_calendar.dart @@ -2,6 +2,7 @@ library; import 'package:diet_guard_app/models/day_status.dart'; +import 'package:diet_guard_app/ui/theme.dart'; import 'package:flutter/material.dart'; /// Monthly calendar widget that colors each day by its [DayStatus]. @@ -74,22 +75,35 @@ class DayStatusCalendar extends StatelessWidget { return statusByDate[_dateKey(year, m, day)] ?? DayStatus.notLogged; } - Color _cellColor(DayStatus? status) => switch (status) { - null => Colors.grey.shade800, - DayStatus.notLogged => Colors.black, - DayStatus.red => Colors.red.shade400, - DayStatus.yellow => Colors.amber.shade700, - DayStatus.green => Colors.green.shade700, + Color _cellColor( + DayStatus? status, + ColorScheme scheme, + AppStatusColors statusColors, + ) => switch (status) { + // Future day: nothing to judge yet, so it reads as part of the + // panel rather than an empty/failed slot. + null => scheme.surfaceContainerHigh, + // Recessed below the panel (darker than the surrounding + // ink-raised-1 container) with a visible outline, so it reads as + // "empty" without vanishing into the background. + DayStatus.notLogged => scheme.surface, + DayStatus.red => scheme.error, + DayStatus.yellow => statusColors.warning, + DayStatus.green => statusColors.success, }; - Color _textColor(DayStatus? status) => switch (status) { - null => Colors.white38, - DayStatus.notLogged => Colors.white, - DayStatus.red || DayStatus.yellow || DayStatus.green => Colors.black, + Color _textColor(DayStatus? status, ColorScheme scheme) => switch (status) { + null => scheme.onSurfaceVariant, + DayStatus.notLogged => scheme.onSurface, + // Status colors sit at mid brightness, so the near-black `ink` role + // (not a separate pure black) gives strong, on-palette contrast. + DayStatus.red || DayStatus.yellow || DayStatus.green => scheme.surface, }; @override Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final statusColors = Theme.of(context).extension()!; final year = month.year; final m = month.month; final daysInMonth = DateTime(year, m + 1, 0).day; @@ -99,10 +113,10 @@ class DayStatusCalendar extends StatelessWidget { final rows = (totalCells / 7).ceil(); return Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), + color: scheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.md), ), child: Column( children: [ @@ -110,28 +124,28 @@ class DayStatusCalendar extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( - icon: const Icon(Icons.chevron_left, color: Colors.white70), + icon: Icon(Icons.chevron_left, color: scheme.onSurfaceVariant), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, onPressed: onPrevMonth, ), Text( '${_monthNames[m - 1]} $year', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: scheme.onSurface, fontWeight: FontWeight.bold, - fontSize: 14, + fontSize: AppTextSize.body, ), ), IconButton( - icon: const Icon(Icons.chevron_right, color: Colors.white70), + icon: Icon(Icons.chevron_right, color: scheme.onSurfaceVariant), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, onPressed: onNextMonth, ), ], ), - const SizedBox(height: 6), + const SizedBox(height: AppSpacing.xs + 2), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: _weekHeaders @@ -141,9 +155,12 @@ class DayStatusCalendar extends StatelessWidget { child: Text( h, textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white38, - fontSize: 11, + // Two-letter day abbreviations are UI chrome (like a + // badge/tag), deliberately kept below the 16px body + // floor rather than forced to grow past their slot. + style: TextStyle( + color: scheme.onSurfaceVariant, + fontSize: AppTextSize.label - 2, fontWeight: FontWeight.bold, ), ), @@ -151,7 +168,7 @@ class DayStatusCalendar extends StatelessWidget { ) .toList(), ), - const SizedBox(height: 4), + const SizedBox(height: AppSpacing.xs), ...List.generate(rows, (row) { return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -168,17 +185,20 @@ class DayStatusCalendar extends StatelessWidget { margin: const EdgeInsets.symmetric(vertical: 2), decoration: BoxDecoration( shape: BoxShape.circle, - color: _cellColor(status), + color: _cellColor(status, scheme, statusColors), border: status == DayStatus.notLogged - ? Border.all(color: Colors.white70) + ? Border.all(color: scheme.outline) : null, ), alignment: Alignment.center, child: Text( '$day', + // A fixed 30px circle can't fit 16px bold digits for + // two-digit days without overflowing — kept as a + // deliberate chrome exception (rule 4), not accidental. style: TextStyle( - color: _textColor(status), - fontSize: 12, + color: _textColor(status, scheme), + fontSize: AppTextSize.label - 2, fontWeight: status != null && status != DayStatus.notLogged ? FontWeight.bold