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
+135
View File
@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
/// Visual theme token set for the AR-Autopilot display app.
///
/// Every widget reads design tokens from [AutopilotTheme] via
/// [AutopilotThemeProvider]. No widget hard-codes colors.
///
/// ## Design invariants (apply to ALL themes)
/// 1. **DISENGAGE** is always the highest-contrast element on screen.
/// If the palette makes red ambiguous, it changes to amber (wine theme).
/// 2. **Action buttons** (DODGE, ±1°, ±10°) must be legible at rest —
/// operators work with gloves in rain; hover/tap is not reliable.
/// 3. **Compass heading text** uses [textMain] with [accentGlowColor] glow.
/// 4. **Set-point** always uses [setLight] (amber/gold) — cognitively stable
/// across palettes; represents "operator intent".
/// 5. **North mark** is always a warm color ([northColor]) — nautical convention.
/// 6. **Touch targets**: 48×48 px nominal, 60×60 px for critical controls.
/// 7. **No glow in light mode**: [accentGlowRadius] == 0.0 for `light` theme.
class AutopilotTheme {
const AutopilotTheme({
required this.id,
required this.displayName,
// Backgrounds
required this.background,
required this.backgroundMid,
required this.backgroundDeep,
required this.backgroundDeepest,
this.backgroundGradient,
// Panels
required this.panelBackground,
required this.panelBorder,
// Text
required this.textMain,
required this.textMuted,
required this.textSoft,
required this.textDisabled,
// Accent — heading arc, active mode indicator, rudder indicator
required this.accentLight,
required this.accentMid,
required this.accentDark,
required this.accentGlowRadius,
required this.accentGlowColor,
// Set-point / desired heading
required this.setLight,
required this.setDark,
required this.setGlow,
// Semantic states
required this.okColor,
required this.warnColor,
required this.northColor,
// DISENGAGE — always high contrast, always unambiguous
required this.disengageBackground,
required this.disengageText,
required this.disengageBorder,
required this.disengageGlow,
// Action buttons (DODGE, ±1°, ±10°)
required this.actionButtonBackground,
required this.actionButtonBorder,
required this.actionButtonText,
required this.actionButtonGlow,
});
final String id;
final String displayName;
// ── Backgrounds ────────────────────────────────────────────────────────────
/// Dominant background color. Used as solid fill (light) or radial
/// gradient center (dark themes). See [backgroundDecoration].
final Color background;
final Color backgroundMid;
final Color backgroundDeep;
final Color backgroundDeepest;
/// Radial gradient for dark themes; `null` for the light theme
/// (which uses a solid [background]).
final Gradient? backgroundGradient;
// ── Panels ──────────────────────────────────────────────────────────────────
final Gradient panelBackground;
final Color panelBorder;
// ── Text ────────────────────────────────────────────────────────────────────
final Color textMain;
final Color textMuted;
final Color textSoft;
final Color textDisabled;
// ── Accent ──────────────────────────────────────────────────────────────────
final Color accentLight;
final Color accentMid;
final Color accentDark;
/// Blur radius for accent glow effect. Zero in light mode (no glow).
final double accentGlowRadius;
final Color accentGlowColor;
// ── Set-point ───────────────────────────────────────────────────────────────
final Color setLight;
final Color setDark;
final Color setGlow;
// ── Semantic states ─────────────────────────────────────────────────────────
/// GPS OK, NMEA sentence valid, SOG within range.
final Color okColor;
/// Heading error, cross-track error warning.
final Color warnColor;
/// North mark on the compass rose. Always a warm color (never blue).
final Color northColor;
// ── DISENGAGE ───────────────────────────────────────────────────────────────
final Gradient disengageBackground;
final Color disengageText;
final Color disengageBorder;
final Color disengageGlow;
// ── Action buttons (DODGE, ±1°, ±10°) ──────────────────────────────────────
final Gradient actionButtonBackground;
final Color actionButtonBorder;
final Color actionButtonText;
final Color actionButtonGlow;
// ── Computed helpers ────────────────────────────────────────────────────────
/// [BoxDecoration] for the full-screen background container.
BoxDecoration get backgroundDecoration => backgroundGradient != null
? BoxDecoration(gradient: backgroundGradient)
: BoxDecoration(color: background);
/// [BoxShadow] list for accented glow (empty in light mode).
List<BoxShadow> glowShadow(Color color, double radius) => radius > 0
? [BoxShadow(color: color, blurRadius: radius, spreadRadius: 1)]
: const [];
}