phone_app: warm-tint the unified-design-system palette
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled

Adds theme.dart (dark-only ColorScheme + AppStatusColors
success/warning extension), wired into main.dart, home_screen.dart,
and settings_screen.dart. Fixes DESIGN_AUDIT_TODO violations: raw
Colors.* replaced with theme roles, off-grid spacing, sub-16px text,
and icon-contrast dimming.
This commit is contained in:
Krzysztof Rudnicki 2026-07-22 20:10:27 +02:00
parent 870a71665d
commit 191fa48b9d
4 changed files with 128 additions and 19 deletions

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:wake_alarm_sync/screens/home_screen.dart';
import 'package:wake_alarm_sync/ui/theme.dart';
/// Entry point.
void main() => runApp(const WakeAlarmSyncApp());
@ -14,12 +15,7 @@ class WakeAlarmSyncApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Wake Alarm Sync',
theme: ThemeData.dark(useMaterial3: true).copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.orange,
brightness: Brightness.dark,
),
),
theme: buildAppTheme(),
home: const HomeScreen(),
);
}

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:wake_alarm_sync/screens/settings_screen.dart';
import 'package:wake_alarm_sync/services/alarm_sync_service.dart';
import 'package:wake_alarm_sync/services/sync_settings.dart';
import 'package:wake_alarm_sync/ui/theme.dart';
/// Main screen: displays the current alarm time and syncs it to the PC via
/// the shared crdt_sync transport.
@ -131,7 +132,11 @@ class _HomeScreenState extends State<HomeScreen> {
title: const Text('Wake Alarm Sync'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
icon: Icon(
Icons.settings,
// Dimmed relative to the adjacent title text (rule 28).
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
onPressed: () async {
await Navigator.push<void>(
context,
@ -195,16 +200,22 @@ class _HomeScreenState extends State<HomeScreen> {
textAlign: TextAlign.center,
style: TextStyle(
color: _status.contains('Error')
? Colors.redAccent
: Colors.greenAccent,
? Theme.of(context).colorScheme.error
: Theme.of(
context,
).extension<AppStatusColors>()!.success,
),
),
if (!_configured) ...[
const SizedBox(height: 16),
const Text(
Text(
'Open Settings (gear icon) to connect GitHub.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.orange),
style: TextStyle(
color: Theme.of(
context,
).extension<AppStatusColors>()!.warning,
),
),
],
],

View File

@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:wake_alarm_sync/services/github_device_auth.dart';
import 'package:wake_alarm_sync/services/sync_settings.dart';
import 'package:wake_alarm_sync/ui/theme.dart';
/// Settings screen for the GitHub connection used to sync the alarm.
///
@ -108,7 +109,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
: const Icon(Icons.link),
label: const Text('Connect GitHub'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(fontSize: 16),
),
)
@ -124,18 +125,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
_status,
style: TextStyle(
color: _status.contains('Error')
? Colors.redAccent
: Colors.greenAccent,
? Theme.of(context).colorScheme.error
: Theme.of(
context,
).extension<AppStatusColors>()!.success,
),
),
const Divider(height: 32),
const Text(
Text(
'PC config — create this file:\n'
'~/.config/wake_alarm/sync.json\n\n'
'{ "token": "<same token>" }\n\n'
'The token needs "repo" scope so the PC can read '
'kuhyx/syncs at shutdown.',
style: TextStyle(fontSize: 12, color: Colors.grey),
// Was 12px/Colors.grey — below the 16px reading floor (rule
// 20) and off-palette (rule 4); this is real setup
// instructions, not decorative chrome.
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
@ -151,6 +160,8 @@ class _StatusCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final success = Theme.of(context).extension<AppStatusColors>()!.success;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
@ -158,9 +169,11 @@ class _StatusCard extends StatelessWidget {
children: [
Icon(
connected ? Icons.check_circle : Icons.cancel,
color: connected ? Colors.greenAccent : Colors.redAccent,
// Dimmed relative to the adjacent label text (rule 28).
color: (connected ? success : colorScheme.error)
.withValues(alpha: 0.8),
),
const SizedBox(width: 12),
const SizedBox(width: 8),
Expanded(
child: Text(
connected ? 'Connected to GitHub' : 'Not connected',
@ -242,7 +255,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: [

View File

@ -0,0 +1,86 @@
/// 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],
// 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,
);
}
}