sprint-4b: Flutter display theme system (AutopilotTheme + 4 palettes + tests)

Implements the visual theme system specified in mock up software.pdf.

Flutter project — display/:
- pubspec.yaml: ar_autopilot_display v0.4.0+4 (provider + shared_preferences)
- lib/theme/autopilot_theme.dart: AutopilotTheme class with 30+ design tokens
  (backgrounds, panels, text, accent, set-point, semantic states, DISENGAGE,
  action buttons, glow helpers, backgroundDecoration getter)
- lib/theme/theme_registry.dart: ThemeRegistry with 4 factory themes,
  byId() fallback, architecture stub for Sprint 9 custom YAML themes
- lib/theme/theme_provider.dart: AutopilotThemeProvider (ChangeNotifier),
  SharedPreferences persistence under 'autopilot.theme.id', NOT sent to ESP32
- lib/theme/themes/: 4 factory themes with exact hex values from spec:
    light (cream/navy, accentGlowRadius=0 — daytime no-glow rule)
    cyan (deep navy/neon-cyan, default, glowRadius=16)
    wine (vinotinto, DISENGAGE=amber not red, glowRadius=18)
    ochre (warm brown/gold, okColor=lime for contrast, glowRadius=18)
- lib/screens/settings/appearance_settings.dart: Appearance screen with
  4-card theme previews (200x120px), 400ms AnimatedContainer transitions,
  triple-tap shortcut note, Sprint-5 placeholders for auto day/night and
  ambient light sensor toggles
- lib/widgets/themed/: 4 themed widgets consuming AutopilotThemeProvider:
    compass_rose.dart (heading arc, N mark, set-point tick, glow ring)
    disengage_button.dart (60x60 min touch target, gradient, glow)
    mode_selector.dart (STANDBY/HDG HOLD/TRACK with accent highlight)
    rudder_indicator.dart (horizontal bar -35° to +35°, accent knob)
- lib/main.dart: app entry point with ChangeNotifierProvider

Tests — display/test/theme/:
- theme_registry_test.dart: 4 themes load, correct IDs, display order,
  no null tokens, glow rules, backgroundGradient rules
- theme_provider_test.dart: default load=cyan, persistence across restarts,
  setTheme notifies listeners, unknown ID falls back to default
- theme_contrast_test.dart: WCAG checks — DISENGAGE ≥7:1 AAA,
  action buttons ≥4.5:1 AA, textMain ≥4.5:1 AA, setLight/okColor ≥3:1

Also:
- .gitignore: added !display/lib/ exception (lib/ was excluded for Python venv)

