fix: align MotorSignal visual system with brand identity

This commit is contained in:
Developer 2026-09-05 09:22:38 -04:00
parent a14d45fac7
commit ea3650f43d
8 changed files with 134 additions and 85 deletions

View File

@ -6,41 +6,62 @@ import 'package:flutter/material.dart';
/// - Dark Mode (Cockpit OLED): deep matte blacks and vibrant saturated accents. /// - Dark Mode (Cockpit OLED): deep matte blacks and vibrant saturated accents.
/// - Light Mode (Daylight HUD): high-contrast daylight surfaces for outdoor sunlight readability. /// - Light Mode (Daylight HUD): high-contrast daylight surfaces for outdoor sunlight readability.
abstract final class AppColors { abstract final class AppColors {
// Brand & High-Contrast Accents (Unified MotorSignal Palette) // MotorSignal Canonical Brand Identity (Single hue family)
static const Color cyberCyan = Color(0xFF00B4D8); static const Color brandBlue = Color(0xFF0066FF);
static const Color technicalBlue = Color(0xFF0077A8); static const Color brandBlueStrong = Color(0xFF0047B3);
static const Color velocityBlue = Color(0xFF0096C7); static const Color brandContainerLight = Color(0xFFD9E7FF);
static const Color raceRed = Color(0xFFFF3B30); static const Color brandContainerDark = Color(0xFF142542);
static const Color hazardRed = Color(0xFFFF3B30); static const Color onBrand = Color(0xFFFFFFFF);
static const Color launchGreen = Color(0xFF34C759); static const Color onBrandContainerLight = Color(0xFF0047B3);
static const Color onBrandContainerDark = Color(0xFFD9E7FF);
// Backward-compatible token aliases mapped to canonical MotorSignal Blue
static const Color cyberCyan = brandBlue;
static const Color technicalBlue = brandBlueStrong;
static const Color velocityBlue = brandBlue;
// Semantic State Colors (Precision, not aggression)
static const Color semanticSuccess = Color(0xFF2E9B62);
static const Color semanticSuccessLight = Color(0xFF247D4F); // Contrast-adjusted for light mode readability (>= 4.5:1)
static const Color semanticWarning = Color(0xFFB97800);
static const Color semanticWarningLight = Color(0xFF8A5800); // Contrast-adjusted for light mode readability (>= 4.5:1)
static const Color semanticDanger = Color(0xFFC94444);
static const Color semanticDangerDark = Color(0xFFE05353); // Contrast-adjusted for dark mode readability (>= 4.5:1)
// Telemetry & Legacy semantic aliases
static const Color raceRed = semanticDanger;
static const Color hazardRed = semanticDanger;
static const Color launchGreen = semanticSuccess;
static const Color telemetryGreen = Color(0xFF30D158); static const Color telemetryGreen = Color(0xFF30D158);
static const Color successGreen = Color(0xFF28A745); static const Color successGreen = semanticSuccess;
static const Color warningAmber = Color(0xFFFFB300); static const Color warningAmber = semanticWarning;
static const Color darkAmber = Color(0xFFC48100); static const Color darkAmber = semanticWarningLight;
// Dark Mode Palette (Graphite & Technical Cockpit) // Dark Neutrals (Graphite & Technical Cockpit)
static const Color darkBackground = Color(0xFF0D0F13); static const Color darkBackground = Color(0xFF0B0D0F);
static const Color darkSurface = Color(0xFF13171E); static const Color darkSurface = Color(0xFF14171A); // primary surface
static const Color darkSurfaceContainer = Color(0xFF1A1F28); static const Color darkSurfaceContainer = Color(0xFF1D2226); // elevated surface
static const Color darkSurfaceContainerHigh = Color(0xFF222935); static const Color darkSurfaceContainerHigh = Color(0xFF252B30);
static const Color darkSurfaceContainerHighest = Color(0xFF2B3342); static const Color darkSurfaceContainerHighest = Color(0xFF2E353B);
static const Color darkOutline = Color(0xFF2B3342); static const Color darkOutline = Color(0xFF30363B); // divider/border
static const Color darkOutlineVariant = Color(0xFF1F242F); static const Color darkOutlineVariant = Color(0xFF22272B);
static const Color darkOnSurface = Color(0xFFF1F3F5); static const Color darkOnSurface = Color(0xFFF5F7F8); // primary text
static const Color darkOnSurfaceVariant = Color(0xFF9EABB8); static const Color darkOnSurfaceVariant = Color(0xFFAEB7BD); // secondary text
static const Color darkPrimary = Color(0xFF00B4D8); static const Color darkDisabled = Color(0xFF687278); // disabled text
static const Color darkSecondary = Color(0xFFFF3B30); static const Color darkPrimary = brandBlue;
static const Color darkSecondary = semanticDanger;
// Light Mode Palette (Daylight Technical HUD) // Light Neutrals (Daylight Technical HUD)
static const Color lightBackground = Color(0xFFF3F5F8); static const Color lightBackground = Color(0xFFF2F3F3);
static const Color lightSurface = Color(0xFFE9EDF2); static const Color lightSurface = Color(0xFFFFFFFF); // primary surface
static const Color lightSurfaceContainer = Color(0xFFFFFFFF); static const Color lightSurfaceContainer = Color(0xFFFFFFFF);
static const Color lightSurfaceContainerHigh = Color(0xFFE1E6EC); static const Color lightSurfaceContainerHigh = Color(0xFFFAFAFA); // elevated surface
static const Color lightSurfaceContainerHighest = Color(0xFFD6DCE4); static const Color lightSurfaceContainerHighest = Color(0xFFF0F2F4);
static const Color lightOutline = Color(0xFFCBD2DC); static const Color lightOutline = Color(0xFFD3D7D9); // divider/border
static const Color lightOutlineVariant = Color(0xFFDFE4EC); static const Color lightOutlineVariant = Color(0xFFE1E5E7);
static const Color lightOnSurface = Color(0xFF13171E); static const Color lightOnSurface = Color(0xFF111417); // primary text
static const Color lightOnSurfaceVariant = Color(0xFF5A6675); static const Color lightOnSurfaceVariant = Color(0xFF596269); // secondary text
static const Color lightPrimary = Color(0xFF0077A8); static const Color lightDisabled = Color(0xFF9AA2A7); // disabled text
static const Color lightSecondary = Color(0xFFD32F2F); static const Color lightPrimary = brandBlue;
static const Color lightSecondary = semanticDanger;
} }

