feat: refine MotorSignal visual system and navigation

This commit is contained in:
Developer 2026-09-05 09:12:16 -04:00
parent a362e7311f
commit a14d45fac7
26 changed files with 1199 additions and 393 deletions

View File

@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../features/dashboard/dashboard_screen.dart'; import '../features/dashboard/dashboard_screen.dart';
import '../features/map/map_screen.dart'; import '../features/map/map_screen.dart';
import '../features/history/history_screen.dart';
import '../features/records/records_screen.dart';
import '../features/settings/settings_screen.dart'; import '../features/settings/settings_screen.dart';
import '../features/settings/diagnostics_screen.dart'; import '../features/settings/diagnostics_screen.dart';
import '../features/garage/garage_screen.dart'; import '../features/garage/garage_screen.dart';
@ -17,6 +15,8 @@ import '../features/field_test/field_test_pre_ride_check_screen.dart';
import '../features/settings/oauth_callback_screen.dart'; import '../features/settings/oauth_callback_screen.dart';
import '../features/settings/account_screen.dart'; import '../features/settings/account_screen.dart';
import '../features/entitlements/pro_screen.dart'; import '../features/entitlements/pro_screen.dart';
import '../features/activity/activity_screen.dart';
import '../features/social/social_hub_screen.dart';
import '../features/social/rider_profile_screen.dart'; import '../features/social/rider_profile_screen.dart';
import '../features/social/friends_screen.dart'; import '../features/social/friends_screen.dart';
import '../features/group_rides/group_rides_hub_screen.dart'; import '../features/group_rides/group_rides_hub_screen.dart';
@ -88,16 +88,14 @@ final appRouter = GoRouter(
StatefulShellBranch( StatefulShellBranch(
routes: [ routes: [
GoRoute( GoRoute(
path: '/history', path: '/activity',
builder: (context, state) => const HistoryScreen(), builder: (context, state) {
), final tabParam = state.uri.queryParameters['tab'];
], final tab = tabParam == 'records'
), ? ActivityTab.records
StatefulShellBranch( : ActivityTab.rides;
routes: [ return ActivityScreen(initialTab: tab);
GoRoute( },
path: '/records',
builder: (context, state) => const RecordsScreen(),
), ),
], ],
), ),
@ -122,6 +120,24 @@ final appRouter = GoRouter(
), ),
], ],
), ),
StatefulShellBranch(
routes: [
GoRoute(
path: '/social',
builder: (context, state) => const SocialHubScreen(),
routes: [
GoRoute(
path: 'profile',
builder: (context, state) => const RiderProfileScreen(),
),
GoRoute(
path: 'friends',
builder: (context, state) => const FriendsScreen(),
),
],
),
],
),
StatefulShellBranch( StatefulShellBranch(
routes: [ routes: [
GoRoute( GoRoute(
@ -172,6 +188,16 @@ final appRouter = GoRouter(
), ),
], ],
), ),
GoRoute(
path: '/history',
builder: (context, state) =>
const ActivityScreen(initialTab: ActivityTab.rides),
),
GoRoute(
path: '/records',
builder: (context, state) =>
const ActivityScreen(initialTab: ActivityTab.records),
),
GoRoute( GoRoute(
path: '/group-rides', path: '/group-rides',
builder: (context, state) => const GroupRidesHubScreen(), builder: (context, state) => const GroupRidesHubScreen(),

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Authoritative semantic color tokens for MotorSignal. /// Authoritative semantic color tokens for MotorSignal.
/// ///
@ -6,41 +6,41 @@
/// - 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 // Brand & High-Contrast Accents (Unified MotorSignal Palette)
static const Color cyberCyan = Color(0xFF00E5FF); static const Color cyberCyan = Color(0xFF00B4D8);
static const Color technicalBlue = Color(0xFF124580); static const Color technicalBlue = Color(0xFF0077A8);
static const Color velocityBlue = Color(0xFF2E7DFF); static const Color velocityBlue = Color(0xFF0096C7);
static const Color raceRed = Color(0xFFFF2A2A); static const Color raceRed = Color(0xFFFF3B30);
static const Color hazardRed = Color(0xFFFF3B30); static const Color hazardRed = Color(0xFFFF3B30);
static const Color launchGreen = Color(0xFF34C759); static const Color launchGreen = Color(0xFF34C759);
static const Color telemetryGreen = Color(0xFF69F0AE); static const Color telemetryGreen = Color(0xFF30D158);
static const Color successGreen = Color(0xFF008547); static const Color successGreen = Color(0xFF28A745);
static const Color warningAmber = Color(0xFFFFD740); static const Color warningAmber = Color(0xFFFFB300);
static const Color darkAmber = Color(0xFFC48100); static const Color darkAmber = Color(0xFFC48100);
// Dark Mode Palette (Cockpit OLED) // Dark Mode Palette (Graphite & Technical Cockpit)
static const Color darkBackground = Color(0xFF0A0A0A); static const Color darkBackground = Color(0xFF0D0F13);
static const Color darkSurface = Color(0xFF0D0D0D); static const Color darkSurface = Color(0xFF13171E);
static const Color darkSurfaceContainer = Color(0xFF141414); static const Color darkSurfaceContainer = Color(0xFF1A1F28);
static const Color darkSurfaceContainerHigh = Color(0xFF1E1E1E); static const Color darkSurfaceContainerHigh = Color(0xFF222935);
static const Color darkSurfaceContainerHighest = Color(0xFF282828); static const Color darkSurfaceContainerHighest = Color(0xFF2B3342);
static const Color darkOutline = Color(0xFF2A2A2A); static const Color darkOutline = Color(0xFF2B3342);
static const Color darkOutlineVariant = Color(0xFF1F1F1F); static const Color darkOutlineVariant = Color(0xFF1F242F);
static const Color darkOnSurface = Color(0xFFFFFFFF); static const Color darkOnSurface = Color(0xFFF1F3F5);
static const Color darkOnSurfaceVariant = Color(0xFFB3B3B3); static const Color darkOnSurfaceVariant = Color(0xFF9EABB8);
static const Color darkPrimary = Color(0xFF00E5FF); static const Color darkPrimary = Color(0xFF00B4D8);
static const Color darkSecondary = Color(0xFFFF2A2A); static const Color darkSecondary = Color(0xFFFF3B30);
// Light Mode Palette (Daylight HUD) // Light Mode Palette (Daylight Technical HUD)
static const Color lightBackground = Color(0xFFF0F0F0); static const Color lightBackground = Color(0xFFF3F5F8);
static const Color lightSurface = Color(0xFFF5F5F5); static const Color lightSurface = Color(0xFFE9EDF2);
static const Color lightSurfaceContainer = Color(0xFFFFFFFF); static const Color lightSurfaceContainer = Color(0xFFFFFFFF);
static const Color lightSurfaceContainerHigh = Color(0xFFEAEAEA); static const Color lightSurfaceContainerHigh = Color(0xFFE1E6EC);
static const Color lightSurfaceContainerHighest = Color(0xFFDFDFDF); static const Color lightSurfaceContainerHighest = Color(0xFFD6DCE4);
static const Color lightOutline = Color(0xFFD1D1D1); static const Color lightOutline = Color(0xFFCBD2DC);
static const Color lightOutlineVariant = Color(0xFFE0E0E0); static const Color lightOutlineVariant = Color(0xFFDFE4EC);
static const Color lightOnSurface = Color(0xFF1A1C1C); static const Color lightOnSurface = Color(0xFF13171E);
static const Color lightOnSurfaceVariant = Color(0xFF555555); static const Color lightOnSurfaceVariant = Color(0xFF5A6675);
static const Color lightPrimary = Color(0xFF124580); static const Color lightPrimary = Color(0xFF0077A8);
static const Color lightSecondary = Color(0xFFD32F2F); static const Color lightSecondary = Color(0xFFD32F2F);
} }

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Authoritative typography tokens for MotorSignal. /// Authoritative typography tokens for MotorSignal.
/// ///
@ -38,28 +38,28 @@ abstract final class AppTypography {
// Headlines // Headlines
static const TextStyle headingCompass = TextStyle( static const TextStyle headingCompass = TextStyle(
fontFamily: fontHeadline, fontFamily: fontHeadline,
fontSize: 28, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
letterSpacing: 0.05 * 28, letterSpacing: 0.5,
); );
static const TextStyle headlineLg = TextStyle( static const TextStyle headlineLg = TextStyle(
fontFamily: fontHeadline, fontFamily: fontHeadline,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w600,
letterSpacing: 0.5, letterSpacing: -0.2,
); );
static const TextStyle headlineMd = TextStyle( static const TextStyle headlineMd = TextStyle(
fontFamily: fontHeadline, fontFamily: fontHeadline,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
); );
static const TextStyle headlineSm = TextStyle( static const TextStyle headlineSm = TextStyle(
fontFamily: fontHeadline, fontFamily: fontHeadline,
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
); );
// Telemetry Labels & Readouts // Telemetry Labels & Readouts
@ -71,16 +71,16 @@ abstract final class AppTypography {
static const TextStyle telemetryLabel = TextStyle( static const TextStyle telemetryLabel = TextStyle(
fontFamily: fontBody, fontFamily: fontBody,
fontSize: 11, fontSize: 10,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w600,
letterSpacing: 1.2, letterSpacing: 0.8,
); );
static const TextStyle badgeLabel = TextStyle( static const TextStyle badgeLabel = TextStyle(
fontFamily: fontTelemetry, fontFamily: fontTelemetry,
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w600,
letterSpacing: 1.0, letterSpacing: 0.5,
); );
// Body text // Body text
@ -108,7 +108,7 @@ abstract final class AppTypography {
static const TextStyle button = TextStyle( static const TextStyle button = TextStyle(
fontFamily: fontHeadline, fontFamily: fontHeadline,
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
letterSpacing: 1.0, letterSpacing: 0.4,
); );
} }

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'app_colors.dart'; import 'app_colors.dart';
import 'app_spacing.dart'; import 'app_spacing.dart';
import 'app_typography.dart'; import 'app_typography.dart';
@ -49,15 +49,15 @@ class AppTheme {
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: AppRadius.lgBorderRadius, borderRadius: AppRadius.lgBorderRadius,
side: const BorderSide(color: AppColors.darkOutline, width: 1), side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8),
), ),
), ),
dialogTheme: DialogThemeData( dialogTheme: DialogThemeData(
backgroundColor: AppColors.darkSurfaceContainerHigh, backgroundColor: AppColors.darkSurfaceContainerHigh,
elevation: 8, elevation: 6,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: AppRadius.lgBorderRadius, borderRadius: AppRadius.lgBorderRadius,
side: const BorderSide(color: AppColors.darkOutline, width: 1), side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8),
), ),
titleTextStyle: AppTypography.headlineMd.copyWith(color: AppColors.darkOnSurface), titleTextStyle: AppTypography.headlineMd.copyWith(color: AppColors.darkOnSurface),
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurfaceVariant), contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurfaceVariant),
@ -65,7 +65,7 @@ class AppTheme {
bottomSheetTheme: BottomSheetThemeData( bottomSheetTheme: BottomSheetThemeData(
backgroundColor: AppColors.darkSurfaceContainerHigh, backgroundColor: AppColors.darkSurfaceContainerHigh,
modalBackgroundColor: AppColors.darkSurfaceContainerHigh, modalBackgroundColor: AppColors.darkSurfaceContainerHigh,
elevation: 16, elevation: 12,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)),
), ),
@ -79,19 +79,19 @@ class AppTheme {
), ),
side: WidgetStateProperty.resolveWith<BorderSide>((states) { side: WidgetStateProperty.resolveWith<BorderSide>((states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return const BorderSide(color: AppColors.cyberCyan, width: 1.5); return const BorderSide(color: AppColors.darkPrimary, width: 1.2);
} }
return const BorderSide(color: AppColors.darkOutline, width: 1); return const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8);
}), }),
backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) { backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return AppColors.cyberCyan.withValues(alpha: 0.15); return AppColors.darkPrimary.withValues(alpha: 0.12);
} }
return AppColors.darkSurfaceContainer; return AppColors.darkSurfaceContainer;
}), }),
foregroundColor: WidgetStateProperty.resolveWith<Color?>((states) { foregroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return AppColors.cyberCyan; return AppColors.darkPrimary;
} }
return AppColors.darkOnSurfaceVariant; return AppColors.darkOnSurfaceVariant;
}), }),
@ -101,7 +101,7 @@ class AppTheme {
filledButtonTheme: FilledButtonThemeData( filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: AppColors.darkPrimary, backgroundColor: AppColors.darkPrimary,
foregroundColor: const Color(0xFF000000), foregroundColor: const Color(0xFF0D0F13),
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius), shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
textStyle: AppTypography.button, textStyle: AppTypography.button,
@ -114,12 +114,12 @@ class AppTheme {
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius), shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
textStyle: AppTypography.button, textStyle: AppTypography.button,
elevation: 2, elevation: 1,
), ),
), ),
outlinedButtonTheme: OutlinedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppColors.cyberCyan, foregroundColor: AppColors.darkPrimary,
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
side: const BorderSide(color: AppColors.darkOutline, width: 1), side: const BorderSide(color: AppColors.darkOutline, width: 1),
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius), shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
@ -128,7 +128,7 @@ class AppTheme {
), ),
textButtonTheme: TextButtonThemeData( textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: AppColors.cyberCyan, foregroundColor: AppColors.darkPrimary,
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
textStyle: AppTypography.button, textStyle: AppTypography.button,
), ),
@ -175,20 +175,20 @@ class AppTheme {
), ),
navigationBarTheme: NavigationBarThemeData( navigationBarTheme: NavigationBarThemeData(
backgroundColor: AppColors.darkSurface, backgroundColor: AppColors.darkSurface,
indicatorColor: AppColors.cyberCyan.withValues(alpha: 0.15), indicatorColor: AppColors.darkPrimary.withValues(alpha: 0.14),
elevation: 0, elevation: 0,
labelTextStyle: WidgetStateProperty.resolveWith((states) { labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return AppTypography.telemetryLabel.copyWith(color: AppColors.cyberCyan); return AppTypography.telemetryLabel.copyWith(color: AppColors.darkPrimary);
} }
return AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant); return AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant);
}), }),
), ),
navigationRailTheme: NavigationRailThemeData( navigationRailTheme: NavigationRailThemeData(
backgroundColor: AppColors.darkSurface, backgroundColor: AppColors.darkSurface,
indicatorColor: AppColors.cyberCyan.withValues(alpha: 0.15), indicatorColor: AppColors.darkPrimary.withValues(alpha: 0.14),
elevation: 0, elevation: 0,
selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.cyberCyan), selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkPrimary),
unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant), unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant),
), ),
snackBarTheme: SnackBarThemeData( snackBarTheme: SnackBarThemeData(
@ -196,7 +196,7 @@ class AppTheme {
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurface), contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurface),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: AppRadius.mdBorderRadius, borderRadius: AppRadius.mdBorderRadius,
side: const BorderSide(color: AppColors.darkOutline, width: 1), side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8),
), ),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
@ -246,15 +246,15 @@ class AppTheme {
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: AppRadius.lgBorderRadius, borderRadius: AppRadius.lgBorderRadius,
side: const BorderSide(color: AppColors.lightOutline, width: 1), side: const BorderSide(color: AppColors.lightOutlineVariant, width: 0.8),
), ),
), ),
dialogTheme: DialogThemeData( dialogTheme: DialogThemeData(
backgroundColor: AppColors.lightSurfaceContainer, backgroundColor: AppColors.lightSurfaceContainer,
elevation: 8, elevation: 6,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: AppRadius.lgBorderRadius, borderRadius: AppRadius.lgBorderRadius,
side: const BorderSide(color: AppColors.lightOutline, width: 1), side: const BorderSide(color: AppColors.lightOutlineVariant, width: 0.8),
), ),
titleTextStyle: AppTypography.headlineMd.copyWith(color: AppColors.lightOnSurface), titleTextStyle: AppTypography.headlineMd.copyWith(color: AppColors.lightOnSurface),
contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.lightOnSurfaceVariant), contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.lightOnSurfaceVariant),
@ -262,7 +262,7 @@ class AppTheme {
bottomSheetTheme: BottomSheetThemeData( bottomSheetTheme: BottomSheetThemeData(
backgroundColor: AppColors.lightSurfaceContainer, backgroundColor: AppColors.lightSurfaceContainer,
modalBackgroundColor: AppColors.lightSurfaceContainer, modalBackgroundColor: AppColors.lightSurfaceContainer,
elevation: 16, elevation: 12,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)),
), ),
@ -276,13 +276,13 @@ class AppTheme {
), ),
side: WidgetStateProperty.resolveWith<BorderSide>((states) { side: WidgetStateProperty.resolveWith<BorderSide>((states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return const BorderSide(color: AppColors.lightPrimary, width: 1.5); return const BorderSide(color: AppColors.lightPrimary, width: 1.2);
} }
return const BorderSide(color: AppColors.lightOutline, width: 1); return const BorderSide(color: AppColors.lightOutlineVariant, width: 0.8);
}), }),
backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) { backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return AppColors.lightPrimary.withValues(alpha: 0.12); return AppColors.lightPrimary.withValues(alpha: 0.10);
} }
return AppColors.lightSurfaceContainer; return AppColors.lightSurfaceContainer;
}), }),
@ -311,7 +311,7 @@ class AppTheme {
minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget),
shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius), shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius),
textStyle: AppTypography.button, textStyle: AppTypography.button,
elevation: 2, elevation: 1,
), ),
), ),
outlinedButtonTheme: OutlinedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'app_colors.dart'; import 'app_colors.dart';
/// Semantic ThemeExtension for MotorSignal telemetry, indicators, and UI surfaces. /// Semantic ThemeExtension for MotorSignal telemetry, indicators, and UI surfaces.
@ -44,48 +44,48 @@ class MotorSignalThemeColors extends ThemeExtension<MotorSignalThemeColors> {
required this.glowShadow, required this.glowShadow,
}); });
/// Dark Mode default telemetry tokens (Cockpit OLED). /// Dark Mode default telemetry tokens (Cockpit Precision).
static const MotorSignalThemeColors dark = MotorSignalThemeColors( static const MotorSignalThemeColors dark = MotorSignalThemeColors(
cyberCyan: AppColors.cyberCyan, cyberCyan: AppColors.darkPrimary,
velocityBlue: AppColors.velocityBlue, velocityBlue: AppColors.velocityBlue,
raceRed: AppColors.raceRed, raceRed: AppColors.raceRed,
launchGreen: AppColors.launchGreen, launchGreen: AppColors.launchGreen,
warningAmber: AppColors.warningAmber, warningAmber: AppColors.warningAmber,
telemetryGreen: AppColors.telemetryGreen, telemetryGreen: AppColors.telemetryGreen,
leanAngleLeft: AppColors.cyberCyan, leanAngleLeft: AppColors.darkPrimary,
leanAngleRight: AppColors.velocityBlue, leanAngleRight: AppColors.velocityBlue,
gForcePositive: AppColors.launchGreen, gForcePositive: AppColors.launchGreen,
gForceNegative: AppColors.raceRed, gForceNegative: AppColors.raceRed,
speedometerDial: AppColors.cyberCyan, speedometerDial: AppColors.darkPrimary,
speedometerGlow: Color(0x3300E5FF), speedometerGlow: Color(0x1400B4D8),
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(0x6600E5FF), glowShadow: Color(0x1F00B4D8),
); );
/// 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.technicalBlue, cyberCyan: AppColors.lightPrimary,
velocityBlue: AppColors.velocityBlue, velocityBlue: AppColors.velocityBlue,
raceRed: AppColors.lightSecondary, raceRed: AppColors.lightSecondary,
launchGreen: AppColors.successGreen, launchGreen: AppColors.launchGreen,
warningAmber: AppColors.darkAmber, warningAmber: AppColors.warningAmber,
telemetryGreen: AppColors.successGreen, telemetryGreen: AppColors.telemetryGreen,
leanAngleLeft: AppColors.technicalBlue, leanAngleLeft: AppColors.lightPrimary,
leanAngleRight: AppColors.velocityBlue, leanAngleRight: AppColors.velocityBlue,
gForcePositive: AppColors.successGreen, gForcePositive: AppColors.launchGreen,
gForceNegative: AppColors.lightSecondary, gForceNegative: AppColors.lightSecondary,
speedometerDial: AppColors.technicalBlue, speedometerDial: AppColors.lightPrimary,
speedometerGlow: Color(0x1F124580), speedometerGlow: Color(0x0F0077A8),
surfaceContainer: AppColors.lightSurfaceContainer, surfaceContainer: AppColors.lightSurfaceContainer,
surfaceContainerHigh: AppColors.lightSurfaceContainerHigh, surfaceContainerHigh: AppColors.lightSurfaceContainerHigh,
surfaceContainerHighest: AppColors.lightSurfaceContainerHighest, surfaceContainerHighest: AppColors.lightSurfaceContainerHighest,
outline: AppColors.lightOutline, outline: AppColors.lightOutline,
outlineVariant: AppColors.lightOutlineVariant, outlineVariant: AppColors.lightOutlineVariant,
glowShadow: Color(0x22000000), glowShadow: Color(0x14000000),
); );
@override @override

