cb138a3248
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>
136 lines
6.0 KiB
Dart
136 lines
6.0 KiB
Dart
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 [];
|
||
}
|