fix: eliminate transient dashboard viewport overflow

This commit is contained in:
Developer 2026-09-05 07:21:02 -04:00
parent 472b1ac994
commit a7cce30e3d
2 changed files with 207 additions and 173 deletions

View File

@ -53,193 +53,223 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
DashboardLayout mode, DashboardLayout mode,
RideSession session, RideSession session,
) { ) {
return Column( return LayoutBuilder(
crossAxisAlignment: CrossAxisAlignment.stretch, builder: (context, constraints) {
children: [ final isShortPortrait = constraints.maxHeight < 620;
// Top status bar
Row( return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Flexible( // Top status bar
child: FittedBox( Row(
fit: BoxFit.scaleDown, mainAxisAlignment: MainAxisAlignment.spaceBetween,
alignment: Alignment.centerLeft, 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( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: const [ children: [
DashboardLayoutSwitcher(), Expanded(
SizedBox(width: 4), child: Center(
GPSStatusIndicator(), child: MetricTile(
SizedBox(width: 8), label: 'TRIP',
HeadingIndicator(), value: session.tripDistanceMiles.toStringAsFixed(2),
unit: 'MI',
compact: isShortPortrait,
),
),
),
Expanded(
child: Center(
child: MetricTile(
label: 'STATUS',
value: session.state.name.toUpperCase(),
compact: isShortPortrait,
),
),
),
], ],
), ),
), ),
), SizedBox(height: isShortPortrait ? 6 : 16),
const SizedBox(width: 4), RideActionButton(compact: isShortPortrait),
const FittedBox( ] else if (mode == DashboardLayout.touring) ...[
fit: BoxFit.scaleDown, SizedBox(height: isShortPortrait ? 2 : 6),
alignment: Alignment.centerRight, SpeedReadout(size: isShortPortrait ? SpeedReadoutSize.compact : SpeedReadoutSize.compact),
child: Row( SizedBox(height: isShortPortrait ? 4 : 8),
mainAxisSize: MainAxisSize.min, 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: [ children: [
PerformanceSummary(), Expanded(child: GForceIndicator(compact: isShortPortrait)),
SizedBox(width: 6), Expanded(child: LeanAngleIndicator(compact: isShortPortrait)),
RideStatusChip(),
], ],
), ),
), SizedBox(height: isShortPortrait ? 4 : 14),
], Container(
), padding: EdgeInsets.symmetric(
horizontal: 10,
// Body based on mode vertical: isShortPortrait ? 4 : 8,
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',
),
),
), ),
Expanded( decoration: _boxDecoration(context),
child: Center( child: Row(
child: MetricTile( mainAxisAlignment: MainAxisAlignment.spaceAround,
label: 'STATUS', children: [
value: session.state.name.toUpperCase(), Expanded(
child: Center(
child: MetricTile(
label: 'TRIP',
value: session.tripDistanceMiles.toStringAsFixed(2),
unit: 'MI',
icon: Icons.pin_drop_outlined,
compact: isShortPortrait,
),
),
), ),
), Expanded(
), child: Center(
], child: MetricTile(
), label: 'TOP SPD',
), value: session.topSpeedMph.toStringAsFixed(0),
const SizedBox(height: 16), unit: 'MPH',
const RideActionButton(), icon: Icons.speed_outlined,
] else if (mode == DashboardLayout.touring) ...[ compact: isShortPortrait,
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(
Expanded( child: MetricTile(
child: Center( label: 'AVG SPD',
child: MetricTile( value: session.averageSpeedMph.toStringAsFixed(1),
label: 'AVG', unit: 'MPH',
value: session.averageSpeedMph.toStringAsFixed(1), icon: Icons.av_timer_outlined,
unit: 'MPH', compact: isShortPortrait,
compact: true, ),
),
), ),
), ],
), ),
Expanded( ),
child: Center( SizedBox(height: isShortPortrait ? 4 : 14),
child: MetricTile( RideActionButton(compact: isShortPortrait),
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()),
], ],
), ],
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(),
],
],
); );
} }

View File

@ -76,6 +76,10 @@ void main() {
group('Canonical Dashboard Viewport Matrix Tests', () { group('Canonical Dashboard Viewport Matrix Tests', () {
final viewports = <String, Size>{ final viewports = <String, Size>{
'320x640 (Small Compact Phone)': const Size(320, 640), '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), '400x800 (Standard Phone Portrait)': const Size(400, 800),
'412x915 (Pixel-class Portrait)': const Size(412, 915), '412x915 (Pixel-class Portrait)': const Size(412, 915),
'780x360 (Short Landscape Mount)': const Size(780, 360), '780x360 (Short Landscape Mount)': const Size(780, 360),