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.
This commit is contained in:
Krzysztof Rudnicki 2026-07-21 21:50:16 +02:00
parent 26f95d42f5
commit 0e9acb9e18
5 changed files with 75 additions and 42 deletions

View File

@ -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(

View File

@ -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<void> 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(),
);
}

View File

@ -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,
),
),
),

View File

@ -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: [

View File

@ -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<AppStatusColors>()!;
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