731 lines
25 KiB
Dart
731 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../../core/responsive/viewport_size.dart';
|
|
import '../../../core/theme/app_colors.dart';
|
|
import '../../../core/theme/theme_extensions.dart';
|
|
import '../../rides/ride_state_machine.dart';
|
|
import '../dashboard_config.dart';
|
|
import 'dashboard_layout_switcher.dart';
|
|
import 'dashboard_map_panel.dart';
|
|
import 'g_force_indicator.dart';
|
|
import 'gps_status_indicator.dart';
|
|
import 'heading_indicator.dart';
|
|
import 'lean_angle_indicator.dart';
|
|
import 'metric_tile.dart';
|
|
import 'performance_summary.dart';
|
|
import 'ride_action_button.dart';
|
|
import 'ride_status_chip.dart';
|
|
import 'speed_readout.dart';
|
|
|
|
/// The single authoritative responsive layout for the MotorSignal dashboard.
|
|
///
|
|
/// Adapts composition based on available [ResponsiveSnapshot] constraints
|
|
/// and user-selected [DashboardLayout] mode (Sport / Touring / Minimal).
|
|
///
|
|
/// Does NOT rebuild on 20 Hz telemetry ticks; high-frequency telemetry
|
|
/// is consumed directly by leaf visualizers.
|
|
class ResponsiveDashboardLayout extends ConsumerWidget {
|
|
const ResponsiveDashboardLayout({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final layoutMode = ref.watch(dashboardConfigProvider);
|
|
final session = ref.watch(rideStateMachineProvider);
|
|
final snapshot = context.responsive;
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: snapshot.isShortViewport ? 10.0 : 16.0,
|
|
vertical: snapshot.isShortViewport ? 6.0 : 12.0,
|
|
),
|
|
child: snapshot.isLandscape
|
|
? (snapshot.isShortViewport
|
|
? _buildShortLandscapeLayout(context, layoutMode, session)
|
|
: _buildExpandedLayout(context, layoutMode, session, snapshot))
|
|
: _buildCompactPortraitLayout(context, layoutMode, session),
|
|
);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// COMPACT PORTRAIT
|
|
// --------------------------------------------------------------------------
|
|
Widget _buildCompactPortraitLayout(
|
|
BuildContext context,
|
|
DashboardLayout mode,
|
|
RideSession session,
|
|
) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
// Group-ride alerts reduce the usable height above this layout.
|
|
final isShortPortrait = constraints.maxHeight < 760;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// 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(
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
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: Center(
|
|
child: Transform.translate(
|
|
offset: Offset(0, -12),
|
|
child: SpeedReadout(size: SpeedReadoutSize.hero),
|
|
),
|
|
),
|
|
),
|
|
if (!isShortPortrait) const Spacer(flex: 1),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Expanded(child: GForceIndicator(compact: isShortPortrait)),
|
|
Expanded(child: LeanAngleIndicator(compact: isShortPortrait)),
|
|
],
|
|
),
|
|
SizedBox(height: isShortPortrait ? 4 : 14),
|
|
Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 10,
|
|
vertical: isShortPortrait ? 4 : 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,
|
|
compact: isShortPortrait,
|
|
),
|
|
),
|
|
),
|
|
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 SPD',
|
|
value: session.averageSpeedMph.toStringAsFixed(1),
|
|
unit: 'MPH',
|
|
icon: Icons.av_timer_outlined,
|
|
compact: isShortPortrait,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: isShortPortrait ? 4 : 14),
|
|
RideActionButton(compact: isShortPortrait),
|
|
if (!isShortPortrait) ...[
|
|
const SizedBox(height: 6),
|
|
const PerformanceLaunchShortcut(),
|
|
],
|
|
],
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// SHORT LANDSCAPE (Cockpit Horizontal Mount)
|
|
// --------------------------------------------------------------------------
|
|
Widget _buildShortLandscapeLayout(
|
|
BuildContext context,
|
|
DashboardLayout mode,
|
|
RideSession session,
|
|
) {
|
|
if (mode == DashboardLayout.touring) {
|
|
return Row(
|
|
children: [
|
|
// Left side: Mode-driven embedded map
|
|
const Expanded(flex: 55, child: DashboardMapPanel()),
|
|
const SizedBox(width: 8),
|
|
// Right side: Cockpit telemetry
|
|
Expanded(
|
|
flex: 45,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
DashboardLayoutSwitcher(),
|
|
GPSStatusIndicator(showLabel: false),
|
|
HeadingIndicator(compact: true),
|
|
RideStatusChip(),
|
|
],
|
|
),
|
|
const Center(
|
|
child: SpeedReadout(size: SpeedReadoutSize.compact),
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
MetricTile(
|
|
label: 'TRIP',
|
|
value: session.tripDistanceMiles.toStringAsFixed(1),
|
|
unit: 'MI',
|
|
compact: true,
|
|
),
|
|
MetricTile(
|
|
label: 'AVG',
|
|
value: session.averageSpeedMph.toStringAsFixed(0),
|
|
unit: 'MPH',
|
|
compact: true,
|
|
),
|
|
MetricTile(
|
|
label: 'ELEV',
|
|
value: session.elevationGain.toStringAsFixed(0),
|
|
unit: 'M',
|
|
compact: true,
|
|
),
|
|
],
|
|
),
|
|
const RideActionButton(compact: true),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
if (mode == DashboardLayout.minimal) {
|
|
return Row(
|
|
children: [
|
|
const Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
DashboardLayoutSwitcher(),
|
|
GPSStatusIndicator(),
|
|
HeadingIndicator(compact: true),
|
|
RideStatusChip(),
|
|
],
|
|
),
|
|
const Expanded(
|
|
child: Center(child: SpeedReadout(size: SpeedReadoutSize.hero)),
|
|
),
|
|
SizedBox(
|
|
width: 140,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
MetricTile(
|
|
label: 'TRIP',
|
|
value: session.tripDistanceMiles.toStringAsFixed(1),
|
|
unit: 'MI',
|
|
),
|
|
const SizedBox(height: 12),
|
|
const RideActionButton(compact: true),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Default: Sport Cockpit Horizontal
|
|
return Row(
|
|
children: [
|
|
// Left Wing: G-Force + Trip Stats
|
|
Expanded(
|
|
flex: 28,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const GForceIndicator(compact: true),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
MetricTile(
|
|
label: 'TRIP',
|
|
value: session.tripDistanceMiles.toStringAsFixed(1),
|
|
unit: 'MI',
|
|
compact: true,
|
|
),
|
|
MetricTile(
|
|
label: 'TOP',
|
|
value: session.topSpeedMph.toStringAsFixed(0),
|
|
unit: 'MPH',
|
|
compact: true,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Center: Speedometer & Controls
|
|
Expanded(
|
|
flex: 44,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
DashboardLayoutSwitcher(),
|
|
GPSStatusIndicator(showLabel: false),
|
|
HeadingIndicator(compact: true),
|
|
RideStatusChip(),
|
|
],
|
|
),
|
|
const Center(child: SpeedReadout(size: SpeedReadoutSize.compact)),
|
|
const RideActionButton(compact: true),
|
|
const SizedBox(height: 6),
|
|
const PerformanceLaunchShortcut(compact: true),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Right Wing: Lean Angle + Avg Speed
|
|
Expanded(
|
|
flex: 28,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const LeanAngleIndicator(compact: true),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
MetricTile(
|
|
label: 'AVG',
|
|
value: session.averageSpeedMph.toStringAsFixed(0),
|
|
unit: 'MPH',
|
|
compact: true,
|
|
),
|
|
const PerformanceSummary(compact: true),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// MEDIUM / EXPANDED (Tablets and Large Displays)
|
|
// --------------------------------------------------------------------------
|
|
Widget _buildExpandedLayout(
|
|
BuildContext context,
|
|
DashboardLayout mode,
|
|
RideSession session,
|
|
ResponsiveSnapshot snapshot,
|
|
) {
|
|
if (mode == DashboardLayout.touring) {
|
|
return Row(
|
|
children: [
|
|
const Expanded(flex: 60, child: DashboardMapPanel()),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
flex: 40,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
DashboardLayoutSwitcher(),
|
|
GPSStatusIndicator(),
|
|
HeadingIndicator(),
|
|
RideStatusChip(),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
const Center(
|
|
child: SpeedReadout(size: SpeedReadoutSize.regular),
|
|
),
|
|
const Spacer(),
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: _boxDecoration(context),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
MetricTile(
|
|
label: 'TRIP',
|
|
value: session.tripDistanceMiles.toStringAsFixed(2),
|
|
unit: 'MI',
|
|
),
|
|
MetricTile(
|
|
label: 'TOP SPD',
|
|
value: session.topSpeedMph.toStringAsFixed(0),
|
|
unit: 'MPH',
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
MetricTile(
|
|
label: 'AVG SPD',
|
|
value: session.averageSpeedMph.toStringAsFixed(1),
|
|
unit: 'MPH',
|
|
),
|
|
MetricTile(
|
|
label: 'ELEV GAIN',
|
|
value: session.elevationGain.toStringAsFixed(0),
|
|
unit: 'M',
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const RideActionButton(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Default Sport Multi-Column
|
|
return Row(
|
|
children: [
|
|
// Left Wing: G-Force & Records
|
|
Expanded(
|
|
flex: 30,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const GForceIndicator(),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: _boxDecoration(context),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
Expanded(
|
|
child: Center(
|
|
child: MetricTile(
|
|
label: 'TRIP',
|
|
value: session.tripDistanceMiles.toStringAsFixed(2),
|
|
unit: 'MI',
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Center(
|
|
child: MetricTile(
|
|
label: 'TOP SPD',
|
|
value: session.topSpeedMph.toStringAsFixed(0),
|
|
unit: 'MPH',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
|
|
// Center: Status + Speed + Action
|
|
Expanded(
|
|
flex: 40,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
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: 4),
|
|
HeadingIndicator(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
const FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
alignment: Alignment.centerRight,
|
|
child: RideStatusChip(),
|
|
),
|
|
],
|
|
),
|
|
Center(
|
|
child: Transform.translate(
|
|
offset: Offset(0, -20),
|
|
child: SpeedReadout(size: SpeedReadoutSize.hero),
|
|
),
|
|
),
|
|
const RideActionButton(),
|
|
const SizedBox(height: 6),
|
|
const PerformanceLaunchShortcut(),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
|
|
// Right Wing: Lean Angle & Averages
|
|
Expanded(
|
|
flex: 30,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const LeanAngleIndicator(),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: _boxDecoration(context),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
Expanded(
|
|
child: Center(
|
|
child: MetricTile(
|
|
label: 'AVG SPD',
|
|
value: session.averageSpeedMph.toStringAsFixed(1),
|
|
unit: 'MPH',
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Center(
|
|
child: MetricTile(
|
|
label: 'MOVING',
|
|
value: '${(session.movingTimeSeconds / 60).floor()}m',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
BoxDecoration _boxDecoration(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
return BoxDecoration(
|
|
color: isDark
|
|
? AppColors.darkSurfaceContainer
|
|
: AppColors.lightSurfaceContainer,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: isDark
|
|
? AppColors.darkOutlineVariant
|
|
: AppColors.lightOutlineVariant,
|
|
width: 1.0,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Fast dashboard entry point for the launch timer used before a run.
|
|
class PerformanceLaunchShortcut extends StatelessWidget {
|
|
final bool compact;
|
|
|
|
const PerformanceLaunchShortcut({super.key, this.compact = false});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = context.motorColors;
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: compact ? 38 : 42,
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => context.push('/launch'),
|
|
icon: Icon(Icons.rocket_launch, size: compact ? 16 : 18),
|
|
label: const Text('PERFORMANCE LAUNCH'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: colors.launchGreen,
|
|
side: BorderSide(color: colors.launchGreen.withValues(alpha: 0.75)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
textStyle: TextStyle(
|
|
fontSize: compact ? 10 : 11,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: 0.35,
|
|
),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|