mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 14:27:39 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
- lightImpact + 160 ms confirm cue on a validated log (audioplayers, no audio focus); device-local opt-out switch in Settings, on by default - post-log card: confirmation line (slot + the meal's kcal), entrance and kcal/streak count-up on motion tokens transcribed into ui/theme.dart; durations collapse to zero under disableAnimations - test/flutter_test_config.dart mutes the cue suite-wide Todo: 8e66c1b9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWVHMLzeojNP4AaPoEj
241 lines
8.9 KiB
Dart
241 lines
8.9 KiB
Dart
/// 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
|
|
// secondary/tertiary — the shared palette has one accent, not a
|
|
// separate secondary hue, so these mirror primary. Without an explicit
|
|
// value here, widgets that reach for secondaryContainer (e.g.
|
|
// SegmentedButton's selected segment) silently fall back to Flutter's
|
|
// stock Material teal — confirmed live on-device in the todo app,
|
|
// which shares this same theme.dart template.
|
|
secondary: Color(0xFFB8862E), // accent
|
|
onSecondary: Color(0xFF211D1B), // on-fill
|
|
secondaryContainer: Color(0xFF463E3A), // line-dark
|
|
onSecondaryContainer: Color(0xFFB8862E), // accent
|
|
tertiary: Color(0xFFB8862E), // accent
|
|
onTertiary: Color(0xFF211D1B), // on-fill
|
|
tertiaryContainer: Color(0xFF463E3A), // line-dark
|
|
onTertiaryContainer: Color(0xFFB8862E), // accent
|
|
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;
|
|
}
|
|
|
|
/// Line-length and layout caps for the desktop surface.
|
|
///
|
|
/// The Linux desktop build is this same Dart running as Flutter **web** in a
|
|
/// Chrome `--app` window, so every screen is laid out at whatever width that
|
|
/// window happens to be — commonly 1366px on the laptop. Without a cap, a
|
|
/// phone-width single-column design simply stretches: prose ran ~180 characters
|
|
/// per line (roughly 2x the readable limit) and a four-digit calorie field
|
|
/// spanned ~1334px. Neither is clipped, so nothing looks broken; it is just
|
|
/// unusable to read.
|
|
abstract final class AppWidth {
|
|
/// 640px — the prose/paragraph cap (tokens.md rule 21, 40rem, ~65-70 chars).
|
|
static const double prose = 640;
|
|
|
|
/// 420px — cap for a single-purpose input (a number, a short name).
|
|
///
|
|
/// Narrower than [prose] because a field wider than its longest plausible
|
|
/// value reads as a layout error rather than an affordance.
|
|
static const double field = 420;
|
|
|
|
/// Viewport height below which the layout should switch to its compact form.
|
|
///
|
|
/// 700px, i.e. 768 minus typical window chrome: the supported floor is a
|
|
/// 1366x768 panel, and these are *short* landscape viewports, which
|
|
/// phone-portrait test sizes never exercise.
|
|
static const double shortViewport = 700;
|
|
}
|
|
|
|
/// Motion durations, hand-transcribed from the shared token source
|
|
/// `~/src/utils/unified-design-system/DOCS-motion.md`; edit both together.
|
|
///
|
|
/// Reduced motion collapses every step to zero via [resolve]. Haptics and
|
|
/// sound are not motion and must NOT be routed through this.
|
|
abstract final class AppDuration {
|
|
/// No animation; the reduced-motion target.
|
|
static const Duration instant = Duration.zero;
|
|
|
|
/// 120ms -- state change on an element already under the finger.
|
|
static const Duration fast = Duration(milliseconds: 120);
|
|
|
|
/// 200ms -- the default: things entering or moving within the view.
|
|
static const Duration base = Duration(milliseconds: 200);
|
|
|
|
/// 320ms -- full-surface change; the ceiling, not a midpoint.
|
|
static const Duration slow = Duration(milliseconds: 320);
|
|
|
|
/// [d], or [instant] when the OS asks for reduced motion.
|
|
static Duration resolve(BuildContext context, Duration d) =>
|
|
MediaQuery.disableAnimationsOf(context) ? instant : d;
|
|
}
|
|
|
|
/// Motion easing curves, hand-transcribed from the same token source as
|
|
/// [AppDuration] (`DOCS-motion.md`).
|
|
abstract final class AppCurve {
|
|
/// Default for anything that starts and ends on screen.
|
|
static const Cubic standard = Cubic(0.2, 0, 0, 1);
|
|
|
|
/// Entering the screen: full speed, eases to rest.
|
|
static const Cubic decelerate = Cubic(0, 0, 0, 1);
|
|
|
|
/// Leaving the screen: eases in, exits at speed.
|
|
static const Cubic accelerate = Cubic(0.3, 0, 1, 1);
|
|
}
|