mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:27:14 +01:00
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:
parent
26f95d42f5
commit
0e9acb9e18
26
CLAUDE.md
26
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(
|
||||
|
||||
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user