View File

@ -39,24 +39,26 @@ const List<AppNavDestination> kAppNavDestinations = [
railKey: ValueKey('rail_map_icon'), railKey: ValueKey('rail_map_icon'),
), ),
AppNavDestination( AppNavDestination(
icon: Icons.history, icon: Icons.show_chart_rounded,
label: 'History', selectedIcon: Icons.show_chart,
navKey: ValueKey('nav_history_icon'), label: 'Activity',
railKey: ValueKey('rail_history_icon'), navKey: ValueKey('nav_activity_icon'),
railKey: ValueKey('rail_activity_icon'),
), ),
AppNavDestination( AppNavDestination(
icon: Icons.emoji_events_outlined, icon: Icons.two_wheeler_outlined,
selectedIcon: Icons.emoji_events, selectedIcon: Icons.two_wheeler,
label: 'Records',
navKey: ValueKey('nav_records_icon'),
railKey: ValueKey('rail_records_icon'),
),
AppNavDestination(
icon: Icons.two_wheeler,
label: 'Garage', label: 'Garage',
navKey: ValueKey('nav_garage_icon'), navKey: ValueKey('nav_garage_icon'),
railKey: ValueKey('rail_garage_icon'), railKey: ValueKey('rail_garage_icon'),
), ),
AppNavDestination(
icon: Icons.people_outline,
selectedIcon: Icons.people,
label: 'Social',
navKey: ValueKey('nav_social_icon'),
railKey: ValueKey('rail_social_icon'),
),
AppNavDestination( AppNavDestination(
icon: Icons.settings_outlined, icon: Icons.settings_outlined,
selectedIcon: Icons.settings, selectedIcon: Icons.settings,
@ -142,29 +144,30 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
selectedIndex: selectedIndex, selectedIndex: selectedIndex,
onDestinationSelected: _onDestinationSelected, onDestinationSelected: _onDestinationSelected,
backgroundColor: colorScheme.surface, backgroundColor: colorScheme.surface,
indicatorColor: colorScheme.primary.withValues(alpha: 0.18), indicatorColor: colorScheme.primary.withValues(alpha: 0.14),
selectedIconTheme: IconThemeData( selectedIconTheme: IconThemeData(
color: colorScheme.primary, color: colorScheme.primary,
size: 20, size: 20,
), ),
unselectedIconTheme: IconThemeData( unselectedIconTheme: IconThemeData(
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
size: 18, size: 19,
), ),
selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith( selectedLabelTextStyle: const TextStyle(
color: colorScheme.primary, fontFamily: AppTypography.fontHeadline,
fontSize: 9, fontSize: 9.5,
fontWeight: FontWeight.w800,
),
unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(
color: colorScheme.onSurfaceVariant,
fontSize: 8.5,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
unselectedLabelTextStyle: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 9.0,
fontWeight: FontWeight.w500,
color: colorScheme.onSurfaceVariant,
),
labelType: NavigationRailLabelType.all, labelType: NavigationRailLabelType.all,
destinations: kAppNavDestinations.map((dest) { destinations: kAppNavDestinations.map((dest) {
return NavigationRailDestination( return NavigationRailDestination(
padding: const EdgeInsets.symmetric(vertical: 2), padding: const EdgeInsets.symmetric(vertical: 3),
icon: Icon(dest.icon, key: dest.railKey), icon: Icon(dest.icon, key: dest.railKey),
selectedIcon: dest.selectedIcon != null selectedIcon: dest.selectedIcon != null
? Icon(dest.selectedIcon, key: dest.railKey) ? Icon(dest.selectedIcon, key: dest.railKey)
@ -198,14 +201,20 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
unselectedItemColor: colorScheme.onSurfaceVariant, unselectedItemColor: colorScheme.onSurfaceVariant,
backgroundColor: colorScheme.surface, backgroundColor: colorScheme.surface,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
selectedLabelStyle: AppTypography.telemetryLabel.copyWith( iconSize: 20,
selectedFontSize: 10,
unselectedFontSize: 9.5,
selectedLabelStyle: const TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w800,
),
unselectedLabelStyle: AppTypography.telemetryLabel.copyWith(
fontSize: 9.5,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
unselectedLabelStyle: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 9.5,
fontWeight: FontWeight.w500,
color: colorScheme.onSurfaceVariant,
),
items: kAppNavDestinations.map((dest) { items: kAppNavDestinations.map((dest) {
return BottomNavigationBarItem( return BottomNavigationBarItem(
icon: Icon(dest.icon, key: dest.navKey), icon: Icon(dest.icon, key: dest.navKey),

View File

@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/theme/app_typography.dart';
import '../../core/theme/theme_extensions.dart';
import '../history/history_screen.dart';
import '../records/records_screen.dart';
enum ActivityTab { rides, records }
/// Canonical unified Activity screen for MotorSignal.
///
/// Houses chronological ride history and personal performance records
/// under a single top-level destination with segmented navigation,
/// preserving information architecture and full responsiveness.
class ActivityScreen extends ConsumerStatefulWidget {
final ActivityTab initialTab;
const ActivityScreen({
super.key,
this.initialTab = ActivityTab.rides,
});
@override
ConsumerState<ActivityScreen> createState() => _ActivityScreenState();
}
class _ActivityScreenState extends ConsumerState<ActivityScreen> {
late ActivityTab _currentTab;
@override
void initState() {
super.initState();
_currentTab = widget.initialTab;
}
@override
void didUpdateWidget(covariant ActivityScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialTab != widget.initialTab) {
_currentTab = widget.initialTab;
}
}
@override
Widget build(BuildContext context) {
final colors = context.motorColors;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: Text(
_currentTab == ActivityTab.rides ? 'Ride History' : 'Personal Records',
style: const TextStyle(fontWeight: FontWeight.w700),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: SizedBox(
width: double.infinity,
height: 36,
child: SegmentedButton<ActivityTab>(
segments: const [
ButtonSegment<ActivityTab>(
value: ActivityTab.rides,
icon: Icon(Icons.history, size: 16),
label: Text(
'Rides',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
ButtonSegment<ActivityTab>(
value: ActivityTab.records,
icon: Icon(Icons.emoji_events_outlined, size: 16),
label: Text(
'Records',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
selected: {_currentTab},
onSelectionChanged: (selection) {
if (selection.isNotEmpty) {
setState(() {
_currentTab = selection.first;
});
}
},
style: ButtonStyle(
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: WidgetStateProperty.all(
BorderSide(
color: colors.outlineVariant,
width: 0.8,
),
),
backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
if (states.contains(WidgetState.selected)) {
return colors.cyberCyan.withValues(alpha: isDark ? 0.16 : 0.12);
}
return isDark ? colors.surfaceContainer : Colors.transparent;
}),
foregroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
if (states.contains(WidgetState.selected)) {
return colors.cyberCyan;
}
return isDark ? Colors.white70 : Colors.black87;
}),
),
),
),
),
),
),
body: IndexedStack(
index: _currentTab == ActivityTab.rides ? 0 : 1,
children: const [
HistoryScreen(isEmbedded: true),
RecordsScreen(isEmbedded: true),
],
),
);
}
}

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_typography.dart';
@ -22,6 +22,8 @@ class GPSStatusIndicator extends ConsumerWidget {
telemetryStreamProvider.select((s) => s.value?.gpsAccuracy), telemetryStreamProvider.select((s) => s.value?.gpsAccuracy),
); );
final theme = Theme.of(context);
final IconData icon; final IconData icon;
final Color color; final Color color;
final String label; final String label;
@ -29,7 +31,7 @@ class GPSStatusIndicator extends ConsumerWidget {
if (gpsAcc != null) { if (gpsAcc != null) {
if (gpsAcc <= 15.0) { if (gpsAcc <= 15.0) {
icon = Icons.gps_fixed; icon = Icons.gps_fixed;
color = AppColors.cyberCyan; color = AppColors.telemetryGreen;
label = 'GPS 3D FIX'; label = 'GPS 3D FIX';
} else if (gpsAcc <= 35.0) { } else if (gpsAcc <= 35.0) {
icon = Icons.gps_fixed; icon = Icons.gps_fixed;
@ -42,7 +44,7 @@ class GPSStatusIndicator extends ConsumerWidget {
} }
} else { } else {
icon = Icons.gps_off; icon = Icons.gps_off;
color = Colors.white38; color = theme.colorScheme.onSurfaceVariant;
label = 'NO GPS'; label = 'NO GPS';
} }

View File

@ -38,8 +38,8 @@ class HeadingIndicator extends ConsumerWidget {
headingStr, headingStr,
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontHeadline, fontFamily: AppTypography.fontHeadline,
fontSize: 16, fontSize: 14,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
color: cardinalColor, color: cardinalColor,
), ),
), ),
@ -48,8 +48,8 @@ class HeadingIndicator extends ConsumerWidget {
'${heading.toStringAsFixed(0)}°', '${heading.toStringAsFixed(0)}°',
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontTelemetry, fontFamily: AppTypography.fontTelemetry,
fontSize: 12, fontSize: 11,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
color: degreesColor, color: degreesColor,
), ),
), ),
@ -66,18 +66,18 @@ class HeadingIndicator extends ConsumerWidget {
headingStr, headingStr,
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontHeadline, fontFamily: AppTypography.fontHeadline,
fontSize: 26, fontSize: 20,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w700,
color: cardinalColor, color: cardinalColor,
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 5),
Text( Text(
'${heading.toStringAsFixed(0)}°', '${heading.toStringAsFixed(0)}°',
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontTelemetry, fontFamily: AppTypography.fontTelemetry,
fontSize: 16, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
color: degreesColor, color: degreesColor,
), ),
), ),

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_typography.dart';
@ -31,11 +31,11 @@ class RideActionButton extends ConsumerWidget {
if (isRiding) { if (isRiding) {
buttonColor = AppColors.raceRed; buttonColor = AppColors.raceRed;
label = 'STOP SESSION'; label = 'Stop Session';
icon = Icons.stop_circle_outlined; icon = Icons.stop_circle_outlined;
} else { } else {
buttonColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary; buttonColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
label = 'START SESSION'; label = 'Start Session';
icon = Icons.play_circle_outlined; icon = Icons.play_circle_outlined;
} }
@ -46,7 +46,7 @@ class RideActionButton extends ConsumerWidget {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: buttonColor.withValues(alpha: isRiding ? 0.16 : 0.12), backgroundColor: buttonColor.withValues(alpha: isRiding ? 0.16 : 0.12),
foregroundColor: buttonColor, foregroundColor: buttonColor,
side: BorderSide(color: buttonColor, width: 2.0), side: BorderSide(color: buttonColor, width: 1.0),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
@ -66,8 +66,8 @@ class RideActionButton extends ConsumerWidget {
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontHeadline, fontFamily: AppTypography.fontHeadline,
fontSize: compact ? 15 : 18, fontSize: compact ? 15 : 18,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
letterSpacing: 1.5, letterSpacing: 0.5,
), ),
), ),
), ),

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_typography.dart';
@ -20,6 +20,9 @@ class RideStatusChip extends ConsumerWidget {
final isRecording = state == RideState.riding; final isRecording = state == RideState.riding;
final isPaused = state == RideState.paused; final isPaused = state == RideState.paused;
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final Color badgeColor; final Color badgeColor;
final String label; final String label;
@ -30,10 +33,10 @@ class RideStatusChip extends ConsumerWidget {
badgeColor = AppColors.warningAmber; badgeColor = AppColors.warningAmber;
label = 'PAUSED'; label = 'PAUSED';
} else if (state == RideState.ready) { } else if (state == RideState.ready) {
badgeColor = AppColors.cyberCyan; badgeColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
label = 'READY'; label = 'READY';
} else { } else {
badgeColor = Colors.white54; badgeColor = theme.colorScheme.onSurfaceVariant;
label = 'STANDBY'; label = 'STANDBY';
} }

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_typography.dart';
@ -87,14 +87,6 @@ class SpeedReadout extends ConsumerWidget {
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
height: 0.95, height: 0.95,
color: speedColor, color: speedColor,
shadows: isDark
? [
Shadow(
color: AppColors.cyberCyan.withValues(alpha: 0.4),
blurRadius: 16,
),
]
: null,
), ),
), ),
), ),

