Improve map navigation UI and add collapsible deliveries sidebar

Changes:
- Remove zoom controls (+ and - buttons) from map navigation
- Remove duplicate delivery info from top of delivery page
- Add collapsible sidebar to map layout with smooth animations
- Sidebar collapses to 64px width when not expanded
- Expanded sidebar shows 280px width with delivery list
- Added "Deliveries" header with animated chevron toggle button
- Updated dark mode styling for collapsible header
- Frees up more space for map navigation window

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jean-Philippe Brule 2025-11-16 12:20:40 -05:00
parent 57b81d1e95
commit dc9282e2c7
3 changed files with 109 additions and 199 deletions

View File

@ -293,36 +293,6 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
}
}
Future<void> _zoomIn() async {
if (_navigationController == null) return;
try {
final currentCamera = await _navigationController!.getCameraPosition();
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(
currentCamera.target,
currentCamera.zoom + 1,
),
);
} catch (e) {
debugPrint('Zoom in error: $e');
}
}
Future<void> _zoomOut() async {
if (_navigationController == null) return;
try {
final currentCamera = await _navigationController!.getCameraPosition();
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(
currentCamera.target,
currentCamera.zoom - 1,
),
);
} catch (e) {
debugPrint('Zoom out error: $e');
}
}
Future<void> _recenterMap() async {
if (_navigationController == null) return;
try {
@ -340,9 +310,8 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
// Driver's current location (defaults to Montreal if not available)
final initialPosition = const LatLng(latitude: 45.5017, longitude: -73.5673);
// Calculate dynamic padding for top info panel and bottom button bar
// Increased to accommodate navigation widget info and action buttons
final topPadding = widget.selectedDelivery != null ? 110.0 : 0.0;
// Calculate dynamic padding for bottom button bar
final topPadding = 0.0;
final bottomPadding = widget.selectedDelivery != null ? 110.0 : 0.0;
return Stack(
@ -369,27 +338,6 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
),
),
),
// Custom dark-themed controls overlay
Positioned(
top: 16,
right: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Zoom in button
_buildIconButton(
icon: Icons.add,
onPressed: _zoomIn,
),
const SizedBox(height: 8),
// Zoom out button
_buildIconButton(
icon: Icons.remove,
onPressed: _zoomOut,
),
],
),
),
// Navigation and action buttons (positioned above bottom button bar)
Positioned(
bottom: 120,
@ -415,87 +363,6 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
],
),
),
// Selected delivery info panel (dark themed)
if (widget.selectedDelivery != null)
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.selectedDelivery!.name,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
widget.selectedDelivery!.deliveryAddress
?.formattedAddress ??
'No address',
style: Theme.of(context)
.textTheme
.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 8),
// Status indicator
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: widget.selectedDelivery!.delivered
? SvrntyColors.statusCompleted
: SvrntyColors.statusPending,
borderRadius: BorderRadius.circular(4),
),
child: Text(
widget.selectedDelivery!.delivered
? 'Delivered'
: 'Pending',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
// Bottom action button bar
if (widget.selectedDelivery != null)
Positioned(
@ -633,40 +500,6 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
);
}
Widget _buildIconButton({
required IconData icon,
required VoidCallback onPressed,
}) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Theme.of(context).colorScheme.surface,
shape: const CircleBorder(),
child: InkWell(
onTap: onPressed,
customBorder: const CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(12),
child: Icon(
icon,
color: Theme.of(context).colorScheme.onSurface,
size: 24,
),
),
),
),
);
}
Widget _buildBottomActionButton({
required String label,
required IconData icon,

View File

@ -195,29 +195,6 @@ class _DeliveryMapState extends State<DeliveryMap> {
),
),
),
Positioned(
top: 16,
right: 16,
child: Column(
children: [
FloatingActionButton.small(
heroTag: 'zoom_in',
onPressed: () {
_navigationController?.animateCamera(CameraUpdate.zoomIn());
},
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton.small(
heroTag: 'zoom_out',
onPressed: () {
_navigationController?.animateCamera(CameraUpdate.zoomOut());
},
child: const Icon(Icons.remove),
),
],
),
),
],
);
}

View File

@ -1,7 +1,11 @@
import 'package:flutter/material.dart';
import '../utils/breakpoints.dart';
import '../theme/spacing_system.dart';
import '../theme/size_system.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
class MapSidebarLayout extends StatelessWidget {
class MapSidebarLayout extends StatefulWidget {
final Widget mapWidget;
final Widget sidebarWidget;
final double mapRatio;
@ -14,22 +18,118 @@ class MapSidebarLayout extends StatelessWidget {
});
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < Breakpoints.tablet;
if (isMobile) {
return sidebarWidget;
State<MapSidebarLayout> createState() => _MapSidebarLayoutState();
}
class _MapSidebarLayoutState extends State<MapSidebarLayout>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
bool _isExpanded = true;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
if (_isExpanded) {
_animationController.forward();
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleSidebar() {
setState(() {
_isExpanded = !_isExpanded;
});
if (_isExpanded) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < Breakpoints.tablet;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
if (isMobile) {
return widget.sidebarWidget;
}
// Desktop: Show map with collapsible sidebar
return Row(
children: [
Expanded(
flex: (mapRatio * 100).toInt(),
child: mapWidget,
flex: (widget.mapRatio * 100).toInt(),
child: widget.mapWidget,
),
// Collapsible sidebar with toggle button
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: _isExpanded ? 280 : 64,
color: isDarkMode ? SvrntyColors.almostBlack : Colors.white,
child: Column(
children: [
// Header with toggle button
Container(
height: kToolbarHeight,
padding: EdgeInsets.symmetric(horizontal: AppSpacing.xs),
decoration: BoxDecoration(
border: Border(
left: BorderSide(
color: isDarkMode
? SvrntyColors.darkSlate
: SvrntyColors.slateGray.withValues(alpha: 0.2),
width: 1,
),
),
),
child: Row(
mainAxisAlignment: _isExpanded
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.center,
children: [
if (_isExpanded)
Expanded(
flex: ((1 - mapRatio) * 100).toInt(),
child: sidebarWidget,
child: Text(
'Deliveries',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: AppSizes.buttonHeightMd,
height: AppSizes.buttonHeightMd,
child: IconButton(
icon: AnimatedRotation(
turns: _isExpanded ? 0 : -0.5,
duration: Duration(
milliseconds:
AppAnimations.durationFast.inMilliseconds,
),
child: const Icon(Icons.chevron_right),
),
onPressed: _toggleSidebar,
iconSize: AppSizes.iconMd,
),
),
],
),
),
// Sidebar content
Expanded(
child: _isExpanded ? widget.sidebarWidget : const SizedBox.shrink(),
),
],
),
),
],
);