From a7cce30e3de02fe314dbcefebeef56ad55963bed Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 5 Sep 2026 07:21:02 -0400 Subject: [PATCH] fix: eliminate transient dashboard viewport overflow --- .../widgets/responsive_dashboard_layout.dart | 376 ++++++++++-------- test/canonical_dashboard_responsive_test.dart | 4 + 2 files changed, 207 insertions(+), 173 deletions(-) diff --git a/lib/features/dashboard/widgets/responsive_dashboard_layout.dart b/lib/features/dashboard/widgets/responsive_dashboard_layout.dart index e6e7367..efbfd19 100644 --- a/lib/features/dashboard/widgets/responsive_dashboard_layout.dart +++ b/lib/features/dashboard/widgets/responsive_dashboard_layout.dart @@ -53,193 +53,223 @@ class ResponsiveDashboardLayout extends ConsumerWidget { DashboardLayout mode, RideSession session, ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Top status bar - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return LayoutBuilder( + builder: (context, constraints) { + final isShortPortrait = constraints.maxHeight < 620; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, + // Top status bar + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + DashboardLayoutSwitcher(), + SizedBox(width: 4), + GPSStatusIndicator(), + SizedBox(width: 8), + HeadingIndicator(), + ], + ), + ), + ), + const SizedBox(width: 4), + const FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + PerformanceSummary(), + SizedBox(width: 6), + RideStatusChip(), + ], + ), + ), + ], + ), + + // Body based on mode + if (mode == DashboardLayout.minimal) ...[ + if (!isShortPortrait) const Spacer(flex: 2) else const Spacer(flex: 1), + const Flexible( + flex: 6, + child: Center( + child: SpeedReadout(size: SpeedReadoutSize.hero), + ), + ), + if (!isShortPortrait) const Spacer(flex: 2) else const Spacer(flex: 1), + Container( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: isShortPortrait ? 4 : 10, + ), + decoration: _boxDecoration(context), child: Row( - mainAxisSize: MainAxisSize.min, - children: const [ - DashboardLayoutSwitcher(), - SizedBox(width: 4), - GPSStatusIndicator(), - SizedBox(width: 8), - HeadingIndicator(), + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: Center( + child: MetricTile( + label: 'TRIP', + value: session.tripDistanceMiles.toStringAsFixed(2), + unit: 'MI', + compact: isShortPortrait, + ), + ), + ), + Expanded( + child: Center( + child: MetricTile( + label: 'STATUS', + value: session.state.name.toUpperCase(), + compact: isShortPortrait, + ), + ), + ), ], ), ), - ), - const SizedBox(width: 4), - const FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, + SizedBox(height: isShortPortrait ? 6 : 16), + RideActionButton(compact: isShortPortrait), + ] else if (mode == DashboardLayout.touring) ...[ + SizedBox(height: isShortPortrait ? 2 : 6), + SpeedReadout(size: isShortPortrait ? SpeedReadoutSize.compact : SpeedReadoutSize.compact), + SizedBox(height: isShortPortrait ? 4 : 8), + const Expanded( + flex: 5, + child: DashboardMapPanel(), + ), + SizedBox(height: isShortPortrait ? 4 : 10), + Container( + padding: EdgeInsets.symmetric( + horizontal: 8, + vertical: isShortPortrait ? 4 : 6, + ), + decoration: _boxDecoration(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: Center( + child: MetricTile( + label: 'TRIP', + value: session.tripDistanceMiles.toStringAsFixed(1), + unit: 'MI', + compact: true, + ), + ), + ), + Expanded( + child: Center( + child: MetricTile( + label: 'AVG', + value: session.averageSpeedMph.toStringAsFixed(1), + unit: 'MPH', + compact: true, + ), + ), + ), + Expanded( + child: Center( + child: MetricTile( + label: 'ELEV', + value: session.elevationGain.toStringAsFixed(0), + unit: 'M', + compact: true, + ), + ), + ), + Expanded( + child: Center( + child: MetricTile( + label: 'MOVING', + value: '${(session.movingTimeSeconds / 60).floor()}m', + compact: true, + ), + ), + ), + ], + ), + ), + SizedBox(height: isShortPortrait ? 4 : 10), + RideActionButton(compact: isShortPortrait), + ] else ...[ + // Sport Cockpit Mode + if (!isShortPortrait) const Spacer(flex: 1), + Flexible( + flex: isShortPortrait ? 3 : 4, + child: const Center( + child: SpeedReadout(size: SpeedReadoutSize.hero), + ), + ), + if (!isShortPortrait) const Spacer(flex: 1), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - PerformanceSummary(), - SizedBox(width: 6), - RideStatusChip(), + Expanded(child: GForceIndicator(compact: isShortPortrait)), + Expanded(child: LeanAngleIndicator(compact: isShortPortrait)), ], ), - ), - ], - ), - - // Body based on mode - if (mode == DashboardLayout.minimal) ...[ - const Spacer(flex: 2), - const SpeedReadout(size: SpeedReadoutSize.hero), - const Spacer(flex: 2), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: _boxDecoration(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: Center( - child: MetricTile( - label: 'TRIP', - value: session.tripDistanceMiles.toStringAsFixed(2), - unit: 'MI', - ), - ), + SizedBox(height: isShortPortrait ? 4 : 14), + Container( + padding: EdgeInsets.symmetric( + horizontal: 10, + vertical: isShortPortrait ? 4 : 8, ), - Expanded( - child: Center( - child: MetricTile( - label: 'STATUS', - value: session.state.name.toUpperCase(), + decoration: _boxDecoration(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: Center( + child: MetricTile( + label: 'TRIP', + value: session.tripDistanceMiles.toStringAsFixed(2), + unit: 'MI', + icon: Icons.pin_drop_outlined, + compact: isShortPortrait, + ), + ), ), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - const RideActionButton(), - ] else if (mode == DashboardLayout.touring) ...[ - const SizedBox(height: 6), - const SpeedReadout(size: SpeedReadoutSize.compact), - const SizedBox(height: 8), - const Expanded( - flex: 5, - child: DashboardMapPanel(), - ), - const SizedBox(height: 10), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - decoration: _boxDecoration(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: Center( - child: MetricTile( - label: 'TRIP', - value: session.tripDistanceMiles.toStringAsFixed(1), - unit: 'MI', - compact: true, + Expanded( + child: Center( + child: MetricTile( + label: 'TOP SPD', + value: session.topSpeedMph.toStringAsFixed(0), + unit: 'MPH', + icon: Icons.speed_outlined, + compact: isShortPortrait, + ), + ), ), - ), - ), - Expanded( - child: Center( - child: MetricTile( - label: 'AVG', - value: session.averageSpeedMph.toStringAsFixed(1), - unit: 'MPH', - compact: true, + Expanded( + child: Center( + child: MetricTile( + label: 'AVG SPD', + value: session.averageSpeedMph.toStringAsFixed(1), + unit: 'MPH', + icon: Icons.av_timer_outlined, + compact: isShortPortrait, + ), + ), ), - ), + ], ), - Expanded( - child: Center( - child: MetricTile( - label: 'ELEV', - value: session.elevationGain.toStringAsFixed(0), - unit: 'M', - compact: true, - ), - ), - ), - Expanded( - child: Center( - child: MetricTile( - label: 'MOVING', - value: '${(session.movingTimeSeconds / 60).floor()}m', - compact: true, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 10), - const RideActionButton(), - ] else ...[ - // Sport Cockpit Mode - const Spacer(flex: 1), - const SpeedReadout(size: SpeedReadoutSize.hero), - const Spacer(flex: 1), - const Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded(child: GForceIndicator()), - Expanded(child: LeanAngleIndicator()), + ), + SizedBox(height: isShortPortrait ? 4 : 14), + RideActionButton(compact: isShortPortrait), ], - ), - const SizedBox(height: 14), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - decoration: _boxDecoration(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: Center( - child: MetricTile( - label: 'TRIP', - value: session.tripDistanceMiles.toStringAsFixed(2), - unit: 'MI', - icon: Icons.pin_drop_outlined, - ), - ), - ), - Expanded( - child: Center( - child: MetricTile( - label: 'TOP SPD', - value: session.topSpeedMph.toStringAsFixed(0), - unit: 'MPH', - icon: Icons.speed_outlined, - ), - ), - ), - Expanded( - child: Center( - child: MetricTile( - label: 'AVG SPD', - value: session.averageSpeedMph.toStringAsFixed(1), - unit: 'MPH', - icon: Icons.av_timer_outlined, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 14), - const RideActionButton(), - ], - ], + ], + ); + }, ); } diff --git a/test/canonical_dashboard_responsive_test.dart b/test/canonical_dashboard_responsive_test.dart index f1b6589..f802e42 100644 --- a/test/canonical_dashboard_responsive_test.dart +++ b/test/canonical_dashboard_responsive_test.dart @@ -76,6 +76,10 @@ void main() { group('Canonical Dashboard Viewport Matrix Tests', () { final viewports = { '320x640 (Small Compact Phone)': const Size(320, 640), + '320x520 (Constrained Small Phone)': const Size(320, 520), + '360x460 (Transient IME/Sheet Inset)': const Size(360, 460), + '412x460 (Pixel 8a Inset Viewport)': const Size(412, 460), + '390x480 (Constrained Portrait Phone)': const Size(390, 480), '400x800 (Standard Phone Portrait)': const Size(400, 800), '412x915 (Pixel-class Portrait)': const Size(412, 915), '780x360 (Short Landscape Mount)': const Size(780, 360),