View File

@ -82,8 +82,8 @@ class GarageScreen extends ConsumerWidget {
foregroundColor: Colors.black, foregroundColor: Colors.black,
icon: const Icon(Icons.two_wheeler), icon: const Icon(Icons.two_wheeler),
label: const Text( label: const Text(
'ADD MOTORCYCLE', 'Add Motorcycle',
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.8), style: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.2),
), ),
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value), onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
) )
@ -127,12 +127,12 @@ class GarageScreen extends ConsumerWidget {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'YOUR GARAGE IS EMPTY', 'Your Garage is Empty',
style: TextStyle( style: TextStyle(
fontFamily: 'Sora', fontFamily: 'Sora',
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w700,
letterSpacing: 1.2, letterSpacing: 0.2,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
), ),
@ -156,15 +156,15 @@ class GarageScreen extends ConsumerWidget {
vertical: 14, vertical: 14,
), ),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(10),
), ),
), ),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text( label: const Text(
'ADD MOTORCYCLE', 'Add Motorcycle',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
letterSpacing: 1.2, letterSpacing: 0.3,
), ),
), ),
onPressed: () => context.push('/garage/add'), onPressed: () => context.push('/garage/add'),
@ -204,9 +204,9 @@ class GarageScreen extends ConsumerWidget {
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border.all( border: Border.all(
color: bike.isDefault color: bike.isDefault
? colors.cyberCyan ? colors.cyberCyan.withValues(alpha: 0.6)
: colors.outlineVariant, : colors.outlineVariant,
width: bike.isDefault ? 1.8 : 1.0, width: bike.isDefault ? 1.0 : 0.8,
), ),
), ),
child: Material( child: Material(
@ -242,7 +242,7 @@ class GarageScreen extends ConsumerWidget {
style: TextStyle( style: TextStyle(
fontFamily: 'Sora', fontFamily: 'Sora',
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
), ),
@ -251,23 +251,24 @@ class GarageScreen extends ConsumerWidget {
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
vertical: 2, vertical: 3,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.cyberCyan.withValues(alpha: 0.15), color: colors.cyberCyan.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(6),
border: Border.all( border: Border.all(
color: colors.cyberCyan, color: colors.cyberCyan.withValues(alpha: 0.4),
width: 0.8,
), ),
), ),
child: Text( child: Text(
'CURRENT', 'Current',
style: TextStyle( style: TextStyle(
fontFamily: 'Inter', fontFamily: 'Inter',
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w600,
color: colors.cyberCyan, color: colors.cyberCyan,
letterSpacing: 0.5, letterSpacing: 0.3,
), ),
), ),
), ),
@ -282,7 +283,7 @@ class GarageScreen extends ConsumerWidget {
fontFamily: 'JetBrains Mono', fontFamily: 'JetBrains Mono',
color: colors.cyberCyan, color: colors.cyberCyan,
fontSize: 12.5, fontSize: 12.5,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
), ),
), ),
], ],

