Implement UI/UX enhancements with collapsible routes sidebar and glassmorphic route cards
Adds new components (CollapsibleRoutesSidebar, GlassmorphicRouteCard) and internationalization support. Updates deliveries and routes pages with improved navigation and visual presentation using Material Design 3 principles. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/delivery_route.dart';
|
||||
import '../theme/spacing_system.dart';
|
||||
import '../theme/size_system.dart';
|
||||
import '../theme/animation_system.dart';
|
||||
import '../theme/color_system.dart';
|
||||
import '../utils/breakpoints.dart';
|
||||
import 'glassmorphic_route_card.dart';
|
||||
|
||||
|
||||
class CollapsibleRoutesSidebar extends StatefulWidget {
|
||||
final List<DeliveryRoute> routes;
|
||||
final DeliveryRoute? selectedRoute;
|
||||
final ValueChanged<DeliveryRoute> onRouteSelected;
|
||||
|
||||
const CollapsibleRoutesSidebar({
|
||||
super.key,
|
||||
required this.routes,
|
||||
this.selectedRoute,
|
||||
required this.onRouteSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CollapsibleRoutesSidebar> createState() =>
|
||||
_CollapsibleRoutesSidebarState();
|
||||
}
|
||||
|
||||
class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
||||
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 = context.isMobile;
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// On mobile, always show as collapsible
|
||||
if (isMobile) {
|
||||
return Container(
|
||||
color: isDarkMode ? SvrntyColors.almostBlack : Colors.white,
|
||||
child: Column(
|
||||
children: [
|
||||
// Header with toggle button
|
||||
Container(
|
||||
padding: EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: isDarkMode ? SvrntyColors.darkSlate : SvrntyColors.slateGray.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (_isExpanded)
|
||||
Text(
|
||||
'Routes',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
IconButton(
|
||||
icon: AnimatedRotation(
|
||||
turns: _isExpanded ? 0 : -0.25,
|
||||
duration: Duration(
|
||||
milliseconds: AppAnimations.durationFast.inMilliseconds,
|
||||
),
|
||||
child: const Icon(Icons.chevron_right),
|
||||
),
|
||||
onPressed: _toggleSidebar,
|
||||
iconSize: AppSizes.iconMd,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Collapsible content
|
||||
if (_isExpanded)
|
||||
Expanded(
|
||||
child: _buildRoutesList(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// On tablet/desktop, show full sidebar with toggle
|
||||
return Container(
|
||||
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(
|
||||
child: Text(
|
||||
'Routes',
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Routes list
|
||||
Expanded(
|
||||
child: _buildRoutesList(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRoutesList(BuildContext context) {
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.all(AppSpacing.sm),
|
||||
itemCount: widget.routes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final route = widget.routes[index];
|
||||
final isSelected = widget.selectedRoute?.id == route.id;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: AppSpacing.sm),
|
||||
child: _buildRouteButton(context, route, isSelected),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRouteButton(
|
||||
BuildContext context,
|
||||
DeliveryRoute route,
|
||||
bool isSelected,
|
||||
) {
|
||||
return GlassmorphicRouteCard(
|
||||
route: route,
|
||||
isSelected: isSelected,
|
||||
isCollapsed: !_isExpanded,
|
||||
onTap: () => widget.onRouteSelected(route),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui' as ui;
|
||||
import '../models/delivery_route.dart';
|
||||
import '../theme/spacing_system.dart';
|
||||
import '../theme/size_system.dart';
|
||||
import '../theme/animation_system.dart';
|
||||
import '../theme/color_system.dart';
|
||||
|
||||
/// Modern glassmorphic route card with status-based gradient and animated progress
|
||||
class GlassmorphicRouteCard extends StatefulWidget {
|
||||
final DeliveryRoute route;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final bool isCollapsed;
|
||||
|
||||
const GlassmorphicRouteCard({
|
||||
super.key,
|
||||
required this.route,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
this.isCollapsed = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<GlassmorphicRouteCard> createState() => _GlassmorphicRouteCardState();
|
||||
}
|
||||
|
||||
class _GlassmorphicRouteCardState extends State<GlassmorphicRouteCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _hoverController;
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hoverController = AnimationController(
|
||||
duration: Duration(milliseconds: AppAnimations.durationFast.inMilliseconds),
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hoverController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Calculate color based on completion percentage
|
||||
Color _getProgressColor(double progress) {
|
||||
if (progress < 0.3) {
|
||||
// Red to orange (0-30%)
|
||||
return Color.lerp(
|
||||
SvrntyColors.crimsonRed,
|
||||
const Color(0xFFFF9800),
|
||||
(progress / 0.3),
|
||||
)!;
|
||||
} else if (progress < 0.7) {
|
||||
// Orange to yellow (30-70%)
|
||||
return Color.lerp(
|
||||
const Color(0xFFFF9800),
|
||||
const Color(0xFFFFC107),
|
||||
((progress - 0.3) / 0.4),
|
||||
)!;
|
||||
} else {
|
||||
// Yellow to green (70-100%)
|
||||
return Color.lerp(
|
||||
const Color(0xFFFFC107),
|
||||
const Color(0xFF4CAF50),
|
||||
((progress - 0.7) / 0.3),
|
||||
)!;
|
||||
}
|
||||
}
|
||||
|
||||
void _setHovered(bool hovered) {
|
||||
setState(() {
|
||||
_isHovered = hovered;
|
||||
});
|
||||
if (hovered) {
|
||||
_hoverController.forward();
|
||||
} else {
|
||||
_hoverController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
final progress = widget.route.deliveredCount / widget.route.deliveriesCount;
|
||||
final progressColor = _getProgressColor(progress);
|
||||
|
||||
if (widget.isCollapsed) {
|
||||
return _buildCollapsedCard(context, isDarkMode, progress, progressColor);
|
||||
}
|
||||
|
||||
return _buildExpandedCard(context, isDarkMode, progress, progressColor);
|
||||
}
|
||||
|
||||
Widget _buildCollapsedCard(BuildContext context, bool isDarkMode,
|
||||
double progress, Color progressColor) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => _setHovered(true),
|
||||
onExit: (_) => _setHovered(false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(
|
||||
milliseconds: AppAnimations.durationFast.inMilliseconds,
|
||||
),
|
||||
height: AppSizes.buttonHeightMd,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppSpacing.md),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Glassmorphic background
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppSpacing.md),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 8, sigmaY: 8),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: (isDarkMode
|
||||
? SvrntyColors.darkSlate
|
||||
: Colors.white)
|
||||
.withValues(alpha: 0.7),
|
||||
border: Border.all(
|
||||
color: (isDarkMode ? Colors.white : Colors.white)
|
||||
.withValues(alpha: 0.2),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Progress indicator at bottom
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(AppSpacing.md - AppSpacing.xs),
|
||||
bottomRight: Radius.circular(AppSpacing.md - AppSpacing.xs),
|
||||
),
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
minHeight: 3,
|
||||
backgroundColor: progressColor.withValues(alpha: 0.2),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(progressColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Content
|
||||
Center(
|
||||
child: Text(
|
||||
widget.route.name.substring(0, 1).toUpperCase(),
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: widget.isSelected ? progressColor : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpandedCard(BuildContext context, bool isDarkMode,
|
||||
double progress, Color progressColor) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => _setHovered(true),
|
||||
onExit: (_) => _setHovered(false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: AnimatedBuilder(
|
||||
animation: _hoverController,
|
||||
builder: (context, child) {
|
||||
final hoverValue = _hoverController.value;
|
||||
final blurSigma = 8 + (hoverValue * 3);
|
||||
final bgOpacity = 0.7 + (hoverValue * 0.1);
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: Duration(
|
||||
milliseconds: AppAnimations.durationFast.inMilliseconds,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppSpacing.lg),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: progressColor.withValues(alpha: 0.1 + (hoverValue * 0.2)),
|
||||
blurRadius: 12 + (hoverValue * 8),
|
||||
offset: Offset(0, 2 + (hoverValue * 2)),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Glassmorphic background
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppSpacing.lg),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: (isDarkMode
|
||||
? SvrntyColors.darkSlate
|
||||
: Colors.white)
|
||||
.withValues(alpha: bgOpacity),
|
||||
border: Border.all(
|
||||
color: (isDarkMode ? Colors.white : Colors.white)
|
||||
.withValues(alpha: 0.2 + (hoverValue * 0.15)),
|
||||
width: 1.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppSpacing.lg),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Content
|
||||
Padding(
|
||||
padding: EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Route name
|
||||
Text(
|
||||
widget.route.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: widget.isSelected ? progressColor : null,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
// Delivery count
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${widget.route.deliveredCount}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: progressColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '/${widget.route.deliveriesCount}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
fontSize: 11,
|
||||
color: (Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black)
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
// Animated gradient progress bar
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background
|
||||
Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: progressColor
|
||||
.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
// Progress fill with gradient
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: Container(
|
||||
height: 6,
|
||||
width: 100 * progress,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
SvrntyColors.crimsonRed,
|
||||
progressColor,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: progressColor
|
||||
.withValues(alpha: 0.4),
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user