app: warm-tint the shared palette, fix Snack chip + on-fill contrast
- theme.dart: warm unified-design-system palette, on-fill for onPrimary/onError, and inputDecorationTheme/dividerTheme (previously unset -> every TextField/Divider fell back to Material defaults). - slot_selector_row.dart: the Snack ChoiceChip had no explicit backgroundColor/side/labelStyle like the hour chips, so it fell through to Flutter's own default -- a literal pure-white border. - Regenerated launcher icon assets (warm charcoal + warm gold accent) via python_pkg/app_icons. - Bump gatelock pin to the released v0.1.1 carrying this same palette. Verified: flutter analyze clean, diet_guard pytest 443/443 @ 100% coverage, phone-installed APK pixel-checked (Snack chip border, on-fill contrast).
@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1B1D21</color>
|
||||
</resources>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<rect width="1024" height="1024" fill="#1B1D21"/>
|
||||
<g fill="none" stroke="#26A69A" stroke-width="72" stroke-linecap="round" stroke-linejoin="round" transform="translate(0,-2)">
|
||||
<rect width="1024" height="1024" fill="#211D1B"/>
|
||||
<g fill="none" stroke="#B8862E" stroke-width="72" stroke-linecap="round" stroke-linejoin="round" transform="translate(0,-2)">
|
||||
<path d="M 512 275 L 752 366 L 752 544 C 752 655 646 723 512 753 C 378 723 272 655 272 544 L 272 366 Z"/>
|
||||
<g transform="translate(356,362) scale(13)" fill="#26A69A" stroke="none">
|
||||
<g transform="translate(356,362) scale(13)" fill="#B8862E" stroke="none">
|
||||
<path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7z"/>
|
||||
<path d="M16 12h2.5v10H21V2c-2.76 0-5 2.24-5 5v5z"/>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 656 B After Width: | Height: | Size: 656 B |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
@ -1,7 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<g fill="none" stroke="#26A69A" stroke-width="72" stroke-linecap="round" stroke-linejoin="round" transform="translate(0,-2)">
|
||||
<g fill="none" stroke="#B8862E" stroke-width="72" stroke-linecap="round" stroke-linejoin="round" transform="translate(0,-2)">
|
||||
<path d="M 512 275 L 752 366 L 752 544 C 752 655 646 723 512 753 C 378 723 272 655 272 544 L 272 366 Z"/>
|
||||
<g transform="translate(356,362) scale(13)" fill="#26A69A" stroke="none">
|
||||
<g transform="translate(356,362) scale(13)" fill="#B8862E" stroke="none">
|
||||
<path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7z"/>
|
||||
<path d="M16 12h2.5v10H21V2c-2.76 0-5 2.24-5 5v5z"/>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 604 B |
163
app/lib/ui/theme.dart
Normal file
@ -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<AppStatusColors> {
|
||||
/// 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;
|
||||
}
|
||||
@ -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<AppStatusColors>()!;
|
||||
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),
|
||||
),
|
||||
],
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||