View File

@ -9,7 +9,12 @@ import '../garage/garage_providers.dart';
import 'ride_detail_screen.dart'; import 'ride_detail_screen.dart';
class HistoryScreen extends ConsumerStatefulWidget { class HistoryScreen extends ConsumerStatefulWidget {
const HistoryScreen({super.key}); final bool isEmbedded;
const HistoryScreen({
super.key,
this.isEmbedded = false,
});
@override @override
ConsumerState<HistoryScreen> createState() => _HistoryScreenState(); ConsumerState<HistoryScreen> createState() => _HistoryScreenState();
@ -26,12 +31,14 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
final colors = context.motorColors; final colors = context.motorColors;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: widget.isEmbedded
title: const Text( ? null
'Ride History', : AppBar(
style: TextStyle(fontWeight: FontWeight.bold), title: const Text(
), 'Ride History',
), style: TextStyle(fontWeight: FontWeight.bold),
),
),
body: Column( body: Column(
children: [ children: [
_buildFilterBar(context, garageBikesAsync.value ?? [], defaultBike), _buildFilterBar(context, garageBikesAsync.value ?? [], defaultBike),

View File

@ -380,12 +380,12 @@ class _SetupView extends StatelessWidget {
), ),
onPressed: onArm, onPressed: onArm,
child: const Text( child: const Text(
'ARM LAUNCH', 'Arm Launch',
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontHeadline, fontFamily: AppTypography.fontHeadline,
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w700,
letterSpacing: 1.2, letterSpacing: 0.5,
), ),
), ),
), ),
@ -541,7 +541,7 @@ class _WaitingView extends ConsumerWidget {
onPressed: () { onPressed: () {
ref.read(performanceLaunchProvider.notifier).reset(); ref.read(performanceLaunchProvider.notifier).reset();
}, },
child: const Text('CANCEL'), child: const Text('Cancel'),
), ),
], ],
), ),
@ -644,12 +644,12 @@ class _ReadyView extends ConsumerWidget {
ref.read(performanceLaunchProvider.notifier).startCountdown(); ref.read(performanceLaunchProvider.notifier).startCountdown();
}, },
child: const Text( child: const Text(
'START COUNTDOWN', 'Start Countdown',
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontHeadline, fontFamily: AppTypography.fontHeadline,
fontSize: 17, fontSize: 16,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w700,
letterSpacing: 1.2, letterSpacing: 0.5,
), ),
), ),
), ),
@ -773,7 +773,7 @@ class _CountdownViewState extends ConsumerState<_CountdownView>
onPressed: () { onPressed: () {
ref.read(performanceLaunchProvider.notifier).cancel(); ref.read(performanceLaunchProvider.notifier).cancel();
}, },
child: const Text('ABORT'), child: const Text('Abort'),
), ),
], ],
), ),
@ -1316,7 +1316,7 @@ class _ResultView extends ConsumerWidget {
onPressed: () { onPressed: () {
ref.read(performanceLaunchProvider.notifier).reset(); ref.read(performanceLaunchProvider.notifier).reset();
}, },
child: const Text('TRY AGAIN'), child: const Text('Try Again'),
), ),
), ),
), ),
@ -1338,7 +1338,7 @@ class _ResultView extends ConsumerWidget {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
}, },
child: const Text('DONE'), child: const Text('Done'),
), ),
), ),
), ),
@ -1436,7 +1436,7 @@ class _AbortedView extends ConsumerWidget {
onPressed: () { onPressed: () {
ref.read(performanceLaunchProvider.notifier).reset(); ref.read(performanceLaunchProvider.notifier).reset();
}, },
child: const Text('TRY AGAIN'), child: const Text('Try Again'),
), ),
), ),
], ],

