wake-alarm/phone_app/lib/ui/theme.dart
Krzysztof kuhy Rudnicki 34037e55c1 phone: sick days — picker, silent ring, stick stand-down, pre-alarm sync
- Sick days screen (own name, sick icon, cat-2 fill): tap today or a later
  day to mark it sick, tap again to clear; past days inert. Writes the
  sickdays wire format (state sick/cleared + actor, one HLC per write) to
  sickdays-sync/devices via crdt_sync syncLog.
- Mutual exclusion in both pickers: each syncs the other pool on open and
  refuses a date it holds, naming the pool; an unreadable pool refuses all.
- Native SickDays cache (fail closed via DayCache); AlarmReceiver skips the
  ring with reason "sick_day" (no markFired, no ring_started); StickCheck
  verdict sick_day, streak skips sick days; stakes card says so.
- Pre-alarm pool sync: allow-while-idle alarm at 21:00 and A-30 (request
  code 1006) runs a headless engine (poolSyncMain) that pulls both pools
  from RTDB and rewrites both native caches, so a PC mark arrives with the
  app closed. The ring decision stays a cache read.
- JVM unit tests (JUnit 4.13.2) for DayCache, RingDecision,
  PoolSyncSchedule, StickCheck; Dart tests; Dart->Python interop check.

pylint hook skipped: verify.py imports sickdays, not in the hook env until
the sickdays-v0.1.0 tag exists; run by hand in a sickdays venv (10.00/10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
2026-10-16 13:22:40 +02:00

124 lines
4.7 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 — filled surfaces use dark text
// secondary/tertiary mirror primary — the shared palette has one accent,
// not a separate secondary hue. Leaving these undefined lets Flutter
// fall back to stock Material teal on any widget that reaches for
// secondaryContainer (Chip, SegmentedButton, etc.) — confirmed as a
// real, live bug on two other apps sharing this template.
secondary: Color(0xFFB8862E),
onSecondary: Color(0xFF211D1B),
secondaryContainer: Color(0xFF463E3A),
onSecondaryContainer: Color(0xFFB8862E),
tertiary: Color(0xFFB8862E),
onTertiary: Color(0xFF211D1B),
tertiaryContainer: Color(0xFF463E3A),
onTertiaryContainer: Color(0xFFB8862E),
error: Color(0xFFE2585F), // danger
onError: Color(0xFF211D1B), // on-fill
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
scaffoldBackgroundColor: colorScheme.surface,
extensions: const [AppStatusColors.dark, DayMarkColors.dark],
// No custom shadow/elevation exists elsewhere in this app — the one
// Card (_StatusCard) is the sole place M3's default elevation would
// otherwise introduce a shadow in an all-dark UI (rule 26).
cardTheme: const CardThemeData(elevation: 0),
);
}
/// Semantic status colors M3's [ColorScheme] has no role for (it only has
/// `error`). Used instead of the ad hoc `Colors.greenAccent`/`Colors.orange`
/// literals previously scattered across both screens.
@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/connected status (e.g. "Connected to GitHub").
final Color success;
/// Caution/pending status (e.g. "Open Settings to connect GitHub").
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,
);
}
}
/// The fill of a marked day on the two day-pool calendars.
///
/// Free days keep `accent` (the theme's primary). Sick days take `cat-2`
/// from the shared categorical ramp: hue-separated from the warm accent /
/// success / warning / danger wedge by design, so a sick day can never be
/// read as a free day (accent), a warning or an error. Text on it is
/// `on-fill` (`ink`, 4.7:1), never `text-on-dark` (3.0:1).
@immutable
class DayMarkColors extends ThemeExtension<DayMarkColors> {
/// Creates a day-mark color set.
const DayMarkColors({required this.sick, required this.onSick});
/// The shared dark-theme instance.
static const dark = DayMarkColors(
sick: Color(0xFF398FC0), // cat-2
onSick: Color(0xFF211D1B), // on-fill
);
/// A sick day's fill.
final Color sick;
/// Text on [sick].
final Color onSick;
@override
DayMarkColors copyWith({Color? sick, Color? onSick}) =>
DayMarkColors(sick: sick ?? this.sick, onSick: onSick ?? this.onSick);
@override
DayMarkColors lerp(DayMarkColors? other, double t) {
if (other is! DayMarkColors) return this;
return DayMarkColors(
sick: Color.lerp(sick, other.sick, t) ?? sick,
onSick: Color.lerp(onSick, other.onSick, t) ?? onSick,
);
}
}