View File

@ -4,12 +4,12 @@ import 'package:flutter/material.dart';
/// ///
/// Multi-font architecture: /// Multi-font architecture:
/// - JetBrains Mono: Dynamic numbers, telemetry readouts, timers (zero numerical jitter). /// - JetBrains Mono: Dynamic numbers, telemetry readouts, timers (zero numerical jitter).
/// - Sora: Headlines, app headers, compass directions. /// - Montserrat: Brand-forward marketing, headlines, and headers.
/// - Inter: Body, labels, metadata, specifications. /// - DM Sans: Application UI, body text, labels, and specifications.
abstract final class AppTypography { abstract final class AppTypography {
static const String fontTelemetry = 'JetBrains Mono'; static const String fontTelemetry = 'JetBrains Mono';
static const String fontHeadline = 'Sora'; static const String fontHeadline = 'Montserrat';
static const String fontBody = 'Inter'; static const String fontBody = 'DM Sans';
// Large Speedometer Readouts // Large Speedometer Readouts
static const TextStyle speedometer = TextStyle( static const TextStyle speedometer = TextStyle(

View File

@ -10,19 +10,19 @@ class AppTheme {
static ThemeData get darkTheme { static ThemeData get darkTheme {
const colorScheme = ColorScheme.dark( const colorScheme = ColorScheme.dark(
primary: AppColors.darkPrimary, primary: AppColors.darkPrimary,
onPrimary: Color(0xFF000000), onPrimary: AppColors.onBrand,
primaryContainer: Color(0xFF004D56), primaryContainer: AppColors.brandContainerDark,
onPrimaryContainer: AppColors.cyberCyan, onPrimaryContainer: AppColors.onBrandContainerDark,
secondary: AppColors.darkSecondary, secondary: AppColors.darkSecondary,
onSecondary: Color(0xFFFFFFFF), onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFF5A0000), secondaryContainer: Color(0xFF311414),
onSecondaryContainer: Color(0xFFFFB4AB), onSecondaryContainer: Color(0xFFFFDAD6),
surface: AppColors.darkSurface, surface: AppColors.darkSurface,
onSurface: AppColors.darkOnSurface, onSurface: AppColors.darkOnSurface,
onSurfaceVariant: AppColors.darkOnSurfaceVariant, onSurfaceVariant: AppColors.darkOnSurfaceVariant,
outline: AppColors.darkOutline, outline: AppColors.darkOutline,
outlineVariant: AppColors.darkOutlineVariant, outlineVariant: AppColors.darkOutlineVariant,
error: AppColors.hazardRed, error: AppColors.semanticDanger,
onError: Color(0xFFFFFFFF), onError: Color(0xFFFFFFFF),
); );
@ -207,19 +207,19 @@ class AppTheme {
static ThemeData get lightTheme { static ThemeData get lightTheme {
const colorScheme = ColorScheme.light( const colorScheme = ColorScheme.light(
primary: AppColors.lightPrimary, primary: AppColors.lightPrimary,
onPrimary: Color(0xFFFFFFFF), onPrimary: AppColors.onBrand,
primaryContainer: Color(0xFFD6E4FF), primaryContainer: AppColors.brandContainerLight,
onPrimaryContainer: Color(0xFF001B3E), onPrimaryContainer: AppColors.onBrandContainerLight,
secondary: AppColors.lightSecondary, secondary: AppColors.lightSecondary,
onSecondary: Color(0xFFFFFFFF), onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFFFFDAD6), secondaryContainer: Color(0xFFFDE8E8),
onSecondaryContainer: Color(0xFF410002), onSecondaryContainer: Color(0xFF410002),
surface: AppColors.lightSurface, surface: AppColors.lightSurface,
onSurface: AppColors.lightOnSurface, onSurface: AppColors.lightOnSurface,
onSurfaceVariant: AppColors.lightOnSurfaceVariant, onSurfaceVariant: AppColors.lightOnSurfaceVariant,
outline: AppColors.lightOutline, outline: AppColors.lightOutline,
outlineVariant: AppColors.lightOutlineVariant, outlineVariant: AppColors.lightOutlineVariant,
error: AppColors.hazardRed, error: AppColors.semanticDanger,
onError: Color(0xFFFFFFFF), onError: Color(0xFFFFFFFF),
); );

View File

@ -47,39 +47,39 @@ class MotorSignalThemeColors extends ThemeExtension<MotorSignalThemeColors> {
/// Dark Mode default telemetry tokens (Cockpit Precision). /// Dark Mode default telemetry tokens (Cockpit Precision).
static const MotorSignalThemeColors dark = MotorSignalThemeColors( static const MotorSignalThemeColors dark = MotorSignalThemeColors(
cyberCyan: AppColors.darkPrimary, cyberCyan: AppColors.darkPrimary,
velocityBlue: AppColors.velocityBlue, velocityBlue: AppColors.brandBlueStrong,
raceRed: AppColors.raceRed, raceRed: AppColors.semanticDangerDark,
launchGreen: AppColors.launchGreen, launchGreen: AppColors.semanticSuccess,
warningAmber: AppColors.warningAmber, warningAmber: AppColors.semanticWarning,
telemetryGreen: AppColors.telemetryGreen, telemetryGreen: AppColors.telemetryGreen,
leanAngleLeft: AppColors.darkPrimary, leanAngleLeft: AppColors.darkPrimary,
leanAngleRight: AppColors.velocityBlue, leanAngleRight: AppColors.brandBlueStrong,
gForcePositive: AppColors.launchGreen, gForcePositive: AppColors.semanticSuccess,
gForceNegative: AppColors.raceRed, gForceNegative: AppColors.semanticDangerDark,
speedometerDial: AppColors.darkPrimary, speedometerDial: AppColors.darkPrimary,
speedometerGlow: Color(0x1400B4D8), speedometerGlow: Color(0x140066FF),
surfaceContainer: AppColors.darkSurfaceContainer, surfaceContainer: AppColors.darkSurfaceContainer,
surfaceContainerHigh: AppColors.darkSurfaceContainerHigh, surfaceContainerHigh: AppColors.darkSurfaceContainerHigh,
surfaceContainerHighest: AppColors.darkSurfaceContainerHighest, surfaceContainerHighest: AppColors.darkSurfaceContainerHighest,
outline: AppColors.darkOutline, outline: AppColors.darkOutline,
outlineVariant: AppColors.darkOutlineVariant, outlineVariant: AppColors.darkOutlineVariant,
glowShadow: Color(0x1F00B4D8), glowShadow: Color(0x1F0066FF),
); );
/// Light Mode default telemetry tokens (Daylight HUD). /// Light Mode default telemetry tokens (Daylight HUD).
static const MotorSignalThemeColors light = MotorSignalThemeColors( static const MotorSignalThemeColors light = MotorSignalThemeColors(
cyberCyan: AppColors.lightPrimary, cyberCyan: AppColors.lightPrimary,
velocityBlue: AppColors.velocityBlue, velocityBlue: AppColors.brandBlueStrong,
raceRed: AppColors.lightSecondary, raceRed: AppColors.semanticDanger,
launchGreen: AppColors.launchGreen, launchGreen: AppColors.semanticSuccessLight,
warningAmber: AppColors.warningAmber, warningAmber: AppColors.semanticWarningLight,
telemetryGreen: AppColors.telemetryGreen, telemetryGreen: AppColors.telemetryGreen,
leanAngleLeft: AppColors.lightPrimary, leanAngleLeft: AppColors.lightPrimary,
leanAngleRight: AppColors.velocityBlue, leanAngleRight: AppColors.brandBlueStrong,
gForcePositive: AppColors.launchGreen, gForcePositive: AppColors.semanticSuccessLight,
gForceNegative: AppColors.lightSecondary, gForceNegative: AppColors.semanticDanger,
speedometerDial: AppColors.lightPrimary, speedometerDial: AppColors.lightPrimary,
speedometerGlow: Color(0x0F0077A8), speedometerGlow: Color(0x0F0066FF),
surfaceContainer: AppColors.lightSurfaceContainer, surfaceContainer: AppColors.lightSurfaceContainer,
surfaceContainerHigh: AppColors.lightSurfaceContainerHigh, surfaceContainerHigh: AppColors.lightSurfaceContainerHigh,
surfaceContainerHighest: AppColors.lightSurfaceContainerHighest, surfaceContainerHighest: AppColors.lightSurfaceContainerHighest,
@ -88,6 +88,16 @@ class MotorSignalThemeColors extends ThemeExtension<MotorSignalThemeColors> {
glowShadow: Color(0x14000000), glowShadow: Color(0x14000000),
); );
Color get brandBlue => cyberCyan;
Color get brandBlueStrong => velocityBlue;
Color get brandContainer => surfaceContainer == AppColors.darkSurfaceContainer
? AppColors.brandContainerDark
: AppColors.brandContainerLight;
Color get onBrand => AppColors.onBrand;
Color get onBrandContainer => surfaceContainer == AppColors.darkSurfaceContainer
? AppColors.onBrandContainerDark
: AppColors.onBrandContainerLight;
@override @override
MotorSignalThemeColors copyWith({ MotorSignalThemeColors copyWith({
Color? cyberCyan, Color? cyberCyan,

View File

@ -6,6 +6,7 @@ import '../../core/app_info.dart';
import '../../core/responsive/viewport_size.dart'; import '../../core/responsive/viewport_size.dart';
import '../../core/theme/theme_extensions.dart'; import '../../core/theme/theme_extensions.dart';
import '../../core/theme/theme_mode_provider.dart'; import '../../core/theme/theme_mode_provider.dart';
import '../../core/theme/app_typography.dart';
import '../../database/database.dart'; import '../../database/database.dart';
import '../../features/rides/ride_state_machine.dart'; import '../../features/rides/ride_state_machine.dart';
import '../../services/alerts/road_alert_models.dart'; import '../../services/alerts/road_alert_models.dart';
@ -206,7 +207,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
return Text( return Text(
title, title,
style: TextStyle( style: TextStyle(
fontFamily: 'Sora', fontFamily: AppTypography.fontHeadline,
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
letterSpacing: 0.2, letterSpacing: 0.2,

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../core/theme/theme_extensions.dart'; import '../../../core/theme/theme_extensions.dart';
import '../../../core/theme/app_typography.dart';
import '../../../services/billing/product_catalog.dart'; import '../../../services/billing/product_catalog.dart';
import '../../../services/entitlements/entitlement_providers.dart'; import '../../../services/entitlements/entitlement_providers.dart';
import '../../../services/entitlements/entitlement_state.dart'; import '../../../services/entitlements/entitlement_state.dart';
@ -123,7 +124,7 @@ class CurrentPlanCard extends ConsumerWidget {
Text( Text(
planTitle, planTitle,
style: TextStyle( style: TextStyle(
fontFamily: 'Sora', fontFamily: AppTypography.fontHeadline,
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: isPro ? colors.cyberCyan : Theme.of(context).colorScheme.onSurface, color: isPro ? colors.cyberCyan : Theme.of(context).colorScheme.onSurface,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../core/theme/app_typography.dart';
import '../../../services/social/rider_profile_models.dart'; import '../../../services/social/rider_profile_models.dart';
/// Renders a rider's avatar using their preset styling or custom photo URL. /// Renders a rider's avatar using their preset styling or custom photo URL.
@ -56,7 +57,7 @@ class AvatarBadge extends StatelessWidget {
color: primaryColor, color: primaryColor,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: radius * 0.9, fontSize: radius * 0.9,
fontFamily: 'Sora', fontFamily: AppTypography.fontHeadline,
), ),
), ),
), ),

View File

@ -13,27 +13,42 @@ void main() {
group('MotorSignal Design System - Tokens & Hierarchy', () { group('MotorSignal Design System - Tokens & Hierarchy', () {
test('AppColors verifies dark and light semantic palette definitions', () { test('AppColors verifies dark and light semantic palette definitions', () {
expect(AppColors.cyberCyan, const Color(0xFF00B4D8)); expect(AppColors.brandBlue, const Color(0xFF0066FF));
expect(AppColors.velocityBlue, const Color(0xFF0096C7)); expect(AppColors.brandBlueStrong, const Color(0xFF0047B3));
expect(AppColors.raceRed, const Color(0xFFFF3B30)); expect(AppColors.brandContainerLight, const Color(0xFFD9E7FF));
expect(AppColors.darkBackground, const Color(0xFF0D0F13)); expect(AppColors.brandContainerDark, const Color(0xFF142542));
expect(AppColors.darkSurface, const Color(0xFF13171E)); expect(AppColors.cyberCyan, const Color(0xFF0066FF));
expect(AppColors.lightBackground, const Color(0xFFF3F5F8)); expect(AppColors.semanticDanger, const Color(0xFFC94444));
expect(AppColors.lightSurface, const Color(0xFFE9EDF2)); expect(AppColors.semanticSuccess, const Color(0xFF2E9B62));
expect(AppColors.semanticWarning, const Color(0xFFB97800));
expect(AppColors.darkBackground, const Color(0xFF0B0D0F));
expect(AppColors.darkSurface, const Color(0xFF14171A));
expect(AppColors.darkSurfaceContainer, const Color(0xFF1D2226));
expect(AppColors.darkOutline, const Color(0xFF30363B));
expect(AppColors.darkOnSurface, const Color(0xFFF5F7F8));
expect(AppColors.darkOnSurfaceVariant, const Color(0xFFAEB7BD));
expect(AppColors.darkDisabled, const Color(0xFF687278));
expect(AppColors.lightBackground, const Color(0xFFF2F3F3));
expect(AppColors.lightSurface, const Color(0xFFFFFFFF));
expect(AppColors.lightSurfaceContainerHigh, const Color(0xFFFAFAFA));
expect(AppColors.lightOutline, const Color(0xFFD3D7D9));
expect(AppColors.lightOnSurface, const Color(0xFF111417));
expect(AppColors.lightOnSurfaceVariant, const Color(0xFF596269));
expect(AppColors.lightDisabled, const Color(0xFF9AA2A7));
}); });
test('AppTypography specifies authoritative multi-font system', () { test('AppTypography specifies authoritative multi-font system', () {
expect(AppTypography.fontTelemetry, 'JetBrains Mono'); expect(AppTypography.fontTelemetry, 'JetBrains Mono');
expect(AppTypography.fontHeadline, 'Sora'); expect(AppTypography.fontHeadline, 'Montserrat');
expect(AppTypography.fontBody, 'Inter'); expect(AppTypography.fontBody, 'DM Sans');
expect(AppTypography.speedometer.fontFamily, 'JetBrains Mono'); expect(AppTypography.speedometer.fontFamily, 'JetBrains Mono');
expect(AppTypography.speedometer.fontSize, 96); expect(AppTypography.speedometer.fontSize, 96);
expect(AppTypography.speedometer.fontWeight, FontWeight.w900); expect(AppTypography.speedometer.fontWeight, FontWeight.w900);
expect(AppTypography.headingCompass.fontFamily, 'Sora'); expect(AppTypography.headingCompass.fontFamily, 'Montserrat');
expect(AppTypography.bodyMd.fontFamily, 'Inter'); expect(AppTypography.bodyMd.fontFamily, 'DM Sans');
expect(AppTypography.button.fontFamily, 'Sora'); expect(AppTypography.button.fontFamily, 'Montserrat');
}); });
test('AppSpacing and AppRadius conform to 4dp base grid and 48dp touch targets', () { test('AppSpacing and AppRadius conform to 4dp base grid and 48dp touch targets', () {
@ -55,13 +70,13 @@ void main() {
group('MotorSignalThemeColors Extension', () { group('MotorSignalThemeColors Extension', () {
test('Dark and Light presets have correct brightness semantics', () { test('Dark and Light presets have correct brightness semantics', () {
const dark = MotorSignalThemeColors.dark; const dark = MotorSignalThemeColors.dark;
expect(dark.cyberCyan, AppColors.cyberCyan); expect(dark.cyberCyan, AppColors.brandBlue);
expect(dark.launchGreen, AppColors.launchGreen); expect(dark.launchGreen, AppColors.semanticSuccess);
expect(dark.surfaceContainer, AppColors.darkSurfaceContainer); expect(dark.surfaceContainer, AppColors.darkSurfaceContainer);
const light = MotorSignalThemeColors.light; const light = MotorSignalThemeColors.light;
expect(light.cyberCyan, AppColors.technicalBlue); expect(light.cyberCyan, AppColors.brandBlue);
expect(light.launchGreen, AppColors.launchGreen); expect(light.launchGreen, AppColors.semanticSuccessLight);
expect(light.surfaceContainer, AppColors.lightSurfaceContainer); expect(light.surfaceContainer, AppColors.lightSurfaceContainer);
}); });
@ -99,7 +114,7 @@ void main() {
final ext = theme.extension<MotorSignalThemeColors>(); final ext = theme.extension<MotorSignalThemeColors>();
expect(ext, isNotNull); expect(ext, isNotNull);
expect(ext?.cyberCyan, AppColors.technicalBlue); expect(ext?.cyberCyan, AppColors.brandBlue);
}); });
}); });