View File

@ -7,7 +7,12 @@ import '../../core/theme/theme_extensions.dart';
import 'records_service.dart'; import 'records_service.dart';
class RecordsScreen extends ConsumerWidget { class RecordsScreen extends ConsumerWidget {
const RecordsScreen({super.key}); final bool isEmbedded;
const RecordsScreen({
super.key,
this.isEmbedded = false,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -15,19 +20,21 @@ class RecordsScreen extends ConsumerWidget {
final colors = context.motorColors; final colors = context.motorColors;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: isEmbedded
title: const Text( ? null
'Personal Records', : AppBar(
style: TextStyle(fontWeight: FontWeight.bold), title: const Text(
), 'Personal Records',
actions: [ style: TextStyle(fontWeight: FontWeight.bold),
IconButton( ),
icon: Icon(Icons.emoji_events, color: colors.warningAmber), actions: [
tooltip: 'Trophies', IconButton(
onPressed: () {}, icon: Icon(Icons.emoji_events, color: colors.warningAmber),
), tooltip: 'Trophies',
], onPressed: () {},
), ),
],
),
body: ResponsiveLayout( body: ResponsiveLayout(
builder: (context, responsive) { builder: (context, responsive) {
final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape); final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
@ -42,11 +49,11 @@ class RecordsScreen extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildSectionHeader('SPEED', colors), _buildSectionHeader('Speed', colors),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors), _buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildSectionHeader('PERFORMANCE TIMES', colors), _buildSectionHeader('Performance Times', colors),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildPerformanceTimesCard(context, records, colors), _buildPerformanceTimesCard(context, records, colors),
], ],
@ -57,11 +64,11 @@ class RecordsScreen extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildSectionHeader('HANDLING & DYNAMICS', colors), _buildSectionHeader('Handling & Dynamics', colors),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildHandlingCard(context, records, colors), _buildHandlingCard(context, records, colors),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildSectionHeader('ENDURANCE & ROUTE', colors), _buildSectionHeader('Endurance & Route', colors),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildEnduranceCard(context, records, colors), _buildEnduranceCard(context, records, colors),
], ],
@ -77,19 +84,19 @@ class RecordsScreen extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildSectionHeader('SPEED', colors), _buildSectionHeader('Speed', colors),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors), _buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
const SizedBox(height: 18), const SizedBox(height: 18),
_buildSectionHeader('PERFORMANCE TIMES', colors), _buildSectionHeader('Performance Times', colors),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildPerformanceTimesCard(context, records, colors), _buildPerformanceTimesCard(context, records, colors),
const SizedBox(height: 18), const SizedBox(height: 18),
_buildSectionHeader('HANDLING & DYNAMICS', colors), _buildSectionHeader('Handling & Dynamics', colors),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildHandlingCard(context, records, colors), _buildHandlingCard(context, records, colors),
const SizedBox(height: 18), const SizedBox(height: 18),
_buildSectionHeader('ENDURANCE & ROUTE', colors), _buildSectionHeader('Endurance & Route', colors),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildEnduranceCard(context, records, colors), _buildEnduranceCard(context, records, colors),
const SizedBox(height: 24), const SizedBox(height: 24),
@ -246,12 +253,12 @@ class RecordsScreen extends ConsumerWidget {
key: const ValueKey('btn_open_launch_mode'), key: const ValueKey('btn_open_launch_mode'),
icon: Icon(Icons.rocket_launch, size: 16, color: colors.launchGreen), icon: Icon(Icons.rocket_launch, size: 16, color: colors.launchGreen),
label: Text( label: Text(
'PERFORMANCE LAUNCH MODE', 'Performance Launch Mode',
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontHeadline, fontFamily: AppTypography.fontHeadline,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
letterSpacing: 1.0, letterSpacing: 0.5,
color: colors.launchGreen, color: colors.launchGreen,
), ),
), ),

View File

@ -97,8 +97,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
_buildSectionHeader('Account & Cloud Identity'), _buildSectionHeader('Account & Cloud Identity'),
const SizedBox(height: 10), const SizedBox(height: 10),
_buildAccountCard(context), _buildAccountCard(context),
const SizedBox(height: 12),
_buildRiderSocialLinks(context),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildSectionHeader('Appearance & Display Theme'), _buildSectionHeader('Appearance & Display Theme'),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -165,8 +163,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
_buildSectionHeader('Account & Cloud Identity'), _buildSectionHeader('Account & Cloud Identity'),
const SizedBox(height: 10), const SizedBox(height: 10),
_buildAccountCard(context), _buildAccountCard(context),
const SizedBox(height: 12),
_buildRiderSocialLinks(context),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildSectionHeader('Appearance & Display Theme'), _buildSectionHeader('Appearance & Display Theme'),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -208,12 +204,12 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
Widget _buildSectionHeader(String title) { Widget _buildSectionHeader(String title) {
return Text( return Text(
title.toUpperCase(), title,
style: TextStyle( style: TextStyle(
fontFamily: 'Sora', fontFamily: 'Sora',
fontSize: 12, fontSize: 13,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
letterSpacing: 0.8, letterSpacing: 0.2,
color: context.motorColors.cyberCyan, color: context.motorColors.cyberCyan,
), ),
); );
@ -310,97 +306,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
); );
} }
Widget _buildRiderSocialLinks(BuildContext context) {
final colors = context.motorColors;
return LayoutBuilder(
builder: (context, constraints) {
final useRow = constraints.maxWidth > 340;
final buttons = [
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface,
side: BorderSide(color: colors.outlineVariant),
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
icon: const Icon(Icons.person_outline, size: 16),
label: const FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'PROFILE',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
),
),
onPressed: () => context.push('/settings/profile'),
),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface,
side: BorderSide(color: colors.outlineVariant),
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
icon: const Icon(Icons.people_outline, size: 16),
label: const FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'FRIENDS',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
),
),
onPressed: () => context.push('/settings/friends'),
),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: colors.cyberCyan,
side: BorderSide(color: colors.cyberCyan.withValues(alpha: 0.6)),
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
icon: const Icon(Icons.groups_outlined, size: 16),
label: const FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'GROUP RIDES',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
),
),
onPressed: () => context.push('/group-rides'),
),
];
if (useRow) {
return Row(
children: [
Expanded(child: buttons[0]),
const SizedBox(width: 8),
Expanded(child: buttons[1]),
const SizedBox(width: 8),
Expanded(child: buttons[2]),
],
);
}
return Wrap(
spacing: 8,
runSpacing: 8,
children: buttons
.map((b) => SizedBox(
width: (constraints.maxWidth - 8) / 2,
child: b,
))
.toList(),
);
},
);
}
Widget _buildAppearanceCard(BuildContext context) { Widget _buildAppearanceCard(BuildContext context) {
final colors = context.motorColors; final colors = context.motorColors;
return Card( return Card(

View File

@ -117,24 +117,24 @@ class CurrentPlanCard extends ConsumerWidget {
Icon( Icon(
isPro ? Icons.verified : Icons.shield_outlined, isPro ? Icons.verified : Icons.shield_outlined,
color: isPro ? colors.cyberCyan : colors.outline, color: isPro ? colors.cyberCyan : colors.outline,
size: 20, size: 18,
), ),
const SizedBox(width: 10), const SizedBox(width: 8),
Text( Text(
planTitle.toUpperCase(), planTitle,
style: TextStyle( style: TextStyle(
fontFamily: 'Sora', fontFamily: 'Sora',
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
color: isPro ? colors.cyberCyan : Theme.of(context).colorScheme.onSurface, color: isPro ? colors.cyberCyan : Theme.of(context).colorScheme.onSurface,
letterSpacing: 0.5, letterSpacing: 0.2,
), ),
), ),
if (isPro) ...[const SizedBox(width: 8), const ProBadge()], if (isPro) ...[const SizedBox(width: 6), const ProBadge()],
], ],
), ),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: badgeColor.withValues(alpha: 0.15), color: badgeColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
@ -148,7 +148,7 @@ class CurrentPlanCard extends ConsumerWidget {
style: TextStyle( style: TextStyle(
fontFamily: 'Inter', fontFamily: 'Inter',
fontSize: 9, fontSize: 9,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w700,
color: badgeColor, color: badgeColor,
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
@ -167,8 +167,8 @@ class CurrentPlanCard extends ConsumerWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Wrap( Wrap(
spacing: 10, spacing: 8,
runSpacing: 10, runSpacing: 8,
children: [ children: [
if (!isPro) if (!isPro)
ElevatedButton( ElevatedButton(
@ -176,19 +176,19 @@ class CurrentPlanCard extends ConsumerWidget {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: colors.cyberCyan, backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
minimumSize: const Size(140, 48), minimumSize: const Size(130, 44),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(10),
), ),
), ),
onPressed: () => context.push('/settings/pro'), onPressed: () => context.push('/settings/pro'),
child: const Text( child: const Text(
'UPGRADE TO PRO', 'Upgrade to Pro',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 11.5,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w600,
letterSpacing: 0.5, letterSpacing: 0.2,
), ),
), ),
), ),
@ -197,18 +197,18 @@ class CurrentPlanCard extends ConsumerWidget {
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface, foregroundColor: Theme.of(context).colorScheme.onSurface,
side: BorderSide(color: colors.outlineVariant), side: BorderSide(color: colors.outlineVariant),
minimumSize: const Size(160, 48), minimumSize: const Size(140, 44),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(10),
), ),
), ),
onPressed: _openGooglePlaySubscriptions, onPressed: _openGooglePlaySubscriptions,
child: const Text( child: const Text(
'MANAGE SUBSCRIPTION', 'Manage Subscription',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11.5,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
), ),
), ),
), ),
@ -217,16 +217,16 @@ class CurrentPlanCard extends ConsumerWidget {
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface, foregroundColor: Theme.of(context).colorScheme.onSurface,
side: BorderSide(color: colors.outlineVariant), side: BorderSide(color: colors.outlineVariant),
minimumSize: const Size(100, 48), minimumSize: const Size(90, 44),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(10),
), ),
), ),
onPressed: () => context.push('/settings/pro'), onPressed: () => context.push('/settings/pro'),
child: Text( child: Text(
isPro ? 'VIEW PLANS' : 'DETAILS', isPro ? 'View Plans' : 'Details',
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.w600),
), ),
), ),
], ],

