diff --git a/lib/core/router.dart b/lib/core/router.dart index 020a811..30efd13 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../features/dashboard/dashboard_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/diagnostics_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/account_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/friends_screen.dart'; import '../features/group_rides/group_rides_hub_screen.dart'; @@ -88,16 +88,14 @@ final appRouter = GoRouter( StatefulShellBranch( routes: [ GoRoute( - path: '/history', - builder: (context, state) => const HistoryScreen(), - ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: '/records', - builder: (context, state) => const RecordsScreen(), + path: '/activity', + builder: (context, state) { + final tabParam = state.uri.queryParameters['tab']; + final tab = tabParam == 'records' + ? ActivityTab.records + : ActivityTab.rides; + return ActivityScreen(initialTab: tab); + }, ), ], ), @@ -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( routes: [ 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( path: '/group-rides', builder: (context, state) => const GroupRidesHubScreen(), diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index c94214c..8489187 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; /// Authoritative semantic color tokens for MotorSignal. /// @@ -6,41 +6,41 @@ /// - Dark Mode (Cockpit OLED): deep matte blacks and vibrant saturated accents. /// - Light Mode (Daylight HUD): high-contrast daylight surfaces for outdoor sunlight readability. abstract final class AppColors { - // Brand & High-Contrast Accents - static const Color cyberCyan = Color(0xFF00E5FF); - static const Color technicalBlue = Color(0xFF124580); - static const Color velocityBlue = Color(0xFF2E7DFF); - static const Color raceRed = Color(0xFFFF2A2A); + // Brand & High-Contrast Accents (Unified MotorSignal Palette) + static const Color cyberCyan = Color(0xFF00B4D8); + static const Color technicalBlue = Color(0xFF0077A8); + static const Color velocityBlue = Color(0xFF0096C7); + static const Color raceRed = Color(0xFFFF3B30); static const Color hazardRed = Color(0xFFFF3B30); static const Color launchGreen = Color(0xFF34C759); - static const Color telemetryGreen = Color(0xFF69F0AE); - static const Color successGreen = Color(0xFF008547); - static const Color warningAmber = Color(0xFFFFD740); + static const Color telemetryGreen = Color(0xFF30D158); + static const Color successGreen = Color(0xFF28A745); + static const Color warningAmber = Color(0xFFFFB300); static const Color darkAmber = Color(0xFFC48100); - // Dark Mode Palette (Cockpit OLED) - static const Color darkBackground = Color(0xFF0A0A0A); - static const Color darkSurface = Color(0xFF0D0D0D); - static const Color darkSurfaceContainer = Color(0xFF141414); - static const Color darkSurfaceContainerHigh = Color(0xFF1E1E1E); - static const Color darkSurfaceContainerHighest = Color(0xFF282828); - static const Color darkOutline = Color(0xFF2A2A2A); - static const Color darkOutlineVariant = Color(0xFF1F1F1F); - static const Color darkOnSurface = Color(0xFFFFFFFF); - static const Color darkOnSurfaceVariant = Color(0xFFB3B3B3); - static const Color darkPrimary = Color(0xFF00E5FF); - static const Color darkSecondary = Color(0xFFFF2A2A); + // Dark Mode Palette (Graphite & Technical Cockpit) + static const Color darkBackground = Color(0xFF0D0F13); + static const Color darkSurface = Color(0xFF13171E); + static const Color darkSurfaceContainer = Color(0xFF1A1F28); + static const Color darkSurfaceContainerHigh = Color(0xFF222935); + static const Color darkSurfaceContainerHighest = Color(0xFF2B3342); + static const Color darkOutline = Color(0xFF2B3342); + static const Color darkOutlineVariant = Color(0xFF1F242F); + static const Color darkOnSurface = Color(0xFFF1F3F5); + static const Color darkOnSurfaceVariant = Color(0xFF9EABB8); + static const Color darkPrimary = Color(0xFF00B4D8); + static const Color darkSecondary = Color(0xFFFF3B30); - // Light Mode Palette (Daylight HUD) - static const Color lightBackground = Color(0xFFF0F0F0); - static const Color lightSurface = Color(0xFFF5F5F5); + // Light Mode Palette (Daylight Technical HUD) + static const Color lightBackground = Color(0xFFF3F5F8); + static const Color lightSurface = Color(0xFFE9EDF2); static const Color lightSurfaceContainer = Color(0xFFFFFFFF); - static const Color lightSurfaceContainerHigh = Color(0xFFEAEAEA); - static const Color lightSurfaceContainerHighest = Color(0xFFDFDFDF); - static const Color lightOutline = Color(0xFFD1D1D1); - static const Color lightOutlineVariant = Color(0xFFE0E0E0); - static const Color lightOnSurface = Color(0xFF1A1C1C); - static const Color lightOnSurfaceVariant = Color(0xFF555555); - static const Color lightPrimary = Color(0xFF124580); + static const Color lightSurfaceContainerHigh = Color(0xFFE1E6EC); + static const Color lightSurfaceContainerHighest = Color(0xFFD6DCE4); + static const Color lightOutline = Color(0xFFCBD2DC); + static const Color lightOutlineVariant = Color(0xFFDFE4EC); + static const Color lightOnSurface = Color(0xFF13171E); + static const Color lightOnSurfaceVariant = Color(0xFF5A6675); + static const Color lightPrimary = Color(0xFF0077A8); static const Color lightSecondary = Color(0xFFD32F2F); } diff --git a/lib/core/theme/app_typography.dart b/lib/core/theme/app_typography.dart index 3df19e2..7aca780 100644 --- a/lib/core/theme/app_typography.dart +++ b/lib/core/theme/app_typography.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; /// Authoritative typography tokens for MotorSignal. /// @@ -38,28 +38,28 @@ abstract final class AppTypography { // Headlines static const TextStyle headingCompass = TextStyle( fontFamily: fontHeadline, - fontSize: 28, - fontWeight: FontWeight.w800, - letterSpacing: 0.05 * 28, + fontSize: 24, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, ); static const TextStyle headlineLg = TextStyle( fontFamily: fontHeadline, fontSize: 24, - fontWeight: FontWeight.w800, - letterSpacing: 0.5, + fontWeight: FontWeight.w600, + letterSpacing: -0.2, ); static const TextStyle headlineMd = TextStyle( fontFamily: fontHeadline, fontSize: 18, - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w600, ); static const TextStyle headlineSm = TextStyle( fontFamily: fontHeadline, fontSize: 15, - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w600, ); // Telemetry Labels & Readouts @@ -71,16 +71,16 @@ abstract final class AppTypography { static const TextStyle telemetryLabel = TextStyle( fontFamily: fontBody, - fontSize: 11, - fontWeight: FontWeight.w800, - letterSpacing: 1.2, + fontSize: 10, + fontWeight: FontWeight.w600, + letterSpacing: 0.8, ); static const TextStyle badgeLabel = TextStyle( fontFamily: fontTelemetry, fontSize: 11, - fontWeight: FontWeight.w800, - letterSpacing: 1.0, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, ); // Body text @@ -108,7 +108,7 @@ abstract final class AppTypography { static const TextStyle button = TextStyle( fontFamily: fontHeadline, fontSize: 14, - fontWeight: FontWeight.w700, - letterSpacing: 1.0, + fontWeight: FontWeight.w600, + letterSpacing: 0.4, ); } diff --git a/lib/core/theme/theme.dart b/lib/core/theme/theme.dart index 2268a8b..c506112 100644 --- a/lib/core/theme/theme.dart +++ b/lib/core/theme/theme.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'app_colors.dart'; import 'app_spacing.dart'; import 'app_typography.dart'; @@ -49,15 +49,15 @@ class AppTheme { margin: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: AppRadius.lgBorderRadius, - side: const BorderSide(color: AppColors.darkOutline, width: 1), + side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8), ), ), dialogTheme: DialogThemeData( backgroundColor: AppColors.darkSurfaceContainerHigh, - elevation: 8, + elevation: 6, shape: RoundedRectangleBorder( 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), contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurfaceVariant), @@ -65,7 +65,7 @@ class AppTheme { bottomSheetTheme: BottomSheetThemeData( backgroundColor: AppColors.darkSurfaceContainerHigh, modalBackgroundColor: AppColors.darkSurfaceContainerHigh, - elevation: 16, + elevation: 12, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), ), @@ -79,19 +79,19 @@ class AppTheme { ), side: WidgetStateProperty.resolveWith((states) { 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((states) { if (states.contains(WidgetState.selected)) { - return AppColors.cyberCyan.withValues(alpha: 0.15); + return AppColors.darkPrimary.withValues(alpha: 0.12); } return AppColors.darkSurfaceContainer; }), foregroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { - return AppColors.cyberCyan; + return AppColors.darkPrimary; } return AppColors.darkOnSurfaceVariant; }), @@ -101,7 +101,7 @@ class AppTheme { filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: AppColors.darkPrimary, - foregroundColor: const Color(0xFF000000), + foregroundColor: const Color(0xFF0D0F13), minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius), textStyle: AppTypography.button, @@ -114,12 +114,12 @@ class AppTheme { minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius), textStyle: AppTypography.button, - elevation: 2, + elevation: 1, ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - foregroundColor: AppColors.cyberCyan, + foregroundColor: AppColors.darkPrimary, minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), side: const BorderSide(color: AppColors.darkOutline, width: 1), shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius), @@ -128,7 +128,7 @@ class AppTheme { ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( - foregroundColor: AppColors.cyberCyan, + foregroundColor: AppColors.darkPrimary, minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), textStyle: AppTypography.button, ), @@ -175,20 +175,20 @@ class AppTheme { ), navigationBarTheme: NavigationBarThemeData( backgroundColor: AppColors.darkSurface, - indicatorColor: AppColors.cyberCyan.withValues(alpha: 0.15), + indicatorColor: AppColors.darkPrimary.withValues(alpha: 0.14), elevation: 0, labelTextStyle: WidgetStateProperty.resolveWith((states) { 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); }), ), navigationRailTheme: NavigationRailThemeData( backgroundColor: AppColors.darkSurface, - indicatorColor: AppColors.cyberCyan.withValues(alpha: 0.15), + indicatorColor: AppColors.darkPrimary.withValues(alpha: 0.14), elevation: 0, - selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.cyberCyan), + selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkPrimary), unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith(color: AppColors.darkOnSurfaceVariant), ), snackBarTheme: SnackBarThemeData( @@ -196,7 +196,7 @@ class AppTheme { contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.darkOnSurface), shape: RoundedRectangleBorder( borderRadius: AppRadius.mdBorderRadius, - side: const BorderSide(color: AppColors.darkOutline, width: 1), + side: const BorderSide(color: AppColors.darkOutlineVariant, width: 0.8), ), behavior: SnackBarBehavior.floating, ), @@ -246,15 +246,15 @@ class AppTheme { margin: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: AppRadius.lgBorderRadius, - side: const BorderSide(color: AppColors.lightOutline, width: 1), + side: const BorderSide(color: AppColors.lightOutlineVariant, width: 0.8), ), ), dialogTheme: DialogThemeData( backgroundColor: AppColors.lightSurfaceContainer, - elevation: 8, + elevation: 6, shape: RoundedRectangleBorder( 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), contentTextStyle: AppTypography.bodyMd.copyWith(color: AppColors.lightOnSurfaceVariant), @@ -262,7 +262,7 @@ class AppTheme { bottomSheetTheme: BottomSheetThemeData( backgroundColor: AppColors.lightSurfaceContainer, modalBackgroundColor: AppColors.lightSurfaceContainer, - elevation: 16, + elevation: 12, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xl)), ), @@ -276,13 +276,13 @@ class AppTheme { ), side: WidgetStateProperty.resolveWith((states) { 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((states) { if (states.contains(WidgetState.selected)) { - return AppColors.lightPrimary.withValues(alpha: 0.12); + return AppColors.lightPrimary.withValues(alpha: 0.10); } return AppColors.lightSurfaceContainer; }), @@ -311,7 +311,7 @@ class AppTheme { minimumSize: const Size(AppSpacing.minTouchTarget, AppSpacing.minTouchTarget), shape: RoundedRectangleBorder(borderRadius: AppRadius.mdBorderRadius), textStyle: AppTypography.button, - elevation: 2, + elevation: 1, ), ), outlinedButtonTheme: OutlinedButtonThemeData( diff --git a/lib/core/theme/theme_extensions.dart b/lib/core/theme/theme_extensions.dart index ef82cef..12afdf5 100644 --- a/lib/core/theme/theme_extensions.dart +++ b/lib/core/theme/theme_extensions.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'app_colors.dart'; /// Semantic ThemeExtension for MotorSignal telemetry, indicators, and UI surfaces. @@ -44,48 +44,48 @@ class MotorSignalThemeColors extends ThemeExtension { required this.glowShadow, }); - /// Dark Mode default telemetry tokens (Cockpit OLED). + /// Dark Mode default telemetry tokens (Cockpit Precision). static const MotorSignalThemeColors dark = MotorSignalThemeColors( - cyberCyan: AppColors.cyberCyan, + cyberCyan: AppColors.darkPrimary, velocityBlue: AppColors.velocityBlue, raceRed: AppColors.raceRed, launchGreen: AppColors.launchGreen, warningAmber: AppColors.warningAmber, telemetryGreen: AppColors.telemetryGreen, - leanAngleLeft: AppColors.cyberCyan, + leanAngleLeft: AppColors.darkPrimary, leanAngleRight: AppColors.velocityBlue, gForcePositive: AppColors.launchGreen, gForceNegative: AppColors.raceRed, - speedometerDial: AppColors.cyberCyan, - speedometerGlow: Color(0x3300E5FF), + speedometerDial: AppColors.darkPrimary, + speedometerGlow: Color(0x1400B4D8), surfaceContainer: AppColors.darkSurfaceContainer, surfaceContainerHigh: AppColors.darkSurfaceContainerHigh, surfaceContainerHighest: AppColors.darkSurfaceContainerHighest, outline: AppColors.darkOutline, outlineVariant: AppColors.darkOutlineVariant, - glowShadow: Color(0x6600E5FF), + glowShadow: Color(0x1F00B4D8), ); /// Light Mode default telemetry tokens (Daylight HUD). static const MotorSignalThemeColors light = MotorSignalThemeColors( - cyberCyan: AppColors.technicalBlue, + cyberCyan: AppColors.lightPrimary, velocityBlue: AppColors.velocityBlue, raceRed: AppColors.lightSecondary, - launchGreen: AppColors.successGreen, - warningAmber: AppColors.darkAmber, - telemetryGreen: AppColors.successGreen, - leanAngleLeft: AppColors.technicalBlue, + launchGreen: AppColors.launchGreen, + warningAmber: AppColors.warningAmber, + telemetryGreen: AppColors.telemetryGreen, + leanAngleLeft: AppColors.lightPrimary, leanAngleRight: AppColors.velocityBlue, - gForcePositive: AppColors.successGreen, + gForcePositive: AppColors.launchGreen, gForceNegative: AppColors.lightSecondary, - speedometerDial: AppColors.technicalBlue, - speedometerGlow: Color(0x1F124580), + speedometerDial: AppColors.lightPrimary, + speedometerGlow: Color(0x0F0077A8), surfaceContainer: AppColors.lightSurfaceContainer, surfaceContainerHigh: AppColors.lightSurfaceContainerHigh, surfaceContainerHighest: AppColors.lightSurfaceContainerHighest, outline: AppColors.lightOutline, outlineVariant: AppColors.lightOutlineVariant, - glowShadow: Color(0x22000000), + glowShadow: Color(0x14000000), ); @override diff --git a/lib/core/widgets/main_layout.dart b/lib/core/widgets/main_layout.dart index d9573b8..cf95536 100644 --- a/lib/core/widgets/main_layout.dart +++ b/lib/core/widgets/main_layout.dart @@ -39,24 +39,26 @@ const List kAppNavDestinations = [ railKey: ValueKey('rail_map_icon'), ), AppNavDestination( - icon: Icons.history, - label: 'History', - navKey: ValueKey('nav_history_icon'), - railKey: ValueKey('rail_history_icon'), + icon: Icons.show_chart_rounded, + selectedIcon: Icons.show_chart, + label: 'Activity', + navKey: ValueKey('nav_activity_icon'), + railKey: ValueKey('rail_activity_icon'), ), AppNavDestination( - icon: Icons.emoji_events_outlined, - selectedIcon: Icons.emoji_events, - label: 'Records', - navKey: ValueKey('nav_records_icon'), - railKey: ValueKey('rail_records_icon'), - ), - AppNavDestination( - icon: Icons.two_wheeler, + icon: Icons.two_wheeler_outlined, + selectedIcon: Icons.two_wheeler, label: 'Garage', navKey: ValueKey('nav_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( icon: Icons.settings_outlined, selectedIcon: Icons.settings, @@ -142,29 +144,30 @@ class _MainLayoutState extends ConsumerState { selectedIndex: selectedIndex, onDestinationSelected: _onDestinationSelected, backgroundColor: colorScheme.surface, - indicatorColor: colorScheme.primary.withValues(alpha: 0.18), + indicatorColor: colorScheme.primary.withValues(alpha: 0.14), selectedIconTheme: IconThemeData( color: colorScheme.primary, size: 20, ), unselectedIconTheme: IconThemeData( color: colorScheme.onSurfaceVariant, - size: 18, + size: 19, ), - selectedLabelTextStyle: AppTypography.telemetryLabel.copyWith( - color: colorScheme.primary, - fontSize: 9, - fontWeight: FontWeight.w800, - ), - unselectedLabelTextStyle: AppTypography.telemetryLabel.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 8.5, + selectedLabelTextStyle: const TextStyle( + fontFamily: AppTypography.fontHeadline, + fontSize: 9.5, fontWeight: FontWeight.w600, ), + unselectedLabelTextStyle: TextStyle( + fontFamily: AppTypography.fontHeadline, + fontSize: 9.0, + fontWeight: FontWeight.w500, + color: colorScheme.onSurfaceVariant, + ), labelType: NavigationRailLabelType.all, destinations: kAppNavDestinations.map((dest) { return NavigationRailDestination( - padding: const EdgeInsets.symmetric(vertical: 2), + padding: const EdgeInsets.symmetric(vertical: 3), icon: Icon(dest.icon, key: dest.railKey), selectedIcon: dest.selectedIcon != null ? Icon(dest.selectedIcon, key: dest.railKey) @@ -198,14 +201,20 @@ class _MainLayoutState extends ConsumerState { unselectedItemColor: colorScheme.onSurfaceVariant, backgroundColor: colorScheme.surface, type: BottomNavigationBarType.fixed, - selectedLabelStyle: AppTypography.telemetryLabel.copyWith( + iconSize: 20, + selectedFontSize: 10, + unselectedFontSize: 9.5, + selectedLabelStyle: const TextStyle( + fontFamily: AppTypography.fontHeadline, fontSize: 10, - fontWeight: FontWeight.w800, - ), - unselectedLabelStyle: AppTypography.telemetryLabel.copyWith( - fontSize: 9.5, fontWeight: FontWeight.w600, ), + unselectedLabelStyle: TextStyle( + fontFamily: AppTypography.fontHeadline, + fontSize: 9.5, + fontWeight: FontWeight.w500, + color: colorScheme.onSurfaceVariant, + ), items: kAppNavDestinations.map((dest) { return BottomNavigationBarItem( icon: Icon(dest.icon, key: dest.navKey), diff --git a/lib/features/activity/activity_screen.dart b/lib/features/activity/activity_screen.dart new file mode 100644 index 0000000..db10334 --- /dev/null +++ b/lib/features/activity/activity_screen.dart @@ -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 createState() => _ActivityScreenState(); +} + +class _ActivityScreenState extends ConsumerState { + 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( + segments: const [ + ButtonSegment( + value: ActivityTab.rides, + icon: Icon(Icons.history, size: 16), + label: Text( + 'Rides', + style: TextStyle( + fontFamily: AppTypography.fontHeadline, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ButtonSegment( + 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((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((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), + ], + ), + ); + } +} diff --git a/lib/features/dashboard/widgets/gps_status_indicator.dart b/lib/features/dashboard/widgets/gps_status_indicator.dart index 813e803..635e4d3 100644 --- a/lib/features/dashboard/widgets/gps_status_indicator.dart +++ b/lib/features/dashboard/widgets/gps_status_indicator.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_typography.dart'; @@ -22,6 +22,8 @@ class GPSStatusIndicator extends ConsumerWidget { telemetryStreamProvider.select((s) => s.value?.gpsAccuracy), ); + final theme = Theme.of(context); + final IconData icon; final Color color; final String label; @@ -29,7 +31,7 @@ class GPSStatusIndicator extends ConsumerWidget { if (gpsAcc != null) { if (gpsAcc <= 15.0) { icon = Icons.gps_fixed; - color = AppColors.cyberCyan; + color = AppColors.telemetryGreen; label = 'GPS 3D FIX'; } else if (gpsAcc <= 35.0) { icon = Icons.gps_fixed; @@ -42,7 +44,7 @@ class GPSStatusIndicator extends ConsumerWidget { } } else { icon = Icons.gps_off; - color = Colors.white38; + color = theme.colorScheme.onSurfaceVariant; label = 'NO GPS'; } diff --git a/lib/features/dashboard/widgets/heading_indicator.dart b/lib/features/dashboard/widgets/heading_indicator.dart index ea246f9..a88982d 100644 --- a/lib/features/dashboard/widgets/heading_indicator.dart +++ b/lib/features/dashboard/widgets/heading_indicator.dart @@ -38,8 +38,8 @@ class HeadingIndicator extends ConsumerWidget { headingStr, style: TextStyle( fontFamily: AppTypography.fontHeadline, - fontSize: 16, - fontWeight: FontWeight.w800, + fontSize: 14, + fontWeight: FontWeight.w700, color: cardinalColor, ), ), @@ -48,8 +48,8 @@ class HeadingIndicator extends ConsumerWidget { '${heading.toStringAsFixed(0)}°', style: TextStyle( fontFamily: AppTypography.fontTelemetry, - fontSize: 12, - fontWeight: FontWeight.w700, + fontSize: 11, + fontWeight: FontWeight.w600, color: degreesColor, ), ), @@ -66,18 +66,18 @@ class HeadingIndicator extends ConsumerWidget { headingStr, style: TextStyle( fontFamily: AppTypography.fontHeadline, - fontSize: 26, - fontWeight: FontWeight.w900, + fontSize: 20, + fontWeight: FontWeight.w700, color: cardinalColor, ), ), - const SizedBox(width: 6), + const SizedBox(width: 5), Text( '${heading.toStringAsFixed(0)}°', style: TextStyle( fontFamily: AppTypography.fontTelemetry, - fontSize: 16, - fontWeight: FontWeight.w700, + fontSize: 14, + fontWeight: FontWeight.w600, color: degreesColor, ), ), diff --git a/lib/features/dashboard/widgets/ride_action_button.dart b/lib/features/dashboard/widgets/ride_action_button.dart index 7f6db17..450db7c 100644 --- a/lib/features/dashboard/widgets/ride_action_button.dart +++ b/lib/features/dashboard/widgets/ride_action_button.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_typography.dart'; @@ -31,11 +31,11 @@ class RideActionButton extends ConsumerWidget { if (isRiding) { buttonColor = AppColors.raceRed; - label = 'STOP SESSION'; + label = 'Stop Session'; icon = Icons.stop_circle_outlined; } else { buttonColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary; - label = 'START SESSION'; + label = 'Start Session'; icon = Icons.play_circle_outlined; } @@ -46,7 +46,7 @@ class RideActionButton extends ConsumerWidget { style: ElevatedButton.styleFrom( backgroundColor: buttonColor.withValues(alpha: isRiding ? 0.16 : 0.12), foregroundColor: buttonColor, - side: BorderSide(color: buttonColor, width: 2.0), + side: BorderSide(color: buttonColor, width: 1.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), @@ -66,8 +66,8 @@ class RideActionButton extends ConsumerWidget { style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: compact ? 15 : 18, - fontWeight: FontWeight.w800, - letterSpacing: 1.5, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, ), ), ), diff --git a/lib/features/dashboard/widgets/ride_status_chip.dart b/lib/features/dashboard/widgets/ride_status_chip.dart index f7de2c1..5e639e2 100644 --- a/lib/features/dashboard/widgets/ride_status_chip.dart +++ b/lib/features/dashboard/widgets/ride_status_chip.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_typography.dart'; @@ -20,6 +20,9 @@ class RideStatusChip extends ConsumerWidget { final isRecording = state == RideState.riding; final isPaused = state == RideState.paused; + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final Color badgeColor; final String label; @@ -30,10 +33,10 @@ class RideStatusChip extends ConsumerWidget { badgeColor = AppColors.warningAmber; label = 'PAUSED'; } else if (state == RideState.ready) { - badgeColor = AppColors.cyberCyan; + badgeColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary; label = 'READY'; } else { - badgeColor = Colors.white54; + badgeColor = theme.colorScheme.onSurfaceVariant; label = 'STANDBY'; } diff --git a/lib/features/dashboard/widgets/speed_readout.dart b/lib/features/dashboard/widgets/speed_readout.dart index 207cd8e..79203a3 100644 --- a/lib/features/dashboard/widgets/speed_readout.dart +++ b/lib/features/dashboard/widgets/speed_readout.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_typography.dart'; @@ -87,14 +87,6 @@ class SpeedReadout extends ConsumerWidget { fontStyle: FontStyle.italic, height: 0.95, color: speedColor, - shadows: isDark - ? [ - Shadow( - color: AppColors.cyberCyan.withValues(alpha: 0.4), - blurRadius: 16, - ), - ] - : null, ), ), ), diff --git a/lib/features/garage/garage_screen.dart b/lib/features/garage/garage_screen.dart index d8871c5..6ab351b 100644 --- a/lib/features/garage/garage_screen.dart +++ b/lib/features/garage/garage_screen.dart @@ -82,8 +82,8 @@ class GarageScreen extends ConsumerWidget { foregroundColor: Colors.black, icon: const Icon(Icons.two_wheeler), label: const Text( - 'ADD MOTORCYCLE', - style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.8), + 'Add Motorcycle', + style: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.2), ), onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value), ) @@ -127,12 +127,12 @@ class GarageScreen extends ConsumerWidget { ), const SizedBox(height: 24), Text( - 'YOUR GARAGE IS EMPTY', + 'Your Garage is Empty', style: TextStyle( fontFamily: 'Sora', fontSize: 18, - fontWeight: FontWeight.w900, - letterSpacing: 1.2, + fontWeight: FontWeight.w700, + letterSpacing: 0.2, color: theme.colorScheme.onSurface, ), ), @@ -156,15 +156,15 @@ class GarageScreen extends ConsumerWidget { vertical: 14, ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(10), ), ), icon: const Icon(Icons.add), label: const Text( - 'ADD MOTORCYCLE', + 'Add Motorcycle', style: TextStyle( - fontWeight: FontWeight.bold, - letterSpacing: 1.2, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, ), ), onPressed: () => context.push('/garage/add'), @@ -204,9 +204,9 @@ class GarageScreen extends ConsumerWidget { borderRadius: BorderRadius.circular(14), border: Border.all( color: bike.isDefault - ? colors.cyberCyan + ? colors.cyberCyan.withValues(alpha: 0.6) : colors.outlineVariant, - width: bike.isDefault ? 1.8 : 1.0, + width: bike.isDefault ? 1.0 : 0.8, ), ), child: Material( @@ -242,7 +242,7 @@ class GarageScreen extends ConsumerWidget { style: TextStyle( fontFamily: 'Sora', fontSize: 16, - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w700, color: theme.colorScheme.onSurface, ), ), @@ -251,23 +251,24 @@ class GarageScreen extends ConsumerWidget { Container( padding: const EdgeInsets.symmetric( horizontal: 8, - vertical: 2, + vertical: 3, ), decoration: BoxDecoration( - color: colors.cyberCyan.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(4), + color: colors.cyberCyan.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(6), border: Border.all( - color: colors.cyberCyan, + color: colors.cyberCyan.withValues(alpha: 0.4), + width: 0.8, ), ), child: Text( - 'CURRENT', + 'Current', style: TextStyle( fontFamily: 'Inter', fontSize: 10, - fontWeight: FontWeight.w900, + fontWeight: FontWeight.w600, color: colors.cyberCyan, - letterSpacing: 0.5, + letterSpacing: 0.3, ), ), ), @@ -282,7 +283,7 @@ class GarageScreen extends ConsumerWidget { fontFamily: 'JetBrains Mono', color: colors.cyberCyan, fontSize: 12.5, - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w600, ), ), ], diff --git a/lib/features/history/history_screen.dart b/lib/features/history/history_screen.dart index 1f47906..fd12c66 100644 --- a/lib/features/history/history_screen.dart +++ b/lib/features/history/history_screen.dart @@ -9,7 +9,12 @@ import '../garage/garage_providers.dart'; import 'ride_detail_screen.dart'; class HistoryScreen extends ConsumerStatefulWidget { - const HistoryScreen({super.key}); + final bool isEmbedded; + + const HistoryScreen({ + super.key, + this.isEmbedded = false, + }); @override ConsumerState createState() => _HistoryScreenState(); @@ -26,12 +31,14 @@ class _HistoryScreenState extends ConsumerState { final colors = context.motorColors; return Scaffold( - appBar: AppBar( - title: const Text( - 'Ride History', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), + appBar: widget.isEmbedded + ? null + : AppBar( + title: const Text( + 'Ride History', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), body: Column( children: [ _buildFilterBar(context, garageBikesAsync.value ?? [], defaultBike), diff --git a/lib/features/performance/performance_launch_screen.dart b/lib/features/performance/performance_launch_screen.dart index 460b664..2b67e86 100644 --- a/lib/features/performance/performance_launch_screen.dart +++ b/lib/features/performance/performance_launch_screen.dart @@ -380,12 +380,12 @@ class _SetupView extends StatelessWidget { ), onPressed: onArm, child: const Text( - 'ARM LAUNCH', + 'Arm Launch', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 16, - fontWeight: FontWeight.w900, - letterSpacing: 1.2, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, ), ), ), @@ -541,7 +541,7 @@ class _WaitingView extends ConsumerWidget { onPressed: () { 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(); }, child: const Text( - 'START COUNTDOWN', + 'Start Countdown', style: TextStyle( fontFamily: AppTypography.fontHeadline, - fontSize: 17, - fontWeight: FontWeight.w900, - letterSpacing: 1.2, + fontSize: 16, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, ), ), ), @@ -773,7 +773,7 @@ class _CountdownViewState extends ConsumerState<_CountdownView> onPressed: () { ref.read(performanceLaunchProvider.notifier).cancel(); }, - child: const Text('ABORT'), + child: const Text('Abort'), ), ], ), @@ -1316,7 +1316,7 @@ class _ResultView extends ConsumerWidget { onPressed: () { 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(); } }, - child: const Text('DONE'), + child: const Text('Done'), ), ), ), @@ -1436,7 +1436,7 @@ class _AbortedView extends ConsumerWidget { onPressed: () { ref.read(performanceLaunchProvider.notifier).reset(); }, - child: const Text('TRY AGAIN'), + child: const Text('Try Again'), ), ), ], diff --git a/lib/features/records/records_screen.dart b/lib/features/records/records_screen.dart index 3bc87df..279c114 100644 --- a/lib/features/records/records_screen.dart +++ b/lib/features/records/records_screen.dart @@ -7,7 +7,12 @@ import '../../core/theme/theme_extensions.dart'; import 'records_service.dart'; class RecordsScreen extends ConsumerWidget { - const RecordsScreen({super.key}); + final bool isEmbedded; + + const RecordsScreen({ + super.key, + this.isEmbedded = false, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -15,19 +20,21 @@ class RecordsScreen extends ConsumerWidget { final colors = context.motorColors; return Scaffold( - appBar: AppBar( - title: const Text( - 'Personal Records', - style: TextStyle(fontWeight: FontWeight.bold), - ), - actions: [ - IconButton( - icon: Icon(Icons.emoji_events, color: colors.warningAmber), - tooltip: 'Trophies', - onPressed: () {}, - ), - ], - ), + appBar: isEmbedded + ? null + : AppBar( + title: const Text( + 'Personal Records', + style: TextStyle(fontWeight: FontWeight.bold), + ), + actions: [ + IconButton( + icon: Icon(Icons.emoji_events, color: colors.warningAmber), + tooltip: 'Trophies', + onPressed: () {}, + ), + ], + ), body: ResponsiveLayout( builder: (context, responsive) { final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape); @@ -42,11 +49,11 @@ class RecordsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionHeader('SPEED', colors), + _buildSectionHeader('Speed', colors), const SizedBox(height: 8), _buildHeroTopSpeedCard(context, records.topSpeedMph, colors), const SizedBox(height: 16), - _buildSectionHeader('PERFORMANCE TIMES', colors), + _buildSectionHeader('Performance Times', colors), const SizedBox(height: 8), _buildPerformanceTimesCard(context, records, colors), ], @@ -57,11 +64,11 @@ class RecordsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionHeader('HANDLING & DYNAMICS', colors), + _buildSectionHeader('Handling & Dynamics', colors), const SizedBox(height: 8), _buildHandlingCard(context, records, colors), const SizedBox(height: 16), - _buildSectionHeader('ENDURANCE & ROUTE', colors), + _buildSectionHeader('Endurance & Route', colors), const SizedBox(height: 8), _buildEnduranceCard(context, records, colors), ], @@ -77,19 +84,19 @@ class RecordsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionHeader('SPEED', colors), + _buildSectionHeader('Speed', colors), const SizedBox(height: 8), _buildHeroTopSpeedCard(context, records.topSpeedMph, colors), const SizedBox(height: 18), - _buildSectionHeader('PERFORMANCE TIMES', colors), + _buildSectionHeader('Performance Times', colors), const SizedBox(height: 8), _buildPerformanceTimesCard(context, records, colors), const SizedBox(height: 18), - _buildSectionHeader('HANDLING & DYNAMICS', colors), + _buildSectionHeader('Handling & Dynamics', colors), const SizedBox(height: 8), _buildHandlingCard(context, records, colors), const SizedBox(height: 18), - _buildSectionHeader('ENDURANCE & ROUTE', colors), + _buildSectionHeader('Endurance & Route', colors), const SizedBox(height: 8), _buildEnduranceCard(context, records, colors), const SizedBox(height: 24), @@ -246,12 +253,12 @@ class RecordsScreen extends ConsumerWidget { key: const ValueKey('btn_open_launch_mode'), icon: Icon(Icons.rocket_launch, size: 16, color: colors.launchGreen), label: Text( - 'PERFORMANCE LAUNCH MODE', + 'Performance Launch Mode', style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 12, - fontWeight: FontWeight.w800, - letterSpacing: 1.0, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, color: colors.launchGreen, ), ), diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index 1957e1b..8df29ac 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -97,8 +97,6 @@ class _SettingsScreenState extends ConsumerState { _buildSectionHeader('Account & Cloud Identity'), const SizedBox(height: 10), _buildAccountCard(context), - const SizedBox(height: 12), - _buildRiderSocialLinks(context), const SizedBox(height: 24), _buildSectionHeader('Appearance & Display Theme'), const SizedBox(height: 10), @@ -165,8 +163,6 @@ class _SettingsScreenState extends ConsumerState { _buildSectionHeader('Account & Cloud Identity'), const SizedBox(height: 10), _buildAccountCard(context), - const SizedBox(height: 12), - _buildRiderSocialLinks(context), const SizedBox(height: 24), _buildSectionHeader('Appearance & Display Theme'), const SizedBox(height: 10), @@ -208,12 +204,12 @@ class _SettingsScreenState extends ConsumerState { Widget _buildSectionHeader(String title) { return Text( - title.toUpperCase(), + title, style: TextStyle( fontFamily: 'Sora', - fontSize: 12, - fontWeight: FontWeight.w800, - letterSpacing: 0.8, + fontSize: 13, + fontWeight: FontWeight.w700, + letterSpacing: 0.2, color: context.motorColors.cyberCyan, ), ); @@ -310,97 +306,6 @@ class _SettingsScreenState extends ConsumerState { ); } - 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) { final colors = context.motorColors; return Card( diff --git a/lib/features/settings/widgets/current_plan_card.dart b/lib/features/settings/widgets/current_plan_card.dart index 4914418..dfb7415 100644 --- a/lib/features/settings/widgets/current_plan_card.dart +++ b/lib/features/settings/widgets/current_plan_card.dart @@ -117,24 +117,24 @@ class CurrentPlanCard extends ConsumerWidget { Icon( isPro ? Icons.verified : Icons.shield_outlined, color: isPro ? colors.cyberCyan : colors.outline, - size: 20, + size: 18, ), - const SizedBox(width: 10), + const SizedBox(width: 8), Text( - planTitle.toUpperCase(), + planTitle, style: TextStyle( fontFamily: 'Sora', fontSize: 13, - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w700, 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( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), decoration: BoxDecoration( color: badgeColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), @@ -148,7 +148,7 @@ class CurrentPlanCard extends ConsumerWidget { style: TextStyle( fontFamily: 'Inter', fontSize: 9, - fontWeight: FontWeight.w900, + fontWeight: FontWeight.w700, color: badgeColor, letterSpacing: 0.5, ), @@ -167,8 +167,8 @@ class CurrentPlanCard extends ConsumerWidget { ), const SizedBox(height: 16), Wrap( - spacing: 10, - runSpacing: 10, + spacing: 8, + runSpacing: 8, children: [ if (!isPro) ElevatedButton( @@ -176,19 +176,19 @@ class CurrentPlanCard extends ConsumerWidget { style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, - minimumSize: const Size(140, 48), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + minimumSize: const Size(130, 44), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(10), ), ), onPressed: () => context.push('/settings/pro'), child: const Text( - 'UPGRADE TO PRO', + 'Upgrade to Pro', style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w800, - letterSpacing: 0.5, + fontSize: 11.5, + fontWeight: FontWeight.w600, + letterSpacing: 0.2, ), ), ), @@ -197,18 +197,18 @@ class CurrentPlanCard extends ConsumerWidget { style: OutlinedButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.onSurface, side: BorderSide(color: colors.outlineVariant), - minimumSize: const Size(160, 48), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + minimumSize: const Size(140, 44), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(10), ), ), onPressed: _openGooglePlaySubscriptions, child: const Text( - 'MANAGE SUBSCRIPTION', + 'Manage Subscription', style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, + fontSize: 11.5, + fontWeight: FontWeight.w600, ), ), ), @@ -217,16 +217,16 @@ class CurrentPlanCard extends ConsumerWidget { style: OutlinedButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.onSurface, side: BorderSide(color: colors.outlineVariant), - minimumSize: const Size(100, 48), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + minimumSize: const Size(90, 44), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(10), ), ), onPressed: () => context.push('/settings/pro'), child: Text( - isPro ? 'VIEW PLANS' : 'DETAILS', - style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), + isPro ? 'View Plans' : 'Details', + style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.w600), ), ), ], diff --git a/lib/features/social/social_hub_screen.dart b/lib/features/social/social_hub_screen.dart new file mode 100644 index 0000000..5383719 --- /dev/null +++ b/lib/features/social/social_hub_screen.dart @@ -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, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/test/activity_screen_test.dart b/test/activity_screen_test.dart new file mode 100644 index 0000000..fc8e735 --- /dev/null +++ b/test/activity_screen_test.dart @@ -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 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> getYears() async => [MotorcycleYear(2024)]; + @override + Future> getMakes(int year) async => [MotorcycleMake(id: 'yamaha', name: 'Yamaha')]; + @override + Future> getModels(int year, String makeId) async => [MotorcycleModel(id: 'r1', name: 'YZF-R1', makeId: makeId)]; + @override + Future> getVariants(int year, String makeId, String modelId) async => [MotorcycleVariant(id: 'std', name: 'Standard')]; + @override + Future 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), 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)); + } + }); + }); +} diff --git a/test/canonical_dashboard_responsive_test.dart b/test/canonical_dashboard_responsive_test.dart index f802e42..30f64cc 100644 --- a/test/canonical_dashboard_responsive_test.dart +++ b/test/canonical_dashboard_responsive_test.dart @@ -118,7 +118,7 @@ void main() { expect(tester.takeException(), isNull); 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 { @@ -130,7 +130,7 @@ void main() { expect(tester.takeException(), isNull); 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.pump(); - expect(find.text('STOP SESSION'), findsOneWidget); + expect(find.text('Stop Session'), findsOneWidget); expect(find.text('RECORDING'), findsOneWidget); // Rotate to landscape @@ -231,7 +231,7 @@ void main() { await tester.pump(); // Verify ride did NOT stop or reset - expect(find.text('STOP SESSION'), findsOneWidget); + expect(find.text('Stop Session'), findsOneWidget); expect(find.text('RECORDING'), findsOneWidget); }); diff --git a/test/main_layout_responsive_test.dart b/test/main_layout_responsive_test.dart index b0d03b9..e01aefe 100644 --- a/test/main_layout_responsive_test.dart +++ b/test/main_layout_responsive_test.dart @@ -17,9 +17,9 @@ void main() { final router = GoRouter( initialLocation: switch (initialIndex) { 1 => '/map', - 2 => '/history', - 3 => '/records', - 4 => '/garage', + 2 => '/activity', + 3 => '/garage', + 4 => '/social', 5 => '/settings', _ => '/', }, @@ -52,19 +52,9 @@ void main() { StatefulShellBranch( routes: [ GoRoute( - path: '/history', + path: '/activity', builder: (context, state) => const Scaffold( - body: Center(child: Text('History Screen Content')), - ), - ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: '/records', - builder: (context, state) => const Scaffold( - body: Center(child: Text('Records Screen Content')), + body: Center(child: Text('Activity 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( routes: [ GoRoute( @@ -131,9 +131,9 @@ void main() { // Verify all 6 navigation labels exist regardless of layout expect(find.text('Dash'), findsOneWidget); expect(find.text('Map'), findsOneWidget); - expect(find.text('History'), findsOneWidget); - expect(find.text('Records'), findsOneWidget); + expect(find.text('Activity'), findsOneWidget); expect(find.text('Garage'), findsOneWidget); + expect(find.text('Social'), findsOneWidget); expect(find.text('Settings'), findsOneWidget); }); } @@ -223,10 +223,10 @@ void main() { expect(find.text('Map Screen Content'), findsOneWidget); - await tester.tap(find.text('Records')); + await tester.tap(find.text('Activity')); 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 { @@ -257,29 +257,29 @@ void main() { await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(400, 800))); await tester.pumpAndSettle(); - // Navigate to History - await tester.tap(find.text('History')); + // Navigate to Activity + await tester.tap(find.text('Activity')); await tester.pumpAndSettle(); - expect(find.text('History Screen Content'), findsOneWidget); + expect(find.text('Activity Screen Content'), findsOneWidget); // Rotate / Resize to short landscape (800x360) tester.view.physicalSize = const Size(800, 360); 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.text('History Screen Content'), findsOneWidget); + expect(find.text('Activity Screen Content'), findsOneWidget); // Resize to large tablet (1024x768) tester.view.physicalSize = const Size(1024, 768); await tester.pumpAndSettle(); - expect(find.text('History Screen Content'), findsOneWidget); + expect(find.text('Activity Screen Content'), findsOneWidget); // Resize back to portrait (320x640) tester.view.physicalSize = const Size(320, 640); await tester.pumpAndSettle(); 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 { diff --git a/test/performance_launch_screen_test.dart b/test/performance_launch_screen_test.dart index 8986713..a73a863 100644 --- a/test/performance_launch_screen_test.dart +++ b/test/performance_launch_screen_test.dart @@ -419,6 +419,17 @@ void main() { telemetryController.add(makeSample(speed: 5.0, monotonicMs: 3750)); 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 telemetryController.add(makeSample(speed: 62.0, monotonicMs: 6950)); await tester.pump(const Duration(milliseconds: 20)); @@ -426,6 +437,7 @@ void main() { expect(stateMachine.state.state, PerformanceLaunchState.completed); expect(stateMachine.state.lastResult!.isFalseStart, isTrue); + expect(stateMachine.state.lastResult!.accelerationTimeSec, closeTo(3.20, 0.05)); // Verify false start badge and negative reaction expect(find.text('FALSE START'), findsOneWidget); diff --git a/test/pro_screen_test.dart b/test/pro_screen_test.dart index 4cee823..cf6fa22 100644 --- a/test/pro_screen_test.dart +++ b/test/pro_screen_test.dart @@ -167,9 +167,9 @@ void main() { await tester.pumpAndSettle(); - expect(find.text('MOTORSIGNAL FREE'), findsOneWidget); + expect(find.text('MotorSignal Free'), findsOneWidget); expect(find.text('FREE'), findsOneWidget); - expect(find.text('UPGRADE TO PRO'), findsOneWidget); + expect(find.text('Upgrade to Pro'), findsOneWidget); expect( find.text('Basic telemetry & local recording active'), findsOneWidget, @@ -193,8 +193,8 @@ void main() { await tester.pumpAndSettle(); - expect(find.text('MOTORSIGNAL PRO'), findsOneWidget); - expect(find.text('MANAGE SUBSCRIPTION'), findsOneWidget); + expect(find.text('MotorSignal Pro'), findsOneWidget); + expect(find.text('Manage Subscription'), findsOneWidget); expect(find.text('MONTHLY'), findsOneWidget); }); @@ -214,7 +214,7 @@ void main() { await tester.pumpAndSettle(); - expect(find.text('MOTORSIGNAL PRO'), findsOneWidget); + expect(find.text('MotorSignal Pro'), findsOneWidget); expect( find.text('Lifetime access • Permanent ownership'), findsOneWidget, @@ -247,7 +247,7 @@ void main() { find.text('Payment grace period • Action needed in Google Play'), findsOneWidget, ); - expect(find.text('MANAGE SUBSCRIPTION'), findsOneWidget); + expect(find.text('Manage Subscription'), findsOneWidget); }); testWidgets('renders on narrow viewport without overflow', (tester) async { diff --git a/test/social_hub_screen_test.dart b/test/social_hub_screen_test.dart new file mode 100644 index 0000000..f90218c --- /dev/null +++ b/test/social_hub_screen_test.dart @@ -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); + } + }); + }); +} diff --git a/test/theme_test.dart b/test/theme_test.dart index b015a93..e7ac07d 100644 --- a/test/theme_test.dart +++ b/test/theme_test.dart @@ -13,13 +13,13 @@ void main() { group('MotorSignal Design System - Tokens & Hierarchy', () { test('AppColors verifies dark and light semantic palette definitions', () { - expect(AppColors.cyberCyan, const Color(0xFF00E5FF)); - expect(AppColors.velocityBlue, const Color(0xFF2E7DFF)); - expect(AppColors.raceRed, const Color(0xFFFF2A2A)); - expect(AppColors.darkBackground, const Color(0xFF0A0A0A)); - expect(AppColors.darkSurface, const Color(0xFF0D0D0D)); - expect(AppColors.lightBackground, const Color(0xFFF0F0F0)); - expect(AppColors.lightSurface, const Color(0xFFF5F5F5)); + expect(AppColors.cyberCyan, const Color(0xFF00B4D8)); + expect(AppColors.velocityBlue, const Color(0xFF0096C7)); + expect(AppColors.raceRed, const Color(0xFFFF3B30)); + expect(AppColors.darkBackground, const Color(0xFF0D0F13)); + expect(AppColors.darkSurface, const Color(0xFF13171E)); + expect(AppColors.lightBackground, const Color(0xFFF3F5F8)); + expect(AppColors.lightSurface, const Color(0xFFE9EDF2)); }); test('AppTypography specifies authoritative multi-font system', () { @@ -61,7 +61,7 @@ void main() { const light = MotorSignalThemeColors.light; expect(light.cyberCyan, AppColors.technicalBlue); - expect(light.launchGreen, AppColors.successGreen); + expect(light.launchGreen, AppColors.launchGreen); expect(light.surfaceContainer, AppColors.lightSurfaceContainer); });