diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f65ea8..e7c2f41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -98,7 +98,7 @@ repos: additional_dependencies: - pytest - requests - - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.0#subdirectory=gatelock + - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.1#subdirectory=gatelock - crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync - mcp - pydantic diff --git a/app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png index 8d04026..bebf82f 100644 Binary files a/app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and b/app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png index ea48eea..e1f799c 100644 Binary files a/app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and b/app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png index b0f89f5..892bba9 100644 Binary files a/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and b/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png index 9bf73b9..c132beb 100644 Binary files a/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and b/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png index a8b1bee..a672a6b 100644 Binary files a/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and b/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 8e5cfc4..24015b2 100644 Binary files a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 23fab87..55879f0 100644 Binary files a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index ac1939a..033abb7 100644 Binary files a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index e4c166f..7ef4372 100644 Binary files a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 6031053..cdb0813 100644 Binary files a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/values/colors.xml b/app/android/app/src/main/res/values/colors.xml index 89a0cc8..610f420 100644 --- a/app/android/app/src/main/res/values/colors.xml +++ b/app/android/app/src/main/res/values/colors.xml @@ -1,4 +1,4 @@ #1B1D21 - + \ No newline at end of file diff --git a/app/assets/icon/icon.png b/app/assets/icon/icon.png index 59b690c..7c82e77 100644 Binary files a/app/assets/icon/icon.png and b/app/assets/icon/icon.png differ diff --git a/app/assets/icon/icon.svg b/app/assets/icon/icon.svg index c5c4a1a..20b6064 100644 --- a/app/assets/icon/icon.svg +++ b/app/assets/icon/icon.svg @@ -1,8 +1,8 @@ - - + + - + diff --git a/app/assets/icon/icon_foreground.png b/app/assets/icon/icon_foreground.png index 177d268..5daa2ab 100644 Binary files a/app/assets/icon/icon_foreground.png and b/app/assets/icon/icon_foreground.png differ diff --git a/app/assets/icon/icon_foreground.svg b/app/assets/icon/icon_foreground.svg index 1f68c44..a51ee01 100644 --- a/app/assets/icon/icon_foreground.svg +++ b/app/assets/icon/icon_foreground.svg @@ -1,7 +1,7 @@ - + - + diff --git a/app/lib/ui/theme.dart b/app/lib/ui/theme.dart new file mode 100644 index 0000000..b54cb21 --- /dev/null +++ b/app/lib/ui/theme.dart @@ -0,0 +1,163 @@ +/// The app's design tokens: one identical palette shared across every one +/// of kuhy's apps (Flutter, web, Python/Tkinter alike) — see the +/// `unified-design-system` skill (`~/.claude/skills/unified-design-system/`) +/// for the frozen token table this file implements. Built from an explicit +/// `ColorScheme`, not `ColorScheme.fromSeed`, because the shared palette is +/// hand-picked, not algorithmically derived from one seed color. +library; + +import 'package:flutter/material.dart'; + +/// Builds the app's single dark `ThemeData` from the shared token set. +ThemeData buildAppTheme() { + const colorScheme = ColorScheme.dark( + surface: Color(0xFF211D1B), // ink + surfaceContainerHighest: Color(0xFF38312E), // ink-raised-2 + surfaceContainerHigh: Color(0xFF2B2624), // ink-raised-1 + onSurface: Color(0xFFECEAE9), // text-on-dark + onSurfaceVariant: Color(0xFFAAA09A), // muted-on-dark + outline: Color(0xFF463E3A), // line-dark + primary: Color(0xFFB8862E), // accent + onPrimary: Color( + 0xFF211D1B, + ), // on-fill — every filled surface uses dark text + error: Color(0xFFE2585F), // danger + onError: Color(0xFF211D1B), // on-fill + ); + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + scaffoldBackgroundColor: colorScheme.surface, + extensions: const [AppStatusColors.dark], + textTheme: const TextTheme( + bodyLarge: TextStyle(fontSize: AppTextSize.body), + bodyMedium: TextStyle(fontSize: AppTextSize.body), + titleLarge: TextStyle(fontSize: AppTextSize.title), + titleMedium: TextStyle(fontSize: AppTextSize.subtitle), + labelMedium: TextStyle(fontSize: AppTextSize.label), + labelSmall: TextStyle(fontSize: AppTextSize.caption), + ), + // Previously unset, so every TextField fell back to M3's default + // indigo focus color instead of the shared palette (open finding from + // the unified-design-system component doc). + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: colorScheme.surfaceContainerHigh, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: BorderSide(color: colorScheme.outline), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: BorderSide(color: colorScheme.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: BorderSide(color: colorScheme.primary, width: 2), + ), + labelStyle: TextStyle(color: colorScheme.onSurfaceVariant), + ), + // Previously unset, so every Divider fell back to M3's default grey + // instead of `line-dark` (same open finding as above). + dividerTheme: DividerThemeData( + color: colorScheme.outline, + thickness: 1, + space: AppSpacing.md, + ), + ); +} + +/// Semantic status colors M3's [ColorScheme] has no role for (it only has +/// `error`). Success/warning are the shared `unified-design-system` values — +/// every status indicator (calendar day, slot chip, sync state) should read +/// these instead of an ad hoc `Colors.green`/`Colors.amber`. +@immutable +class AppStatusColors extends ThemeExtension { + /// Creates a status-color set. + const AppStatusColors({required this.success, required this.warning}); + + /// The shared dark-theme instance — success/warning from the unified + /// palette (danger already exists as `colorScheme.error`). + static const dark = AppStatusColors( + success: Color(0xFF8A9A3C), + warning: Color(0xFFE0A63C), + ); + + /// Positive/on-track status (e.g. a fully-logged, in-budget day). + final Color success; + + /// Caution/pending status (e.g. a partially-logged day). + final Color warning; + + @override + AppStatusColors copyWith({Color? success, Color? warning}) => AppStatusColors( + success: success ?? this.success, + warning: warning ?? this.warning, + ); + + @override + AppStatusColors lerp(AppStatusColors? other, double t) { + if (other is! AppStatusColors) return this; + return AppStatusColors( + success: Color.lerp(success, other.success, t) ?? success, + warning: Color.lerp(warning, other.warning, t) ?? warning, + ); + } +} + +/// Shared spacing scale (4px base) — round any new value to one of these +/// instead of introducing an off-scale literal. +abstract final class AppSpacing { + /// 4px. + static const double xs = 4; + + /// 8px. + static const double sm = 8; + + /// 16px. + static const double md = 16; + + /// 24px. + static const double lg = 24; + + /// 32px. + static const double xl = 32; + + /// 48px. + static const double xxl = 48; +} + +/// Shared corner-radius scale. Nested radii should be `outer - gap`, not a +/// fixed constant — compute per instance per safe-design-rules rule 24. +abstract final class AppRadius { + /// Buttons, inputs, chips. + static const double sm = 8; + + /// Cards. + static const double md = 12; + + /// Sheets, dialogs. + static const double lg = 16; +} + +/// Shared type scale (px). `body` is the floor for anything a user reads; +/// `label`/`caption` are for UI chrome only (timestamps, badges, tags). +abstract final class AppTextSize { + /// 12px — chrome only. + static const double caption = 12; + + /// 14px — chrome only. + static const double label = 14; + + /// 16px — the floor for actual reading content. + static const double body = 16; + + /// 20px. + static const double subtitle = 20; + + /// 24px. + static const double title = 24; + + /// 32px. + static const double display = 32; +} diff --git a/app/lib/widgets/slot_selector_row.dart b/app/lib/widgets/slot_selector_row.dart index 7d1d159..71d10c5 100644 --- a/app/lib/widgets/slot_selector_row.dart +++ b/app/lib/widgets/slot_selector_row.dart @@ -4,6 +4,7 @@ library; import 'package:diet_guard_app/models/slot.dart'; +import 'package:diet_guard_app/ui/theme.dart'; import 'package:flutter/material.dart'; /// One row of [ChoiceChip]s: one per today's slot hour plus a fixed @@ -34,24 +35,42 @@ class SlotSelectorRow extends StatelessWidget { @override Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final statusColors = Theme.of(context).extension()!; final elapsed = elapsedSlots(now).toSet(); + // Snack has no logged/due/upcoming status like the hour chips, so it + // uses `accent` as its "color" instead of a status color -- but still + // needs every property below set explicitly like the hour chips are. + // Leaving any of them unset falls through to Flutter's own ChoiceChip + // default, a literal pure-white border (the one banned value in this + // system) -- the open finding the unified-design-system doc flagged. + final snackSelected = selectedSlot == null; + final snackColor = snackSelected ? scheme.primary : scheme.onSurfaceVariant; return Wrap( - spacing: 6, - runSpacing: 4, + spacing: AppSpacing.xs + 2, + runSpacing: AppSpacing.xs, children: [ ...daySlots().map((slot) { final isLogged = loggedSlots.contains(slot); final isDue = !isLogged && elapsed.contains(slot); final color = isLogged - ? Colors.green + ? statusColors.success : isDue - ? Colors.red - : Colors.grey; + ? scheme.error + : scheme.onSurfaceVariant; final isSelected = selectedSlot == slot; return ChoiceChip( label: Text(slotLabel(slot)), selected: isSelected, - avatar: isLogged ? Icon(Icons.check, size: 14, color: color) : null, + // Icon reads lighter than the label text (rule 28): reduced + // opacity instead of the label's full-strength color. + avatar: isLogged + ? Icon( + Icons.check, + size: 14, + color: color.withValues(alpha: 0.72), + ) + : null, backgroundColor: color.withValues(alpha: 0.15), selectedColor: color.withValues(alpha: 0.35), labelStyle: TextStyle(color: color), @@ -64,8 +83,23 @@ class SlotSelectorRow extends StatelessWidget { }), ChoiceChip( label: const Text('Snack'), - avatar: const Icon(Icons.fastfood, size: 14), - selected: selectedSlot == null, + // Icon reads lighter than the label text (rule 28): reduced + // opacity instead of the label's full-strength color. + avatar: Icon( + Icons.fastfood, + size: 14, + color: snackColor.withValues(alpha: 0.72), + ), + selected: snackSelected, + backgroundColor: scheme.onSurfaceVariant.withValues(alpha: 0.15), + selectedColor: scheme.primary.withValues(alpha: 0.35), + labelStyle: TextStyle(color: snackColor), + side: BorderSide( + width: snackSelected ? 2 : 1, + color: snackSelected + ? snackColor + : snackColor.withValues(alpha: 0.4), + ), onSelected: (_) => onSlotSelected(null), ), ], diff --git a/pyproject.toml b/pyproject.toml index df80097..1305bb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "1.0.0" description = "Tkinter/systemd log-to-unlock meal gate with calorie/macro budget tracking" requires-python = ">=3.10" dependencies = [ - "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.0#subdirectory=gatelock", + "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.1#subdirectory=gatelock", "crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync", "requests>=2.31.0", ] diff --git a/requirements.txt b/requirements.txt index 268e520..006e2d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ bandit>=1.7.0 codespell>=2.2.0 coverage>=7.4.0 crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync -gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.0#subdirectory=gatelock +gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.1#subdirectory=gatelock mcp>=1.9 mypy>=1.8.0 pre-commit>=3.6.0