View File

@ -0,0 +1,326 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/responsive/responsive_layout.dart';
import '../../core/theme/app_typography.dart';
import '../../core/theme/theme_extensions.dart';
import '../../services/auth/auth_providers.dart';
import '../../services/social/social_providers.dart';
import 'widgets/avatar_badge.dart';
/// The authoritative landing screen for MotorSignal Social & Community.
///
/// Unifies Rider Profile, Friends, and Group Rides under a dedicated,
/// restrained automotive social home completely decoupled from Settings.
class SocialHubScreen extends ConsumerWidget {
const SocialHubScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final colors = context.motorColors;
final user = ref.watch(currentUserProvider);
final profileAsync = ref.watch(myRiderProfileProvider);
final requestsAsync = ref.watch(myIncomingRequestsProvider);
final pendingCount = requestsAsync.value?.length ?? 0;
return Scaffold(
appBar: AppBar(
title: const Text(
'Social',
style: TextStyle(fontWeight: FontWeight.w700),
),
),
body: SafeArea(
child: ResponsiveLayout(
builder: (context, responsive) {
final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Rider Identity Header Card
_buildRiderHeroCard(context, profileAsync.value, user, colors),
const SizedBox(height: 18),
Text(
'Rider Network',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 13,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: colors.cyberCyan,
),
),
const SizedBox(height: 10),
if (isWide) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_profile'),
icon: Icons.badge_outlined,
title: 'Rider Profile',
subtitle: 'Manage public handle, bio, region & visibility preferences',
colors: colors,
onTap: () => context.push('/social/profile'),
),
),
const SizedBox(width: 14),
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_friends'),
icon: Icons.people_outline,
title: 'Friends',
subtitle: 'Find fellow riders, manage requests & connections',
badgeCount: pendingCount,
colors: colors,
onTap: () => context.push('/social/friends'),
),
),
],
),
const SizedBox(height: 14),
_buildNavigationCard(
context,
key: const ValueKey('social_card_group_rides'),
icon: Icons.groups_outlined,
title: 'Group Rides',
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
colors: colors,
onTap: () => context.push('/group-rides'),
),
] else ...[
_buildNavigationCard(
context,
key: const ValueKey('social_card_profile'),
icon: Icons.badge_outlined,
title: 'Rider Profile',
subtitle: 'Manage public handle, bio, region & visibility preferences',
colors: colors,
onTap: () => context.push('/social/profile'),
),
const SizedBox(height: 12),
_buildNavigationCard(
context,
key: const ValueKey('social_card_friends'),
icon: Icons.people_outline,
title: 'Friends',
subtitle: 'Find fellow riders, manage requests & connections',
badgeCount: pendingCount,
colors: colors,
onTap: () => context.push('/social/friends'),
),
const SizedBox(height: 12),
_buildNavigationCard(
context,
key: const ValueKey('social_card_group_rides'),
icon: Icons.groups_outlined,
title: 'Group Rides',
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
colors: colors,
onTap: () => context.push('/group-rides'),
),
],
const SizedBox(height: 24),
],
),
),
),
);
},
),
),
);
}
Widget _buildRiderHeroCard(
BuildContext context,
dynamic profile,
dynamic user,
MotorSignalThemeColors colors,
) {
final hasProfile = profile != null;
final displayName = hasProfile ? profile.displayName : (user?.displayLabel ?? 'Rider');
final handle = hasProfile ? '@${profile.handle}' : 'Tap to setup rider profile';
final avatarPreset = hasProfile ? profile.avatarPreset : null;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: colors.outlineVariant, width: 0.8),
),
child: Row(
children: [
AvatarBadge(
avatarPreset: avatarPreset,
displayName: displayName,
radius: 22,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
handle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: AppTypography.fontBody,
fontSize: 11.5,
fontWeight: FontWeight.w500,
color: colors.cyberCyan,
),
),
],
),
),
const SizedBox(width: 8),
OutlinedButton(
style: OutlinedButton.styleFrom(
visualDensity: VisualDensity.compact,
side: BorderSide(color: colors.outlineVariant, width: 0.8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
),
onPressed: () => context.push('/social/profile'),
child: const Text(
'Edit',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildNavigationCard(
BuildContext context, {
required Key key,
required IconData icon,
required String title,
required String subtitle,
required MotorSignalThemeColors colors,
required VoidCallback onTap,
int badgeCount = 0,
}) {
return Container(
key: key,
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: colors.outlineVariant, width: 0.8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: colors.cyberCyan.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: colors.cyberCyan, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
if (badgeCount > 0) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: colors.cyberCyan,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'$badgeCount',
style: const TextStyle(
fontFamily: AppTypography.fontTelemetry,
fontSize: 10,
fontWeight: FontWeight.w800,
color: Colors.black,
),
),
),
],
],
),
const SizedBox(height: 3),
Text(
subtitle,
style: TextStyle(
fontFamily: AppTypography.fontBody,
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1.3,
),
),
],
),
),
const SizedBox(width: 8),
Icon(
Icons.chevron_right,
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
size: 20,
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,251 @@
import 'dart:convert';
import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:motorcycle_dash/core/theme/theme.dart';
import 'package:motorcycle_dash/database/database.dart';
import 'package:motorcycle_dash/features/activity/activity_screen.dart';
import 'package:motorcycle_dash/features/garage/garage_providers.dart';
import 'package:motorcycle_dash/features/history/history_screen.dart';
import 'package:motorcycle_dash/features/records/records_screen.dart';
import 'package:motorcycle_dash/features/records/records_service.dart';
import 'package:motorcycle_dash/models/motorcycle_models.dart';
import 'package:motorcycle_dash/services/github/github_sync_service.dart';
import 'package:motorcycle_dash/services/motorcycle/motorcycle_repository.dart';
class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService {
@override
GitHubUploadState get state => GitHubUploadState.idle;
@override
String get statusMessage => '';
@override
Set<int> get queuedRideIds => const {};
@override
bool isRideUploaded(int rideId) => false;
@override
String? getRideCommitSha(int rideId) => null;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeMotorcycleRepository implements MotorcycleRepository {
@override
Future<List<MotorcycleYear>> getYears() async => [MotorcycleYear(2024)];
@override
Future<List<MotorcycleMake>> getMakes(int year) async => [MotorcycleMake(id: 'yamaha', name: 'Yamaha')];
@override
Future<List<MotorcycleModel>> getModels(int year, String makeId) async => [MotorcycleModel(id: 'r1', name: 'YZF-R1', makeId: makeId)];
@override
Future<List<MotorcycleVariant>> getVariants(int year, String makeId, String modelId) async => [MotorcycleVariant(id: 'std', name: 'Standard')];
@override
Future<MotorcycleSpecification?> getSpecifications(MotorcycleSelection selection) async => null;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late AppDatabase db;
late SharedPreferences prefs;
setUp(() async {
db = AppDatabase.forTesting(NativeDatabase.memory());
SharedPreferences.setMockInitialValues({
'topSpeedMph': 120.0,
'fastest0To60': 3.5,
'fastest0To100': 7.2,
'maxLeanLeft': 42.0,
'maxLeanRight': 44.0,
});
prefs = await SharedPreferences.getInstance();
final spec = MotorcycleSpecification(
year: 2024,
manufacturer: 'Yamaha',
model: 'YZF-R1',
displacementCc: 998,
horsepower: 198,
torqueLbFt: 83.0,
wetWeightLbs: 448,
claimedTopSpeedMph: 186,
claimedZeroToSixtySec: 2.8,
);
await db.into(db.garageMotorcycles).insert(
GarageMotorcyclesCompanion.insert(
year: 2024,
makeId: 'yamaha',
makeName: 'Yamaha',
modelId: 'r1',
modelName: 'YZF-R1',
nickname: const drift.Value('Blue Thunder'),
isDefault: const drift.Value(true),
dateAdded: DateTime.now(),
lastUpdated: DateTime.now(),
specificationJson: drift.Value(json.encode(spec.toJson())),
),
);
await db.into(db.rides).insert(
RidesCompanion.insert(
startTime: DateTime(2026, 1, 1, 10, 0),
endTime: drift.Value(DateTime(2026, 1, 1, 10, 45)),
distanceMiles: const drift.Value(25.5),
topSpeedMph: const drift.Value(65.0),
movingTime: const drift.Value(2400),
stoppedTime: const drift.Value(300),
distance: const drift.Value(25.5),
topSpeed: const drift.Value(65.0),
averageSpeed: const drift.Value(42.0),
elevationGain: const drift.Value(350.0),
elevationLoss: const drift.Value(300.0),
maxLeanLeft: const drift.Value(34.0),
maxLeanRight: const drift.Value(36.0),
maxAccelG: const drift.Value(0.75),
maxBrakingG: const drift.Value(0.85),
motorcycleId: const drift.Value(1),
motorcycleYear: const drift.Value(2024),
motorcycleMake: const drift.Value('Yamaha'),
motorcycleModel: const drift.Value('YZF-R1'),
),
);
});
tearDown(() async {
await db.close();
});
Widget buildTestActivityScreen({
ActivityTab initialTab = ActivityTab.rides,
ThemeData? theme,
Size size = const Size(400, 800),
}) {
return ProviderScope(
overrides: [
databaseProvider.overrideWithValue(db),
sharedPreferencesProvider.overrideWithValue(prefs),
githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()),
motorcycleRepositoryProvider.overrideWith((ref) => _FakeMotorcycleRepository()),
],
child: MaterialApp(
theme: theme ?? AppTheme.darkTheme,
home: MediaQuery(
data: MediaQueryData(size: size),
child: ActivityScreen(initialTab: initialTab),
),
),
);
}
group('ActivityScreen Widget & Presentation Tests', () {
testWidgets('Renders segmented button with Rides and Records tabs', (tester) async {
await tester.pumpWidget(buildTestActivityScreen());
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(find.byType(SegmentedButton<ActivityTab>), findsOneWidget);
expect(find.text('Rides'), findsOneWidget);
expect(find.text('Records'), findsOneWidget);
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(milliseconds: 100));
});
testWidgets('Defaults to Rides tab hosting embedded HistoryScreen without standalone AppBar', (tester) async {
await tester.pumpWidget(buildTestActivityScreen());
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(find.byType(HistoryScreen), findsOneWidget);
expect(find.byType(RecordsScreen), findsNothing);
// Embedded history screen suppresses standalone AppBar
final historyFinder = find.byType(HistoryScreen);
expect(find.descendant(of: historyFinder, matching: find.byType(AppBar)), findsNothing);
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(milliseconds: 100));
});
testWidgets('Switches to Records tab when tapped', (tester) async {
await tester.pumpWidget(buildTestActivityScreen());
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(find.byType(HistoryScreen), findsOneWidget);
expect(find.byType(RecordsScreen), findsNothing);
// Tap Records segment
await tester.tap(find.text('Records'));
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(find.byType(RecordsScreen), findsOneWidget);
expect(find.byType(HistoryScreen), findsNothing);
// Tap Rides segment back
await tester.tap(find.text('Rides'));
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(find.byType(HistoryScreen), findsOneWidget);
expect(find.byType(RecordsScreen), findsNothing);
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(milliseconds: 100));
});
testWidgets('Initializes directly to Records tab when requested', (tester) async {
await tester.pumpWidget(buildTestActivityScreen(initialTab: ActivityTab.records));
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(find.byType(RecordsScreen), findsOneWidget);
expect(find.byType(HistoryScreen), findsNothing);
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(milliseconds: 100));
});
testWidgets('Renders cleanly under Light Theme without exceptions', (tester) async {
await tester.pumpWidget(buildTestActivityScreen(theme: AppTheme.lightTheme));
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(find.byType(ActivityScreen), findsOneWidget);
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(milliseconds: 100));
});
testWidgets('Adapts cleanly across multiple viewports without overflow', (tester) async {
const viewports = [
Size(320, 640),
Size(412, 915),
Size(800, 360),
Size(1024, 768),
];
for (final vp in viewports) {
tester.view.physicalSize = vp;
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
await tester.pumpWidget(buildTestActivityScreen(size: vp));
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull, reason: 'No overflow on $vp');
expect(find.byType(ActivityScreen), findsOneWidget);
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(milliseconds: 100));
}
});
});
}

