Files
AR-Autopilot/display/lib/theme/autopilot_theme.dart
T
alro65 cb138a3248 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>
2026-05-21 00:33:04 -04:00

136 lines
6.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 [];
}