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.
/// - Light Mode (Daylight HUD): high-contrast daylight surfaces for outdoor sunlight readability.
abstract final class AppColors {
// Brand & High-Contrast Accents (Unified MotorSignal Palette)
static const Color cyberCyan = Color(0xFF00B4D8);
static const Color technicalBlue = Color(0xFF0077A8);
static const Color velocityBlue = Color(0xFF0096C7);
static const Color raceRed = Color(0xFFFF3B30);
static const Color hazardRed = Color(0xFFFF3B30);
static const Color launchGreen = Color(0xFF34C759);
// MotorSignal Canonical Brand Identity (Single hue family)
static const Color brandBlue = Color(0xFF0066FF);
static const Color brandBlueStrong = Color(0xFF0047B3);
static const Color brandContainerLight = Color(0xFFD9E7FF);
static const Color brandContainerDark = Color(0xFF142542);
static const Color onBrand = Color(0xFFFFFFFF);
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 successGreen = Color(0xFF28A745);
static const Color warningAmber = Color(0xFFFFB300);
static const Color darkAmber = Color(0xFFC48100);
static const Color successGreen = semanticSuccess;
static const Color warningAmber = semanticWarning;
static const Color darkAmber = semanticWarningLight;
// Dark Mode Palette (Graphite & Technical Cockpit)
static const Color darkBackground = Color(0xFF0D0F13);
static const Color darkSurface = Color(0xFF13171E);
static const Color darkSurfaceContainer = Color(0xFF1A1F28);
static const Color darkSurfaceContainerHigh = Color(0xFF222935);
static const Color darkSurfaceContainerHighest = Color(0xFF2B3342);
static const Color darkOutline = Color(0xFF2B3342);
static const Color darkOutlineVariant = Color(0xFF1F242F);
static const Color darkOnSurface = Color(0xFFF1F3F5);
static const Color darkOnSurfaceVariant = Color(0xFF9EABB8);
static const Color darkPrimary = Color(0xFF00B4D8);
static const Color darkSecondary = Color(0xFFFF3B30);
// Dark Neutrals (Graphite & Technical Cockpit)
static const Color darkBackground = Color(0xFF0B0D0F);
static const Color darkSurface = Color(0xFF14171A); // primary surface
static const Color darkSurfaceContainer = Color(0xFF1D2226); // elevated surface
static const Color darkSurfaceContainerHigh = Color(0xFF252B30);
static const Color darkSurfaceContainerHighest = Color(0xFF2E353B);
static const Color darkOutline = Color(0xFF30363B); // divider/border
static const Color darkOutlineVariant = Color(0xFF22272B);
static const Color darkOnSurface = Color(0xFFF5F7F8); // primary text
static const Color darkOnSurfaceVariant = Color(0xFFAEB7BD); // secondary text
static const Color darkDisabled = Color(0xFF687278); // disabled text
static const Color darkPrimary = brandBlue;
static const Color darkSecondary = semanticDanger;
// Light Mode Palette (Daylight Technical HUD)
static const Color lightBackground = Color(0xFFF3F5F8);
static const Color lightSurface = Color(0xFFE9EDF2);
// Light Neutrals (Daylight Technical HUD)
static const Color lightBackground = Color(0xFFF2F3F3);
static const Color lightSurface = Color(0xFFFFFFFF); // primary surface
static const Color lightSurfaceContainer = Color(0xFFFFFFFF);
static const Color lightSurfaceContainerHigh = Color(0xFFE1E6EC);
static const Color lightSurfaceContainerHighest = Color(0xFFD6DCE4);
static const Color lightOutline = Color(0xFFCBD2DC);
static const Color lightOutlineVariant = Color(0xFFDFE4EC);
static const Color lightOnSurface = Color(0xFF13171E);
static const Color lightOnSurfaceVariant = Color(0xFF5A6675);
static const Color lightPrimary = Color(0xFF0077A8);
static const Color lightSecondary = Color(0xFFD32F2F);
static const Color lightSurfaceContainerHigh = Color(0xFFFAFAFA); // elevated surface
static const Color lightSurfaceContainerHighest = Color(0xFFF0F2F4);
static const Color lightOutline = Color(0xFFD3D7D9); // divider/border
static const Color lightOutlineVariant = Color(0xFFE1E5E7);
static const Color lightOnSurface = Color(0xFF111417); // primary text
static const Color lightOnSurfaceVariant = Color(0xFF596269); // secondary text
static const Color lightDisabled = Color(0xFF9AA2A7); // disabled text
static const Color lightPrimary = brandBlue;
static const Color lightSecondary = semanticDanger;
}

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import '../../core/app_info.dart';
import '../../core/responsive/viewport_size.dart';
import '../../core/theme/theme_extensions.dart';
import '../../core/theme/theme_mode_provider.dart';
import '../../core/theme/app_typography.dart';
import '../../database/database.dart';
import '../../features/rides/ride_state_machine.dart';
import '../../services/alerts/road_alert_models.dart';
@ -206,7 +207,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
return Text(
title,
style: TextStyle(
fontFamily: 'Sora',
fontFamily: AppTypography.fontHeadline,
fontSize: 13,
fontWeight: FontWeight.w700,
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:url_launcher/url_launcher.dart';
import '../../../core/theme/theme_extensions.dart';
import '../../../core/theme/app_typography.dart';
import '../../../services/billing/product_catalog.dart';
import '../../../services/entitlements/entitlement_providers.dart';
import '../../../services/entitlements/entitlement_state.dart';
@ -123,7 +124,7 @@ class CurrentPlanCard extends ConsumerWidget {
Text(
planTitle,
style: TextStyle(
fontFamily: 'Sora',
fontFamily: AppTypography.fontHeadline,
fontSize: 13,
fontWeight: FontWeight.w700,
color: isPro ? colors.cyberCyan : Theme.of(context).colorScheme.onSurface,

View File

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

View File

@ -13,27 +13,42 @@ void main() {
group('MotorSignal Design System - Tokens & Hierarchy', () {
test('AppColors verifies dark and light semantic palette definitions', () {
expect(AppColors.cyberCyan, const Color(0xFF00B4D8));
expect(AppColors.velocityBlue, const Color(0xFF0096C7));
expect(AppColors.raceRed, const Color(0xFFFF3B30));
expect(AppColors.darkBackground, const Color(0xFF0D0F13));
expect(AppColors.darkSurface, const Color(0xFF13171E));
expect(AppColors.lightBackground, const Color(0xFFF3F5F8));
expect(AppColors.lightSurface, const Color(0xFFE9EDF2));
expect(AppColors.brandBlue, const Color(0xFF0066FF));
expect(AppColors.brandBlueStrong, const Color(0xFF0047B3));
expect(AppColors.brandContainerLight, const Color(0xFFD9E7FF));
expect(AppColors.brandContainerDark, const Color(0xFF142542));
expect(AppColors.cyberCyan, const Color(0xFF0066FF));
expect(AppColors.semanticDanger, const Color(0xFFC94444));
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', () {
expect(AppTypography.fontTelemetry, 'JetBrains Mono');
expect(AppTypography.fontHeadline, 'Sora');
expect(AppTypography.fontBody, 'Inter');
expect(AppTypography.fontHeadline, 'Montserrat');
expect(AppTypography.fontBody, 'DM Sans');
expect(AppTypography.speedometer.fontFamily, 'JetBrains Mono');
expect(AppTypography.speedometer.fontSize, 96);
expect(AppTypography.speedometer.fontWeight, FontWeight.w900);
expect(AppTypography.headingCompass.fontFamily, 'Sora');
expect(AppTypography.bodyMd.fontFamily, 'Inter');
expect(AppTypography.button.fontFamily, 'Sora');
expect(AppTypography.headingCompass.fontFamily, 'Montserrat');
expect(AppTypography.bodyMd.fontFamily, 'DM Sans');
expect(AppTypography.button.fontFamily, 'Montserrat');
});
test('AppSpacing and AppRadius conform to 4dp base grid and 48dp touch targets', () {
@ -55,13 +70,13 @@ void main() {
group('MotorSignalThemeColors Extension', () {
test('Dark and Light presets have correct brightness semantics', () {
const dark = MotorSignalThemeColors.dark;
expect(dark.cyberCyan, AppColors.cyberCyan);
expect(dark.launchGreen, AppColors.launchGreen);
expect(dark.cyberCyan, AppColors.brandBlue);
expect(dark.launchGreen, AppColors.semanticSuccess);
expect(dark.surfaceContainer, AppColors.darkSurfaceContainer);
const light = MotorSignalThemeColors.light;
expect(light.cyberCyan, AppColors.technicalBlue);
expect(light.launchGreen, AppColors.launchGreen);
expect(light.cyberCyan, AppColors.brandBlue);
expect(light.launchGreen, AppColors.semanticSuccessLight);
expect(light.surfaceContainer, AppColors.lightSurfaceContainer);
});
@ -99,7 +114,7 @@ void main() {
final ext = theme.extension<MotorSignalThemeColors>();
expect(ext, isNotNull);
expect(ext?.cyberCyan, AppColors.technicalBlue);
expect(ext?.cyberCyan, AppColors.brandBlue);
});
});