View File

@ -118,7 +118,7 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
expect(find.byType(SpeedReadout), findsOneWidget); expect(find.byType(SpeedReadout), findsOneWidget);
expect(find.text('START SESSION'), findsOneWidget); expect(find.text('Start Session'), findsOneWidget);
}); });
testWidgets('Renders properly in Dark Mode', (tester) async { testWidgets('Renders properly in Dark Mode', (tester) async {
@ -130,7 +130,7 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
expect(find.byType(SpeedReadout), findsOneWidget); expect(find.byType(SpeedReadout), findsOneWidget);
expect(find.text('START SESSION'), findsOneWidget); expect(find.text('Start Session'), findsOneWidget);
}); });
}); });
@ -223,7 +223,7 @@ void main() {
await tester.tap(find.byType(RideActionButton)); await tester.tap(find.byType(RideActionButton));
await tester.pump(); await tester.pump();
expect(find.text('STOP SESSION'), findsOneWidget); expect(find.text('Stop Session'), findsOneWidget);
expect(find.text('RECORDING'), findsOneWidget); expect(find.text('RECORDING'), findsOneWidget);
// Rotate to landscape // Rotate to landscape
@ -231,7 +231,7 @@ void main() {
await tester.pump(); await tester.pump();
// Verify ride did NOT stop or reset // Verify ride did NOT stop or reset
expect(find.text('STOP SESSION'), findsOneWidget); expect(find.text('Stop Session'), findsOneWidget);
expect(find.text('RECORDING'), findsOneWidget); expect(find.text('RECORDING'), findsOneWidget);
}); });

View File