Design rules enforced:
- No glow in light mode (accentGlowRadius=0)
- DISENGAGE = amber in wine theme (red would blend into palette)
- North mark = warm colour on all themes (nautical convention)
- Touch targets: 48px nominal, 60px critical
- Theme not sent to ESP32, not synced between displays

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 00:33:04 -04:00
parent e4812e9b44
commit cb138a3248
18 changed files with 1701 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import '../autopilot_theme.dart';
/// **cyan** — Cockpit cian (default)
///
/// The default factory theme. Matches the visual language of professional
/// marine electronics (Raymarine, Garmin, Simrad). Neon cyan on deep navy.
/// Used for twilight / night operation with ambient lighting.
const AutopilotTheme cyanTheme = AutopilotTheme(
id: 'cyan',
displayName: 'Cockpit cian',
background: Color(0xFF0D1822),
backgroundMid: Color(0xFF0F172A),
backgroundDeep: Color(0xFF0A1220),
backgroundDeepest: Color(0xFF020610),
backgroundGradient: RadialGradient(
colors: [Color(0xFF0D1822), Color(0xFF050810)],
stops: [0.0, 0.7],
),
panelBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xE6142030), Color(0xE608101C)], // rgba(20,32,48,0.9) → rgba(8,16,28,0.9)
),
panelBorder: Color(0x4038BDF8), // rgba(56,189,248,0.25)
textMain: Color(0xFFE0F2FE),
textMuted: Color(0xFF64748B),
textSoft: Color(0xFFCBD5E1),
textDisabled: Color(0xFF475569),
accentLight: Color(0xFF7DD3FC),
accentMid: Color(0xFF0EA5E9),
accentDark: Color(0xFF0369A1),
accentGlowRadius: 16.0,
accentGlowColor: Color(0x9938BDF8), // rgba(56,189,248,0.6)
setLight: Color(0xFFFBBF24),
setDark: Color(0xFFA16207),
setGlow: Color(0x66FBBF24),
okColor: Color(0xFF4ADE80),
warnColor: Color(0xFFFBBF24),
northColor: Color(0xFFF87171),
disengageBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFDC2626), Color(0xFF7F1D1D)],
),
disengageText: Color(0xFFFFFFFF),
disengageBorder: Color(0xFFF87171),
disengageGlow: Color(0x80DC2626),
actionButtonBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF1E4A72), Color(0xFF0C2540)], // improved contrast
),
actionButtonBorder: Color(0xFF38BDF8), // accented border, 1px
actionButtonText: Color(0xFFE0F2FE),
actionButtonGlow: Color(0x667DD3FC), // subtle text-shadow
);
+61
View File
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import '../autopilot_theme.dart';
/// **light** — Claro (día)
///
/// Daytime operation under direct sunlight. Navy-blue accent on cream white.
/// No glow effects — bloom is invisible and distracting in bright ambient light.
const AutopilotTheme lightTheme = AutopilotTheme(
id: 'light',
displayName: 'Claro (día)',
background: Color(0xFFF5F4EE),
backgroundMid: Color(0xFFFFFFFF),
backgroundDeep: Color(0xFFF1EFE8),
backgroundDeepest: Color(0xFFE8E6DD),
backgroundGradient: null, // solid background — no gradient in light mode
panelBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFFFFFF), Color(0xFFFFFFFF)],
),
panelBorder: Color(0x1F000000), // rgba(0,0,0,0.12)
textMain: Color(0xFF2C2C2A),
textMuted: Color(0xFF6B6862),
textSoft: Color(0xFF2C2C2A),
textDisabled: Color(0xFFB4B2A9),
accentLight: Color(0xFF378ADD),
accentMid: Color(0xFF185FA5),
accentDark: Color(0xFF0C447C),
accentGlowRadius: 0.0, // no glow in daytime mode
accentGlowColor: Colors.transparent,
setLight: Color(0xFFCA8A04),
setDark: Color(0xFF854F0B),
setGlow: Color(0x33CA8A04),
okColor: Color(0xFF15803D),
warnColor: Color(0xFFCA8A04),
northColor: Color(0xFFB91C1C),
disengageBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFDC2626), Color(0xFF991B1B)],
),
disengageText: Color(0xFFFFFFFF),
disengageBorder: Color(0xFFEF4444),
disengageGlow: Color(0x4DDC2626),
actionButtonBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFFFFFF), Color(0xFFF1EFE8)],
),
actionButtonBorder: Color(0x33000000), // 0.2 alpha — more visible than 0.12
actionButtonText: Color(0xFF2C2C2A),
actionButtonGlow: Colors.transparent,
);
+66
View File
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import '../autopilot_theme.dart';
/// **ochre** — Ocre
///
/// Classic cabin / aged leather aesthetic. Warm amber-gold accent on deep
/// brown. DISENGAGE returns to red (which contrasts well against ochre).
/// okColor is lime-green to stand out from the dominant gold palette.
const AutopilotTheme ochreTheme = AutopilotTheme(
id: 'ochre',
displayName: 'Ocre',
background: Color(0xFF2A1A08),
backgroundMid: Color(0xFF2A1A08),
backgroundDeep: Color(0xFF1A1004),
backgroundDeepest: Color(0xFF080502),
backgroundGradient: RadialGradient(
colors: [Color(0xFF2A1A08), Color(0xFF0F0904)],
stops: [0.0, 0.7],
),
panelBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xEB32200C), Color(0xEB181008)], // rgba(50,32,12,0.92) → rgba(24,16,8,0.92)
),
panelBorder: Color(0x52D97706), // rgba(217,119,6,0.32)
textMain: Color(0xFFFEF3C7),
textMuted: Color(0xFFA8855A),
textSoft: Color(0xFFFCD34D),
textDisabled: Color(0xFF6B5435),
accentLight: Color(0xFFFBBF24),
accentMid: Color(0xFFD97706),
accentDark: Color(0xFF92400E),
accentGlowRadius: 18.0,
accentGlowColor: Color(0xB3D97706),
setLight: Color(0xFFFDE68A),
setDark: Color(0xFFB45309),
setGlow: Color(0x66FDE68A),
okColor: Color(0xFF84CC16), // lime-green — contrasts with gold
warnColor: Color(0xFFFDE68A),
northColor: Color(0xFFFDE047),
// DISENGAGE: red — contrasts well on ochre background
disengageBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFDC2626), Color(0xFF7F1D1D)],
),
disengageText: Color(0xFFFFFFFF),
disengageBorder: Color(0xFFF87171),
disengageGlow: Color(0x99DC2626),
actionButtonBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF7A4E1A), Color(0xFF3A240C)],
),
actionButtonBorder: Color(0xFFFBBF24),
actionButtonText: Color(0xFFFEF3C7),
actionButtonGlow: Color(0x66FBBF24),
);
+67
View File
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import '../autopilot_theme.dart';
/// **wine** — Vinotinto
///
/// Premium yacht / aesthetic preference palette. Deep crimson backgrounds
/// with rose accent. IMPORTANT: DISENGAGE switches from red to amber-gold
/// because red blends into the wine cockpit and loses its emergency signal.
/// The amber still carries the "caution/critical action" cognitive association.
const AutopilotTheme wineTheme = AutopilotTheme(
id: 'wine',
displayName: 'Vinotinto',
background: Color(0xFF2A0A14),
backgroundMid: Color(0xFF2A0A14),
backgroundDeep: Color(0xFF1A0610),
backgroundDeepest: Color(0xFF080205),
backgroundGradient: RadialGradient(
colors: [Color(0xFF2A0A14), Color(0xFF0F0408)],
stops: [0.0, 0.7],
),
panelBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xE630121C), Color(0xE614080E)], // rgba(48,18,28,0.9) → rgba(20,8,14,0.9)
),
panelBorder: Color(0x4DBE1746), // rgba(190,23,70,0.3)
textMain: Color(0xFFFDE0E7),
textMuted: Color(0xFF8A5560),
textSoft: Color(0xFFF9A8B8),
textDisabled: Color(0xFF6A3848),
accentLight: Color(0xFFFB7185),
accentMid: Color(0xFFE11D48),
accentDark: Color(0xFF881337),
accentGlowRadius: 18.0,
accentGlowColor: Color(0xB3E11D48), // rgba(225,29,72,0.7)
setLight: Color(0xFFFBBF24),
setDark: Color(0xFFA16207),
setGlow: Color(0x66FBBF24),
okColor: Color(0xFFFBBF24), // gold not green — keeps warm palette
warnColor: Color(0xFFFBBF24),
northColor: Color(0xFFFDE047), // yellow not red — red is lost on wine background
// DISENGAGE: amber-gold instead of red — highest contrast on wine cockpit
disengageBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFBBF24), Color(0xFFB45309)],
),
disengageText: Color(0xFF1C0A02), // near-black — maximum contrast on amber
disengageBorder: Color(0xFFFDE047),
disengageGlow: Color(0x99FBBF24),
actionButtonBackground: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF7A1F3A), Color(0xFF3A0E1C)], // improved contrast
),
actionButtonBorder: Color(0xFFFB7185), // vivid rose border
actionButtonText: Color(0xFFFDE0E7),
actionButtonGlow: Color(0x66FB7185),
);