@ -17,9 +17,9 @@ void main() {
final router = GoRouter( final router = GoRouter(
initialLocation: switch (initialIndex) { initialLocation: switch (initialIndex) {
1 => '/map', 1 => '/map',
2 => '/history', 2 => '/activity',
3 => '/records', 3 => '/garage',
4 => '/garage', 4 => '/social',
5 => '/settings', 5 => '/settings',
_ => '/', _ => '/',
}, },
@ -52,19 +52,9 @@ void main() {
StatefulShellBranch( StatefulShellBranch(
routes: [ routes: [
GoRoute( GoRoute(
path: '/history', path: '/activity',
builder: (context, state) => const Scaffold( builder: (context, state) => const Scaffold(
body: Center(child: Text('History Screen Content')), body: Center(child: Text('Activity Screen Content')),
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/records',
builder: (context, state) => const Scaffold(
body: Center(child: Text('Records Screen Content')),
), ),
), ),
], ],
@ -79,6 +69,16 @@ void main() {
), ),
], ],
), ),
StatefulShellBranch(
routes: [
GoRoute(
path: '/social',
builder: (context, state) => const Scaffold(
body: Center(child: Text('Social Screen Content')),
),
),
],
),
StatefulShellBranch( StatefulShellBranch(
routes: [ routes: [
GoRoute( GoRoute(
@ -131,9 +131,9 @@ void main() {
// Verify all 6 navigation labels exist regardless of layout // Verify all 6 navigation labels exist regardless of layout
expect(find.text('Dash'), findsOneWidget); expect(find.text('Dash'), findsOneWidget);
expect(find.text('Map'), findsOneWidget); expect(find.text('Map'), findsOneWidget);
expect(find.text('History'), findsOneWidget); expect(find.text('Activity'), findsOneWidget);
expect(find.text('Records'), findsOneWidget);
expect(find.text('Garage'), findsOneWidget); expect(find.text('Garage'), findsOneWidget);
expect(find.text('Social'), findsOneWidget);
expect(find.text('Settings'), findsOneWidget); expect(find.text('Settings'), findsOneWidget);
}); });
} }
@ -223,10 +223,10 @@ void main() {
expect(find.text('Map Screen Content'), findsOneWidget); expect(find.text('Map Screen Content'), findsOneWidget);
await tester.tap(find.text('Records')); await tester.tap(find.text('Activity'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Records Screen Content'), findsOneWidget); expect(find.text('Activity Screen Content'), findsOneWidget);
}); });
testWidgets('NavigationRail touch targets have adequate interactive hit bounds', (tester) async { testWidgets('NavigationRail touch targets have adequate interactive hit bounds', (tester) async {
@ -257,29 +257,29 @@ void main() {
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(400, 800))); await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(400, 800)));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Navigate to History // Navigate to Activity
await tester.tap(find.text('History')); await tester.tap(find.text('Activity'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('History Screen Content'), findsOneWidget); expect(find.text('Activity Screen Content'), findsOneWidget);
// Rotate / Resize to short landscape (800x360) // Rotate / Resize to short landscape (800x360)
tester.view.physicalSize = const Size(800, 360); tester.view.physicalSize = const Size(800, 360);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Shell adapts to NavigationRail, while maintaining History selection! // Shell adapts to NavigationRail, while maintaining Activity selection!
expect(find.byType(NavigationRail), findsOneWidget); expect(find.byType(NavigationRail), findsOneWidget);
expect(find.text('History Screen Content'), findsOneWidget); expect(find.text('Activity Screen Content'), findsOneWidget);
// Resize to large tablet (1024x768) // Resize to large tablet (1024x768)
tester.view.physicalSize = const Size(1024, 768); tester.view.physicalSize = const Size(1024, 768);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('History Screen Content'), findsOneWidget); expect(find.text('Activity Screen Content'), findsOneWidget);
// Resize back to portrait (320x640) // Resize back to portrait (320x640)
tester.view.physicalSize = const Size(320, 640); tester.view.physicalSize = const Size(320, 640);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(BottomNavigationBar), findsOneWidget); expect(find.byType(BottomNavigationBar), findsOneWidget);
expect(find.text('History Screen Content'), findsOneWidget); expect(find.text('Activity Screen Content'), findsOneWidget);
}); });
testWidgets('Renders cleanly under Light Theme without visual regressions', (tester) async { testWidgets('Renders cleanly under Light Theme without visual regressions', (tester) async {

View File

@ -419,6 +419,17 @@ void main() {
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 3750)); telemetryController.add(makeSample(speed: 5.0, monotonicMs: 3750));
await tester.pump(const Duration(milliseconds: 20)); await tester.pump(const Duration(milliseconds: 20));
// Timing continues actively despite false start
expect(stateMachine.state.state, PerformanceLaunchState.launched);
expect(stateMachine.state.isFalseStart, isTrue);
expect(find.byKey(const ValueKey('speed_readout')), findsOneWidget);
expect(find.byKey(const ValueKey('elapsed_timer_readout')), findsOneWidget);
// Interim sample while timing
telemetryController.add(makeSample(speed: 35.0, monotonicMs: 5000));
await tester.pump(const Duration(milliseconds: 20));
expect(stateMachine.state.state, PerformanceLaunchState.launched);
// Reaches 60 at t=6950ms // Reaches 60 at t=6950ms
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 6950)); telemetryController.add(makeSample(speed: 62.0, monotonicMs: 6950));
await tester.pump(const Duration(milliseconds: 20)); await tester.pump(const Duration(milliseconds: 20));
@ -426,6 +437,7 @@ void main() {
expect(stateMachine.state.state, PerformanceLaunchState.completed); expect(stateMachine.state.state, PerformanceLaunchState.completed);
expect(stateMachine.state.lastResult!.isFalseStart, isTrue); expect(stateMachine.state.lastResult!.isFalseStart, isTrue);
expect(stateMachine.state.lastResult!.accelerationTimeSec, closeTo(3.20, 0.05));
// Verify false start badge and negative reaction // Verify false start badge and negative reaction
expect(find.text('FALSE START'), findsOneWidget); expect(find.text('FALSE START'), findsOneWidget);

View File

@ -167,9 +167,9 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('MOTORSIGNAL FREE'), findsOneWidget); expect(find.text('MotorSignal Free'), findsOneWidget);
expect(find.text('FREE'), findsOneWidget); expect(find.text('FREE'), findsOneWidget);
expect(find.text('UPGRADE TO PRO'), findsOneWidget); expect(find.text('Upgrade to Pro'), findsOneWidget);
expect( expect(
find.text('Basic telemetry & local recording active'), find.text('Basic telemetry & local recording active'),
findsOneWidget, findsOneWidget,
@ -193,8 +193,8 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('MOTORSIGNAL PRO'), findsOneWidget); expect(find.text('MotorSignal Pro'), findsOneWidget);
expect(find.text('MANAGE SUBSCRIPTION'), findsOneWidget); expect(find.text('Manage Subscription'), findsOneWidget);
expect(find.text('MONTHLY'), findsOneWidget); expect(find.text('MONTHLY'), findsOneWidget);
}); });
@ -214,7 +214,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('MOTORSIGNAL PRO'), findsOneWidget); expect(find.text('MotorSignal Pro'), findsOneWidget);
expect( expect(
find.text('Lifetime access • Permanent ownership'), find.text('Lifetime access • Permanent ownership'),
findsOneWidget, findsOneWidget,
@ -247,7 +247,7 @@ void main() {
find.text('Payment grace period • Action needed in Google Play'), find.text('Payment grace period • Action needed in Google Play'),
findsOneWidget, findsOneWidget,
); );
expect(find.text('MANAGE SUBSCRIPTION'), findsOneWidget); expect(find.text('Manage Subscription'), findsOneWidget);
}); });
testWidgets('renders on narrow viewport without overflow', (tester) async { testWidgets('renders on narrow viewport without overflow', (tester) async {

View File

@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:motorcycle_dash/core/theme/theme.dart';
import 'package:motorcycle_dash/features/social/social_hub_screen.dart';
import 'package:motorcycle_dash/features/social/widgets/avatar_badge.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
Widget buildTestSocialHub({
ThemeData? theme,
Size size = const Size(400, 800),
GoRouter? router,
}) {
final testRouter = router ??
GoRouter(
initialLocation: '/social',
routes: [
GoRoute(
path: '/social',
builder: (context, state) => const SocialHubScreen(),
routes: [
GoRoute(
path: 'profile',
builder: (context, state) => const Scaffold(body: Center(child: Text('Profile Target'))),
),
GoRoute(
path: 'friends',
builder: (context, state) => const Scaffold(body: Center(child: Text('Friends Target'))),
),
],
),
GoRoute(
path: '/group-rides',
builder: (context, state) => const Scaffold(body: Center(child: Text('Group Rides Target'))),
),
GoRoute(
path: '/garage',
builder: (context, state) => const Scaffold(body: Center(child: Text('Garage Target'))),
),
],
);
return ProviderScope(
child: MaterialApp.router(
theme: theme ?? AppTheme.darkTheme,
routerConfig: testRouter,
),
);
}
group('SocialHubScreen Widget Tests', () {
testWidgets('Renders Rider Hero Card with avatar and handle', (tester) async {
await tester.pumpWidget(buildTestSocialHub());
await tester.pumpAndSettle();
expect(find.text('Social'), findsOneWidget);
expect(find.text('Rider'), findsOneWidget);
expect(find.byType(AvatarBadge), findsOneWidget);
});
testWidgets('Renders all social destination cards', (tester) async {
await tester.pumpWidget(buildTestSocialHub());
await tester.pumpAndSettle();
expect(find.text('Rider Profile'), findsOneWidget);
expect(find.text('Friends'), findsOneWidget);
expect(find.text('Group Rides'), findsOneWidget);
});
testWidgets('Navigates to Rider Profile when profile card is tapped', (tester) async {
await tester.pumpWidget(buildTestSocialHub());
await tester.pumpAndSettle();
await tester.tap(find.text('Rider Profile'));
await tester.pumpAndSettle();
expect(find.text('Profile Target'), findsOneWidget);
});
testWidgets('Navigates to Friends when friends card is tapped', (tester) async {
await tester.pumpWidget(buildTestSocialHub());
await tester.pumpAndSettle();
await tester.tap(find.text('Friends'));
await tester.pumpAndSettle();
expect(find.text('Friends Target'), findsOneWidget);
});
testWidgets('Navigates to Group Rides when group rides card is tapped', (tester) async {
await tester.pumpWidget(buildTestSocialHub());
await tester.pumpAndSettle();
await tester.tap(find.text('Group Rides'));
await tester.pumpAndSettle();
expect(find.text('Group Rides Target'), findsOneWidget);
});
testWidgets('Renders cleanly under Light Theme without errors', (tester) async {
await tester.pumpWidget(buildTestSocialHub(theme: AppTheme.lightTheme));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(find.text('Social'), findsOneWidget);
});
testWidgets('Adapts to multiple responsive viewports without overflow', (tester) async {
const viewports = [
Size(320, 640),
Size(412, 915),
Size(800, 360),
Size(1024, 768),
];
for (final vp in viewports) {
tester.view.physicalSize = vp;
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
await tester.pumpWidget(buildTestSocialHub(size: vp));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull, reason: 'No overflow on $vp');
expect(find.text('Social'), findsOneWidget);
}
});
});
}

View File

@ -13,13 +13,13 @@ 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(0xFF00E5FF)); expect(AppColors.cyberCyan, const Color(0xFF00B4D8));
expect(AppColors.velocityBlue, const Color(0xFF2E7DFF)); expect(AppColors.velocityBlue, const Color(0xFF0096C7));
expect(AppColors.raceRed, const Color(0xFFFF2A2A)); expect(AppColors.raceRed, const Color(0xFFFF3B30));
expect(AppColors.darkBackground, const Color(0xFF0A0A0A)); expect(AppColors.darkBackground, const Color(0xFF0D0F13));
expect(AppColors.darkSurface, const Color(0xFF0D0D0D)); expect(AppColors.darkSurface, const Color(0xFF13171E));
expect(AppColors.lightBackground, const Color(0xFFF0F0F0)); expect(AppColors.lightBackground, const Color(0xFFF3F5F8));
expect(AppColors.lightSurface, const Color(0xFFF5F5F5)); expect(AppColors.lightSurface, const Color(0xFFE9EDF2));
}); });
test('AppTypography specifies authoritative multi-font system', () { test('AppTypography specifies authoritative multi-font system', () {
@ -61,7 +61,7 @@ void main() {
const light = MotorSignalThemeColors.light; const light = MotorSignalThemeColors.light;
expect(light.cyberCyan, AppColors.technicalBlue); expect(light.cyberCyan, AppColors.technicalBlue);
expect(light.launchGreen, AppColors.successGreen); expect(light.launchGreen, AppColors.launchGreen);
expect(light.surfaceContainer, AppColors.lightSurfaceContainer); expect(light.surfaceContainer, AppColors.lightSurfaceContainer);
}); });