Refactor theme system and remove unused platforms

- Overhaul theme system with Svrnty design and WCAG AAA compliance
- Remove android, macos, and web platform files (iOS-only focus)
- Update components with improved dark mode map and UI refinements
- Enhance settings page with additional configuration options
- Add theme system documentation in lib/theme/README.md
- Update CLAUDE.md with comprehensive theme guidelines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 14:47:51 -05:00
parent 554b26cfd1
commit edb106a7fd
102 changed files with 1225 additions and 2785 deletions
+10 -3
View File
@@ -32,9 +32,16 @@ class GrpcCqrsApiClient {
ClientChannel get channel {
if (_channel == null) {
final credentials = config.useTls
? const ChannelCredentials.secure()
: const ChannelCredentials.insecure();
final ChannelCredentials credentials;
if (!config.useTls) {
credentials = const ChannelCredentials.insecure();
} else if (config.allowSelfSignedCertificate) {
credentials = ChannelCredentials.secure(
onBadCertificate: (certificate, host) => true,
);
} else {
credentials = const ChannelCredentials.secure();
}
_channel = ClientChannel(
config.host,
+3 -3
View File
@@ -34,12 +34,12 @@ class GrpcConfig {
/// Development configuration pointing to local/development gRPC server.
///
/// Uses plaintext (insecure) credentials for development convenience.
/// Uses TLS with self-signed certificate support for local HTTPS.
static const GrpcConfig development = GrpcConfig(
host: '192.168.88.228',
host: 'localhost',
port: 5011,
timeout: Duration(seconds: 30),
useTls: false,
useTls: true,
allowSelfSignedCertificate: true,
);
+312 -98
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:google_navigation_flutter/google_navigation_flutter.dart';
import '../models/delivery.dart';
import '../theme/color_system.dart';
import '../utils/breakpoints.dart';
import '../utils/toast_helper.dart';
/// Enhanced dark-mode aware map component with custom styling
@@ -36,11 +37,21 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
Brightness? _lastBrightness;
bool _isMapViewReady = false;
bool _isDisposed = false;
bool _isAudioGuidanceEnabled = false; // Audio guidance off by default
bool _pendingNavigationStart = false; // User requested navigation before map was ready
@override
void initState() {
super.initState();
// Set destination if delivery is already selected when widget is created
_updateDestination();
_initializeNavigation();
// Ensure state is synced after a brief delay to allow map to fully load
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted && !_isDisposed) {
_syncNavigationState();
}
});
}
@override
@@ -55,6 +66,8 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedDelivery != widget.selectedDelivery) {
_updateDestination();
// Sync navigation state with actual SDK state
_syncNavigationState();
// If navigation was active, restart navigation to new delivery
if (_isNavigating &&
@@ -95,13 +108,48 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
}
}
/// Sync local navigation state with actual SDK navigation state
Future<void> _syncNavigationState() async {
try {
final bool isActuallyNavigating = await GoogleMapsNavigator.isGuidanceRunning();
if (mounted && !_isDisposed) {
if (_isNavigating != isActuallyNavigating) {
debugPrint('State mismatch detected! Local: $_isNavigating, SDK: $isActuallyNavigating');
setState(() {
_isNavigating = isActuallyNavigating;
});
debugPrint('Navigation state synced to: $_isNavigating');
} else {
debugPrint('Navigation state already in sync: $_isNavigating');
}
}
} catch (e) {
debugPrint('Failed to sync navigation state: $e');
}
}
Future<void> _initializeNavigation() async {
if (_isInitializing || _isSessionInitialized) return;
if (_isInitializing || _isSessionInitialized) {
debugPrint('Skipping initialization: initializing=$_isInitializing, initialized=$_isSessionInitialized');
return;
}
debugPrint('Initializing navigation session...');
setState(() {
_isInitializing = true;
});
// Safety timeout to reset flag if initialization takes too long
Future.delayed(const Duration(seconds: 15), () {
if (mounted && _isInitializing && !_isSessionInitialized) {
debugPrint('Initialization timeout - resetting flag');
setState(() {
_isInitializing = false;
});
}
});
try {
final termsAccepted = await GoogleMapsNavigator.areTermsAccepted();
if (!termsAccepted) {
@@ -148,15 +196,25 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
if (widget.selectedDelivery != null) {
final address = widget.selectedDelivery!.deliveryAddress;
if (address?.latitude != null && address?.longitude != null) {
final lat = address!.latitude!;
final lon = address.longitude!;
setState(() {
_destinationLocation = LatLng(
latitude: address!.latitude!,
longitude: address.longitude!,
latitude: lat,
longitude: lon,
);
});
debugPrint('Destination set to: $lat, $lon for delivery: ${widget.selectedDelivery!.name}');
// Just store the destination, don't move camera
// The navigation will handle camera positioning
} else {
debugPrint('Delivery ${widget.selectedDelivery!.name} has no valid address');
}
} else {
debugPrint('No delivery selected, clearing destination');
setState(() {
_destinationLocation = null;
});
}
}
@@ -167,6 +225,15 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
try {
if (!mounted || _isDisposed) return;
// Get current theme brightness
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
// Force night mode based on theme
await _navigationController!.setForceNightMode(
isDarkMode ? NavigationForceNightMode.forceNight : NavigationForceNightMode.forceDay,
);
debugPrint('Night mode set to: ${isDarkMode ? 'night' : 'day'}');
// Force dark mode map style using Google's standard dark theme
const String darkMapStyle = '''
[
@@ -269,8 +336,51 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
}
}
Future<void> _toggleAudioGuidance() async {
if (_isDisposed) return;
try {
final newState = !_isAudioGuidanceEnabled;
await GoogleMapsNavigator.setAudioGuidance(
NavigationAudioGuidanceSettings(
guidanceType: newState
? NavigationAudioGuidanceType.alertsAndGuidance
: NavigationAudioGuidanceType.silent,
),
);
if (mounted) {
setState(() {
_isAudioGuidanceEnabled = newState;
});
debugPrint('Audio guidance ${newState ? 'enabled' : 'disabled'}');
}
} catch (e) {
debugPrint('Error toggling audio guidance: $e');
}
}
Future<void> _startNavigation() async {
if (_destinationLocation == null) return;
if (_destinationLocation == null) {
debugPrint('Cannot start navigation: no destination set');
return;
}
debugPrint('Starting navigation to: $_destinationLocation');
// Check if map is ready before attempting to start
if (!_isMapViewReady) {
debugPrint('Map not ready yet - navigation will start automatically when ready');
if (mounted) {
setState(() {
_pendingNavigationStart = true;
_isStartingNavigation = true;
_loadingMessage = 'Waiting for map to load...';
});
ToastHelper.showInfo(context, 'Map is loading. Navigation will start automatically...');
}
return;
}
// Show loading indicator
if (mounted) {
@@ -280,6 +390,19 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
});
}
// Safety timeout to reset flag if navigation start takes too long
Future.delayed(const Duration(seconds: 10), () {
if (mounted && _isStartingNavigation) {
debugPrint('Navigation start timeout - resetting flag');
setState(() {
_isStartingNavigation = false;
});
ToastHelper.showError(context, 'Navigation start timed out. Please try again.');
}
});
bool navigationStarted = false;
try {
// Ensure session is initialized before starting navigation
if (!_isSessionInitialized && !_isInitializing) {
@@ -296,9 +419,6 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
if (!_isSessionInitialized) {
if (mounted) {
setState(() {
_isStartingNavigation = false;
});
ToastHelper.showError(context, 'Navigation initialization timeout');
}
return;
@@ -364,6 +484,7 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
setState(() {
_isNavigating = true;
_isStartingNavigation = false;
_pendingNavigationStart = false; // Clear pending flag on success
});
}
} catch (e) {
@@ -374,6 +495,7 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
if (mounted) {
setState(() {
_isStartingNavigation = false;
_pendingNavigationStart = false; // Clear pending flag on error
});
ToastHelper.showError(context, 'Navigation error: $errorMessage', duration: const Duration(seconds: 4));
@@ -396,14 +518,28 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
await GoogleMapsNavigator.stopGuidance();
await GoogleMapsNavigator.clearDestinations();
// Wait a moment for the SDK to fully stop
await Future.delayed(const Duration(milliseconds: 200));
// Verify navigation actually stopped by checking SDK state
final bool isStillRunning = await GoogleMapsNavigator.isGuidanceRunning();
if (mounted) {
setState(() {
_isNavigating = false;
_isNavigating = isStillRunning;
_pendingNavigationStart = false; // Clear pending flag when stopping
});
debugPrint('Navigation stopped, state synced: $_isNavigating');
}
} catch (e) {
if (mounted) {
debugPrint('Navigation stop error: $e');
setState(() {
_pendingNavigationStart = false; // Clear pending flag on error
});
// Even on error, try to sync state
await _syncNavigationState();
}
}
}
@@ -429,12 +565,17 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
@override
Widget build(BuildContext context) {
// Driver's current location (defaults to Montreal if not available)
final initialPosition = const LatLng(latitude: 45.5017, longitude: -73.5673);
// Driver's current location (defaults to Trois-Rivières if not available)
final initialPosition = const LatLng(latitude: 46.33857534324389, longitude: -72.60787418369715);
// Get safe area insets to avoid rounded corners and notches
final bottomSafeArea = MediaQuery.of(context).padding.bottom;
// Calculate dynamic padding for bottom button bar
// Must account for full button container height to prevent map showing underneath
// Button container height = top padding (8) + button height (~44) + bottom padding (bottomSafeArea + 8)
final topPadding = 0.0;
final bottomPadding = 60.0;
final bottomPadding = 100.0; // Full height of button container to properly cut off map
return Stack(
children: [
@@ -477,11 +618,34 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
if (!mounted || _isDisposed) return;
await controller.setNavigationFooterEnabled(true);
if (!mounted || _isDisposed) return;
await controller.setNavigationTripProgressBarEnabled(true);
// Disable navigation trip progress bar
await controller.setNavigationTripProgressBarEnabled(false);
if (!mounted || _isDisposed) return;
// Enable recenter button
await controller.setRecenterButtonEnabled(true);
if (!mounted || _isDisposed) return;
// Enable speed limit icon
await controller.setSpeedLimitIconEnabled(true);
if (!mounted || _isDisposed) return;
// Enable traffic incident cards
await controller.setTrafficIncidentCardsEnabled(true);
if (!mounted || _isDisposed) return;
// Disable traffic prompts
await controller.setTrafficPromptsEnabled(false);
if (!mounted || _isDisposed) return;
// Disable report incident button
await controller.setReportIncidentButtonEnabled(false);
debugPrint('Navigation UI elements enabled');
if (!mounted || _isDisposed) return;
// Enable speedometer
await controller.setSpeedometerEnabled(true);
if (!mounted || _isDisposed) return;
// Set audio guidance to silent by default
await GoogleMapsNavigator.setAudioGuidance(
NavigationAudioGuidanceSettings(
guidanceType: NavigationAudioGuidanceType.silent,
),
);
debugPrint('Navigation UI elements configured');
// Configure map settings to reduce GPU load for devices with limited graphics capabilities
if (!mounted || _isDisposed) return;
@@ -500,40 +664,42 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
if (!mounted || _isDisposed) return;
await _applyDarkModeStyle();
// Wrap camera animation in try-catch to handle "No valid view found" errors
// This can happen on Android when the view isn't fully ready
// Immediately follow user location on map initialization
try {
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
await controller.animateCamera(
CameraUpdate.newLatLngZoom(initialPosition, 12),
);
// Start following user location immediately
await _recenterMap();
debugPrint('Map initialized following user location');
// Sync navigation state to ensure button reflects actual navigation state
await _syncNavigationState();
// Auto-recenter to current location after initial setup
await Future.delayed(const Duration(milliseconds: 500));
if (mounted && _navigationController != null && !_isDisposed) {
await _recenterMap();
debugPrint('Auto-recentered map to current location on initialization');
// Auto-start navigation if user requested it before map was ready
if (_pendingNavigationStart && !_isNavigating && !_isDisposed && mounted) {
debugPrint('Auto-starting navigation as requested by user');
_pendingNavigationStart = false;
await _startNavigation();
}
}
} catch (e) {
debugPrint('Camera animation error (view may not be ready): $e');
debugPrint('Follow location error (view may not be ready): $e');
if (_isDisposed || !mounted) return;
// Retry once after a longer delay
await Future.delayed(const Duration(milliseconds: 1500));
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
try {
await controller.animateCamera(
CameraUpdate.newLatLngZoom(initialPosition, 12),
);
await _recenterMap();
debugPrint('Map initialized following user location (retry)');
// Sync navigation state to ensure button reflects actual navigation state
await _syncNavigationState();
// Auto-recenter to current location after retry
await Future.delayed(const Duration(milliseconds: 500));
if (mounted && _navigationController != null && !_isDisposed) {
await _recenterMap();
debugPrint('Auto-recentered map to current location on initialization (retry)');
// Auto-start navigation if user requested it before map was ready
if (_pendingNavigationStart && !_isNavigating && !_isDisposed && mounted) {
debugPrint('Auto-starting navigation as requested by user (after retry)');
_pendingNavigationStart = false;
await _startNavigation();
}
} catch (e2) {
debugPrint('Camera animation retry failed: $e2');
debugPrint('Follow location retry failed: $e2');
}
}
}
@@ -560,63 +726,108 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
),
],
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
padding: EdgeInsets.only(
left: 12,
right: 12,
top: 8,
// Add safe area padding plus extra margin to avoid rounded corners
bottom: bottomSafeArea > 0 ? bottomSafeArea + 8 : 16,
),
child: Row(
children: [
// Start button
Expanded(
child: _buildBottomActionButton(
label: _isNavigating ? 'Stop' : 'Start',
icon: _isNavigating ? Icons.stop : Icons.navigation,
onPressed: _isStartingNavigation || _isInitializing || (widget.selectedDelivery == null && !_isNavigating)
? null
: (_isNavigating ? _stopNavigation : _startNavigation),
isDanger: _isNavigating,
),
),
const SizedBox(width: 8),
// Photo button (disabled when no delivery selected or warehouse delivery)
Expanded(
child: _buildBottomActionButton(
label: 'Photo',
icon: Icons.camera_alt,
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
? () => widget.onAction?.call('photo')
: null,
),
),
const SizedBox(width: 8),
// Note button (only enabled if delivery has notes and not warehouse)
Expanded(
child: _buildBottomActionButton(
label: 'Note',
icon: Icons.note_add,
onPressed: _hasNotes() && widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
? () => widget.onAction?.call('note')
: null,
),
),
const SizedBox(width: 8),
// Completed button (disabled for warehouse delivery)
Expanded(
child: _buildBottomActionButton(
label: widget.selectedDelivery?.delivered == true ? 'Undo' : 'Completed',
icon: widget.selectedDelivery?.delivered == true ? Icons.undo : Icons.check_circle,
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
? () => widget.onAction?.call(
widget.selectedDelivery!.delivered ? 'uncomplete' : 'complete',
)
: null,
isPrimary: widget.selectedDelivery != null && !widget.selectedDelivery!.delivered && !widget.selectedDelivery!.isWarehouseDelivery,
),
),
],
child: Builder(
builder: (context) {
final isMobile = context.isMobile;
final showButtonLabels = !isMobile;
return Row(
children: [
// Start button
Expanded(
child: _buildBottomActionButton(
label: _isNavigating ? 'Stop' : 'Start',
icon: _isNavigating ? Icons.stop : Icons.navigation,
onPressed: _isStartingNavigation || _isInitializing || (!_isMapViewReady && !_isNavigating) || (widget.selectedDelivery == null && !_isNavigating)
? null
: (_isNavigating ? _stopNavigation : _startNavigation),
isDanger: _isNavigating,
showLabel: showButtonLabels,
),
),
const SizedBox(width: 8),
// Photo button (disabled when no delivery selected or warehouse delivery)
Expanded(
child: _buildBottomActionButton(
label: 'Photo',
icon: Icons.camera_alt,
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
? () => widget.onAction?.call('photo')
: null,
showLabel: showButtonLabels,
),
),
const SizedBox(width: 8),
// Note button (only enabled if delivery has notes and not warehouse)
Expanded(
child: _buildBottomActionButton(
label: 'Note',
icon: Icons.note_add,
onPressed: _hasNotes() && widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
? () => widget.onAction?.call('note')
: null,
showLabel: showButtonLabels,
),
),
const SizedBox(width: 8),
// Completed button (disabled for warehouse delivery)
Expanded(
child: _buildBottomActionButton(
label: widget.selectedDelivery?.delivered == true ? 'Undo' : 'Completed',
icon: widget.selectedDelivery?.delivered == true ? Icons.undo : Icons.check_circle,
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
? () => widget.onAction?.call(
widget.selectedDelivery!.delivered ? 'uncomplete' : 'complete',
)
: null,
isPrimary: widget.selectedDelivery != null && !widget.selectedDelivery!.delivered && !widget.selectedDelivery!.isWarehouseDelivery,
showLabel: showButtonLabels,
),
),
],
);
},
),
),
),
// Audio guidance toggle button - positioned below green navigation card
Positioned(
top: topPadding + 48,
right: 16,
child: Material(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(
_isAudioGuidanceEnabled ? Icons.volume_up : Icons.volume_off,
color: _isAudioGuidanceEnabled
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
onPressed: _toggleAudioGuidance,
tooltip: _isAudioGuidanceEnabled ? 'Mute navigation' : 'Unmute navigation',
),
),
),
),
// Loading overlay during navigation initialization and start
if (_isStartingNavigation || _isInitializing)
Positioned.fill(
@@ -684,6 +895,7 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
required VoidCallback? onPressed,
bool isPrimary = false,
bool isDanger = false,
bool showLabel = true,
}) {
Color backgroundColor;
Color textColor = Colors.white;
@@ -709,9 +921,9 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
onTap: onPressed,
borderRadius: BorderRadius.circular(6),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 10,
padding: EdgeInsets.symmetric(
horizontal: showLabel ? 8 : 12,
vertical: showLabel ? 10 : 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -720,17 +932,19 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
Icon(
icon,
color: textColor,
size: 18,
size: showLabel ? 18 : 20,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w600,
fontSize: 14,
if (showLabel) ...[
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
],
),
),
+16 -16
View File
@@ -125,7 +125,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
borderRadius: BorderRadius.circular(10),
border: widget.isSelected
? Border.all(
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
width: 3,
)
: null,
@@ -134,7 +134,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
BoxShadow(
color: widget.isSelected
? statusColor.withValues(alpha: 0.5)
: Colors.black.withValues(
: Theme.of(context).colorScheme.scrim.withValues(
alpha: isDark ? 0.3 : 0.15,
),
blurRadius: widget.isSelected ? 12 : 8,
@@ -146,15 +146,15 @@ class _DeliveryListItemState extends State<DeliveryListItem>
),
child: Center(
child: widget.delivery.isWarehouseDelivery
? const Icon(
? Icon(
Icons.warehouse,
color: Colors.white,
color: Theme.of(context).colorScheme.onPrimary,
size: 32,
)
: Text(
'${widget.delivery.deliveryIndex + 1}',
style: const TextStyle(
color: Colors.white,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 26,
fontWeight: FontWeight.w700,
),
@@ -178,10 +178,10 @@ class _DeliveryListItemState extends State<DeliveryListItem>
),
child: Transform.rotate(
angle: 4.71239, // 270 degrees in radians (3*pi/2)
child: const Icon(
child: Icon(
Icons.note,
size: 12,
color: Colors.white,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
@@ -232,7 +232,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
boxShadow: (_isHovered || widget.isSelected) && !widget.delivery.delivered
? [
BoxShadow(
color: Colors.black.withValues(
color: Theme.of(context).colorScheme.scrim.withValues(
alpha: isDark ? 0.3 : 0.08,
),
blurRadius: 8,
@@ -261,15 +261,15 @@ class _DeliveryListItemState extends State<DeliveryListItem>
),
child: Center(
child: widget.delivery.isWarehouseDelivery
? const Icon(
? Icon(
Icons.warehouse,
color: Colors.white,
color: Theme.of(context).colorScheme.onPrimary,
size: 24,
)
: Text(
'${widget.delivery.deliveryIndex + 1}',
style: const TextStyle(
color: Colors.white,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
@@ -343,7 +343,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
color: Theme.of(context).colorScheme.scrim.withValues(alpha: 0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -351,10 +351,10 @@ class _DeliveryListItemState extends State<DeliveryListItem>
),
child: Transform.rotate(
angle: 4.71239, // 270 degrees in radians (3*pi/2)
child: const Icon(
child: Icon(
Icons.note,
size: 14,
color: Colors.white,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
+5 -5
View File
@@ -50,21 +50,21 @@ class _GlassmorphicRouteCardState extends State<GlassmorphicRouteCard>
// Red to orange (0-30%)
return Color.lerp(
SvrntyColors.crimsonRed,
const Color(0xFFFF9800),
SvrntyColors.progressLow,
(progress / 0.3),
)!;
} else if (progress < 0.7) {
// Orange to yellow (30-70%)
return Color.lerp(
const Color(0xFFFF9800),
const Color(0xFFFFC107),
SvrntyColors.progressLow,
SvrntyColors.progressMedium,
((progress - 0.3) / 0.4),
)!;
} else {
// Yellow to green (70-100%)
return Color.lerp(
const Color(0xFFFFC107),
const Color(0xFF4CAF50),
SvrntyColors.progressMedium,
SvrntyColors.progressHigh,
((progress - 0.7) / 0.3),
)!;
}
+13 -6
View File
@@ -193,8 +193,8 @@ class _MobileMapWithOverlayState extends ConsumerState<MobileMapWithOverlay>
scrollController: _listScrollController,
onDeliverySelected: (delivery) {
widget.onDeliverySelected(delivery);
// Optionally close the overlay after selection
// _toggleList();
// Auto-close the overlay after selection for better mobile UX
_toggleList();
},
onItemAction: (delivery, action) {
widget.onDeliveryAction(delivery, action);
@@ -213,14 +213,21 @@ class _MobileMapWithOverlayState extends ConsumerState<MobileMapWithOverlay>
// Floating toggle button (FAB) - only show when list is closed
if (!isListOpen)
Positioned(
bottom: 80, // Above bottom action buttons
bottom: 110, // Slightly lowered for better positioning
right: 16,
child: FloatingActionButton.extended(
heroTag: 'mobile_deliveries_toggle_fab',
onPressed: _toggleList,
icon: const Icon(Icons.list),
label: Text('$_completedCount/$_totalCount'),
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
label: Text(
'$_completedCount/$_totalCount',
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 15,
),
),
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
elevation: 4,
),
),
+1
View File
@@ -318,6 +318,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
endpoint: 'completeDelivery',
command: CompleteDeliveryCommand(
deliveryId: delivery.id,
deliveredAt: DateTime.now().toUtc().toIso8601String(),
),
);
result.when(
+65 -78
View File
@@ -6,9 +6,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:http/http.dart' as http;
import '../models/delivery.dart';
import '../models/delivery_route.dart';
import '../models/delivery_commands.dart';
import '../providers/providers.dart';
import '../api/client.dart';
import '../utils/toast_helper.dart';
import '../api/openapi_config.dart';
import '../utils/http_client_factory.dart';
@@ -76,7 +74,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
void _backToRoutes() {
setState(() {
_selectedRoute = null;
_selectedDelivery = null;
// Keep _selectedDelivery to preserve selection when returning to map
});
}
@@ -98,11 +96,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
return;
}
// Create API client with auth service for automatic token refresh
final authClient = CqrsApiClient(
config: ApiClientConfig.development,
authService: authService,
);
// Use gRPC client for commands
final grpcClient = ref.read(grpcClientProvider);
switch (action) {
case 'complete':
@@ -110,11 +105,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
LoadingDialog.show(context, message: l10n.completingDelivery);
}
final result = await authClient.executeCommand(
endpoint: 'completeDelivery',
command: CompleteDeliveryCommand(
deliveryId: delivery.id,
),
final result = await grpcClient.completeDelivery(
deliveryId: delivery.id,
);
result.when(
success: (_) async {
@@ -123,17 +115,13 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
}
if (mounted) {
// Invalidate both providers to force refresh
ref.invalidate(deliveriesProvider(routeFragmentId));
ref.invalidate(allDeliveriesProvider);
ref.invalidate(deliveryRoutesProvider);
// Wait for providers to refresh
await Future.delayed(const Duration(milliseconds: 500));
// Refresh providers to force fresh data fetch
await ref.refresh(deliveriesProvider(routeFragmentId).future);
await ref.refresh(allDeliveriesProvider.future);
if (mounted) {
// Get refreshed deliveries
final allDeliveries = await ref.read(allDeliveriesProvider.future);
final allDeliveries = ref.read(allDeliveriesProvider).value ?? [];
final routeDeliveries = allDeliveries
.where((d) => d.routeFragmentId == routeFragmentId)
.toList();
@@ -172,12 +160,17 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
debugPrint('Complete delivery failed - Type: ${error.type}, Message: ${error.message}');
debugPrint('Error details: ${error.details}');
debugPrint('Status code: ${error.statusCode}');
if (mounted) {
String errorMessage = l10n.error(error.message);
if (error.statusCode == 500) {
errorMessage = l10n.serverError;
} else if (error.statusCode == 401) {
errorMessage = 'Unauthorized: You may not have permission to complete this delivery. Please check with your administrator.';
} else if (error.details != null) {
errorMessage = 'Error: ${error.details}';
}
ToastHelper.showError(context, errorMessage);
ToastHelper.showError(context, errorMessage, duration: const Duration(seconds: 5));
}
},
);
@@ -188,9 +181,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
LoadingDialog.show(context, message: l10n.markingAsUncompleted);
}
final uncompleteResult = await authClient.executeCommand(
endpoint: 'markDeliveryAsUncompleted',
command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id),
final uncompleteResult = await grpcClient.markDeliveryAsUncompleted(
deliveryId: delivery.id,
);
uncompleteResult.when(
success: (_) async {
@@ -199,17 +191,13 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
}
if (mounted) {
// Invalidate both providers to force refresh
ref.invalidate(deliveriesProvider(routeFragmentId));
ref.invalidate(allDeliveriesProvider);
ref.invalidate(deliveryRoutesProvider);
// Wait for providers to refresh
await Future.delayed(const Duration(milliseconds: 500));
// Refresh providers to force fresh data fetch
await ref.refresh(deliveriesProvider(routeFragmentId).future);
await ref.refresh(allDeliveriesProvider.future);
if (mounted) {
// Get refreshed deliveries
final allDeliveries = await ref.read(allDeliveriesProvider.future);
final allDeliveries = ref.read(allDeliveriesProvider).value ?? [];
final updatedDelivery = allDeliveries.firstWhere(
(d) => d.id == delivery.id,
orElse: () => delivery,
@@ -299,12 +287,12 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
try {
final Uri uploadUrl = Uri.parse(
'${ApiClientConfig.development.baseUrl}/api/delivery/uploadDeliveryPicture?deliveryId=${delivery.id}',
'${ApiClientConfig.production.baseUrl}/api/delivery/uploadDeliveryPicture?deliveryId=${delivery.id}',
);
// Create HTTP client that accepts self-signed certificates
final client = HttpClientFactory.createClient(
allowSelfSigned: ApiClientConfig.development.allowSelfSignedCertificate,
allowSelfSigned: ApiClientConfig.production.allowSelfSignedCertificate,
);
final http.MultipartRequest request = http.MultipartRequest('POST', uploadUrl);
@@ -385,28 +373,40 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
final userProfile = ref.watch(userProfileProvider);
final l10n = AppLocalizations.of(context);
final isMobile = context.isMobile;
return Scaffold(
appBar: AppBar(
leading: (isMobile && _selectedRoute != null)
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: _backToRoutes,
tooltip: 'Back to routes',
)
: null,
title: Text(l10n.deliveryRoutes),
elevation: 0,
scrolledUnderElevation: 0,
actions: [
IconButton(
icon: (routesData.isLoading || allDeliveriesData.isLoading)
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: (routesData.isLoading || allDeliveriesData.isLoading)
? null
: () {
ref.invalidate(deliveryRoutesProvider);
ref.invalidate(allDeliveriesProvider);
},
tooltip: 'Refresh',
),
// Hide refresh button when on map view (mobile + route selected)
// Google Maps Navigation has its own built-in volume controls
if (!(isMobile && _selectedRoute != null))
IconButton(
icon: (routesData.isLoading || allDeliveriesData.isLoading)
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: (routesData.isLoading || allDeliveriesData.isLoading)
? null
: () {
ref.invalidate(deliveryRoutesProvider);
ref.invalidate(allDeliveriesProvider);
},
tooltip: 'Refresh',
),
userProfile.when(
data: (profile) {
String getInitials(String? fullName) {
@@ -431,12 +431,13 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
child: CircleAvatar(
radius: 16,
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
child: Text(
getInitials(profile?.fullName),
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 14,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
),
),
),
@@ -496,32 +497,18 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
// ignore: unused_result
ref.refresh(allDeliveriesProvider);
},
child: Stack(
children: [
MobileMapWithOverlay(
deliveries: routeDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
_autoShowNotesIfNeeded(delivery);
},
onDeliveryAction: (delivery, action) {
_handleDeliveryAction(action, delivery, _selectedRoute!.id);
},
),
// Back button to return to routes list
Positioned(
top: 16,
left: 16,
child: FloatingActionButton.small(
onPressed: _backToRoutes,
backgroundColor: Theme.of(context).colorScheme.surface,
child: const Icon(Icons.arrow_back),
),
),
],
child: MobileMapWithOverlay(
deliveries: routeDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
_autoShowNotesIfNeeded(delivery);
},
onDeliveryAction: (delivery, action) {
_handleDeliveryAction(action, delivery, _selectedRoute!.id);
},
),
);
}
+171 -63
View File
@@ -31,7 +31,10 @@ class SettingsPage extends ConsumerWidget {
children: [
Text(
l10n.profile,
style: Theme.of(context).textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
userProfile.when(
@@ -49,9 +52,14 @@ class SettingsPage extends ConsumerWidget {
children: [
CircleAvatar(
radius: 32,
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
child: Text(
profile.firstName[0].toUpperCase(),
style: Theme.of(context).textTheme.titleLarge,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 16),
@@ -61,34 +69,53 @@ class SettingsPage extends ConsumerWidget {
children: [
Text(
profile.fullName,
style: Theme.of(context).textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
profile.email,
style: Theme.of(context).textTheme.bodySmall,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
],
),
),
IconButton.filled(
icon: const Icon(Icons.logout),
onPressed: () async {
final authService = ref.read(authServiceProvider);
await authService.logout();
if (context.mounted) {
// ignore: unused_result
ref.refresh(isAuthenticatedProvider);
if (context.mounted) {
Navigator.of(context).pushReplacementNamed('/');
}
}
},
color: Theme.of(context).colorScheme.error,
tooltip: l10n.logout,
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton.tonalIcon(
onPressed: () async {
final authService = ref.read(authServiceProvider);
await authService.logout();
if (context.mounted) {
// ignore: unused_result
ref.refresh(isAuthenticatedProvider);
if (context.mounted) {
Navigator.of(context).pushReplacementNamed('/');
}
}
},
icon: const Icon(Icons.logout),
label: Text(
l10n.logout,
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.errorContainer,
foregroundColor: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
],
),
),
@@ -108,17 +135,32 @@ class SettingsPage extends ConsumerWidget {
children: [
Text(
l10n.preferences,
style: Theme.of(context).textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
ListTile(
title: Text(l10n.language),
title: Text(
l10n.language,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
subtitle: Text(
language == 'system'
? l10n.systemLanguage
: language == 'fr'
? l10n.french
: l10n.english
: l10n.english,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
trailing: DropdownButton<String>(
value: language,
@@ -130,52 +172,105 @@ class SettingsPage extends ConsumerWidget {
items: [
DropdownMenuItem(
value: 'system',
child: Text(l10n.systemLanguage),
child: Text(
l10n.systemLanguage,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
DropdownMenuItem(
value: 'en',
child: Text(l10n.english),
child: Text(
l10n.english,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
DropdownMenuItem(
value: 'fr',
child: Text(l10n.french),
child: Text(
l10n.french,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
),
const SizedBox(height: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.theme,
style: Theme.of(context).textTheme.titleSmall,
ListTile(
title: Text(
l10n.theme,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w700,
fontSize: 16,
),
const SizedBox(height: 8),
SegmentedButton<ThemeMode>(
selected: {themeMode},
onSelectionChanged: (Set<ThemeMode> newSelection) {
ref.read(themeModeProvider.notifier).setThemeMode(newSelection.first);
},
segments: [
ButtonSegment<ThemeMode>(
value: ThemeMode.light,
label: Text(l10n.themeLight),
icon: const Icon(Icons.light_mode),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.dark,
label: Text(l10n.themeDark),
icon: const Icon(Icons.dark_mode),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.system,
label: Text(l10n.themeSystem),
icon: const Icon(Icons.brightness_auto),
),
],
),
subtitle: Text(
themeMode == ThemeMode.light
? l10n.themeLight
: themeMode == ThemeMode.dark
? l10n.themeDark
: 'Device',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
],
),
trailing: DropdownButton<ThemeMode>(
value: themeMode,
onChanged: (ThemeMode? newValue) {
if (newValue != null) {
ref.read(themeModeProvider.notifier).setThemeMode(newValue);
}
},
items: [
DropdownMenuItem(
value: ThemeMode.light,
child: Text(
l10n.themeLight,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
DropdownMenuItem(
value: ThemeMode.dark,
child: Text(
l10n.themeDark,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
DropdownMenuItem(
value: ThemeMode.system,
child: Text(
'Device',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
),
],
),
@@ -188,16 +283,29 @@ class SettingsPage extends ConsumerWidget {
children: [
Text(
l10n.about,
style: Theme.of(context).textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
ListTile(
title: Text(l10n.appVersion),
subtitle: const Text('1.0.0'),
),
ListTile(
title: Text(l10n.builtWithFlutter),
subtitle: Text(l10n.appDescription),
title: Text(
l10n.appVersion,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
subtitle: Text(
'1.0.0',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
+5 -5
View File
@@ -112,7 +112,7 @@ final authServiceProvider = Provider<AuthService>((ref) {
final apiClientProvider = Provider<CqrsApiClient>((ref) {
final authService = ref.watch(authServiceProvider);
return CqrsApiClient(
config: ApiClientConfig.development,
config: ApiClientConfig.production,
authService: authService,
);
});
@@ -171,7 +171,7 @@ final _httpDeliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) as
// Create a new client with auth service for automatic token refresh
final authClient = CqrsApiClient(
config: ApiClientConfig.development,
config: ApiClientConfig.production,
authService: authService,
);
@@ -313,7 +313,7 @@ final _httpDeliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref,
}
final authClient = CqrsApiClient(
config: ApiClientConfig.development,
config: ApiClientConfig.production,
authService: authService,
);
@@ -381,7 +381,7 @@ final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, rout
/// Provider to get all deliveries from all routes
final allDeliveriesProvider = FutureProvider<List<Delivery>>((ref) async {
final routes = await ref.read(deliveryRoutesProvider.future);
final routes = await ref.watch(deliveryRoutesProvider.future);
if (routes.isEmpty) {
return [];
@@ -389,7 +389,7 @@ final allDeliveriesProvider = FutureProvider<List<Delivery>>((ref) async {
// Fetch deliveries for all routes in parallel using Future.wait
final deliveriesFutures = routes.map((route) {
return ref.read(deliveriesProvider(route.id).future);
return ref.watch(deliveriesProvider(route.id).future);
}).toList();
// Wait for all futures to complete
+181 -425
View File
@@ -6,55 +6,55 @@ class MaterialTheme {
const MaterialTheme(this.textTheme);
// Svrnty Brand Colors - Light Theme
// Svrnty Brand Colors - Light Theme (Enhanced Contrast)
static ColorScheme lightScheme() {
return const ColorScheme(
brightness: Brightness.light,
primary: Color(0xffDF2D45), // Svrnty Crimson Red (updated)
surfaceTint: Color(0xffDF2D45),
primary: Color(0xffC91F37), // Darker Crimson Red for better contrast
surfaceTint: Color(0xffC91F37),
onPrimary: Color(0xffffffff),
primaryContainer: Color(0xffFFE0E5),
onPrimaryContainer: Color(0xff06080C),
secondary: Color(0xff3A4958), // Svrnty Dark Slate
primaryContainer: Color(0xffFFE5E9),
onPrimaryContainer: Color(0xff2D0009),
secondary: Color(0xff2D3843), // Darker Slate for better contrast
onSecondary: Color(0xffffffff),
secondaryContainer: Color(0xffD0DCE8),
onSecondaryContainer: Color(0xff06080C),
tertiary: Color(0xff1D2C39), // Svrnty Teal
secondaryContainer: Color(0xffE0E7EE),
onSecondaryContainer: Color(0xff0A0F15),
tertiary: Color(0xff16803D), // Darker Green for contrast
onTertiary: Color(0xffffffff),
tertiaryContainer: Color(0xffBFD5E3),
onTertiaryContainer: Color(0xff06080C),
error: Color(0xffEF4444),
tertiaryContainer: Color(0xffD1F4DD),
onTertiaryContainer: Color(0xff00210B),
error: Color(0xffD32F2F),
onError: Color(0xffffffff),
errorContainer: Color(0xffFEE2E2),
onErrorContainer: Color(0xff7F1D1D),
surface: Color(0xffFAFAFC),
onSurface: Color(0xff06080C),
onSurfaceVariant: Color(0xff2D3843), // Enhanced contrast: 7.2:1 (WCAG AAA)
outline: Color(0xff737A82), // Enhanced contrast: 4.6:1
outlineVariant: Color(0xffD1D5DB),
shadow: Color(0x1A000000),
errorContainer: Color(0xffFFEBEE),
onErrorContainer: Color(0xff5F0000),
surface: Color(0xffFCFCFC),
onSurface: Color(0xff1A1C1E), // Very dark gray for maximum contrast
onSurfaceVariant: Color(0xff3E4A56), // Darker gray for secondary text (7:1 contrast)
outline: Color(0xff5F6B77), // Darker outline (4.5:1 contrast)
outlineVariant: Color(0xffC4C7CC),
shadow: Color(0x1F000000),
scrim: Color(0xff000000),
inverseSurface: Color(0xff06080C),
inverseSurface: Color(0xff1A1C1E),
inversePrimary: Color(0xffFF6B7D),
primaryFixed: Color(0xffFFE0E5),
onPrimaryFixed: Color(0xff06080C),
primaryFixedDim: Color(0xffFFC0C9),
primaryFixed: Color(0xffFFE5E9),
onPrimaryFixed: Color(0xff2D0009),
primaryFixedDim: Color(0xffFFB3C0),
onPrimaryFixedVariant: Color(0xff8B1A2A),
secondaryFixed: Color(0xffD0DCE8),
onSecondaryFixed: Color(0xff06080C),
secondaryFixedDim: Color(0xffB0C4D8),
onSecondaryFixedVariant: Color(0xff3A4958),
tertiaryFixed: Color(0xffBFD5E3),
onTertiaryFixed: Color(0xff06080C),
tertiaryFixedDim: Color(0xff9FBDCF),
onTertiaryFixedVariant: Color(0xff1D2C39),
surfaceDim: Color(0xffdadcde),
surfaceBright: Color(0xfffafafa),
secondaryFixed: Color(0xffE0E7EE),
onSecondaryFixed: Color(0xff0A0F15),
secondaryFixedDim: Color(0xffA8B8C8),
onSecondaryFixedVariant: Color(0xff2D3843),
tertiaryFixed: Color(0xffD1F4DD),
onTertiaryFixed: Color(0xff00210B),
tertiaryFixedDim: Color(0xff9FD8B1),
onTertiaryFixedVariant: Color(0xff16803D),
surfaceDim: Color(0xffDEE1E4),
surfaceBright: Color(0xffFCFCFC),
surfaceContainerLowest: Color(0xffffffff),
surfaceContainerLow: Color(0xfff6f6f8),
surfaceContainer: Color(0xfff1f1f4),
surfaceContainerHigh: Color(0xffebebee),
surfaceContainerHighest: Color(0xffe5e5e8),
surfaceContainerLow: Color(0xffF5F6F7),
surfaceContainer: Color(0xffEFF1F3),
surfaceContainerHigh: Color(0xffE9EBED),
surfaceContainerHighest: Color(0xffE3E5E8),
);
}
@@ -62,165 +62,55 @@ class MaterialTheme {
return theme(lightScheme());
}
static ColorScheme lightMediumContrastScheme() {
return const ColorScheme(
brightness: Brightness.light,
primary: Color(0xff0d3665),
surfaceTint: Color(0xff3d5f90),
onPrimary: Color(0xffffffff),
primaryContainer: Color(0xff4d6ea0),
onPrimaryContainer: Color(0xffffffff),
secondary: Color(0xff2d3747),
onSecondary: Color(0xffffffff),
secondaryContainer: Color(0xff636d80),
onSecondaryContainer: Color(0xffffffff),
tertiary: Color(0xff442e4c),
onTertiary: Color(0xffffffff),
tertiaryContainer: Color(0xff7d6485),
onTertiaryContainer: Color(0xffffffff),
error: Color(0xff740006),
onError: Color(0xffffffff),
errorContainer: Color(0xffcf2c27),
onErrorContainer: Color(0xffffffff),
surface: Color(0xfff9f9ff),
onSurface: Color(0xff0f1116),
onSurfaceVariant: Color(0xff33363d),
outline: Color(0xff4f525a),
outlineVariant: Color(0xff6a6d75),
shadow: Color(0xff000000),
scrim: Color(0xff000000),
inverseSurface: Color(0xff2e3035),
inversePrimary: Color(0xffa6c8ff),
primaryFixed: Color(0xff4d6ea0),
onPrimaryFixed: Color(0xffffffff),
primaryFixedDim: Color(0xff335686),
onPrimaryFixedVariant: Color(0xffffffff),
secondaryFixed: Color(0xff636d80),
onSecondaryFixed: Color(0xffffffff),
secondaryFixedDim: Color(0xff4b5567),
onSecondaryFixedVariant: Color(0xffffffff),
tertiaryFixed: Color(0xff7d6485),
onTertiaryFixed: Color(0xffffffff),
tertiaryFixedDim: Color(0xff644c6c),
onTertiaryFixedVariant: Color(0xffffffff),
surfaceDim: Color(0xffc5c6cd),
surfaceBright: Color(0xfff9f9ff),
surfaceContainerLowest: Color(0xffffffff),
surfaceContainerLow: Color(0xfff3f3fa),
surfaceContainer: Color(0xffe7e8ee),
surfaceContainerHigh: Color(0xffdcdce3),
surfaceContainerHighest: Color(0xffd0d1d8),
);
}
ThemeData lightMediumContrast() {
return theme(lightMediumContrastScheme());
}
static ColorScheme lightHighContrastScheme() {
return const ColorScheme(
brightness: Brightness.light,
primary: Color(0xff002c58),
surfaceTint: Color(0xff3d5f90),
onPrimary: Color(0xffffffff),
primaryContainer: Color(0xff264a79),
onPrimaryContainer: Color(0xffffffff),
secondary: Color(0xff232d3d),
onSecondary: Color(0xffffffff),
secondaryContainer: Color(0xff404a5b),
onSecondaryContainer: Color(0xffffffff),
tertiary: Color(0xff392441),
onTertiary: Color(0xffffffff),
tertiaryContainer: Color(0xff584160),
onTertiaryContainer: Color(0xffffffff),
error: Color(0xff600004),
onError: Color(0xffffffff),
errorContainer: Color(0xff98000a),
onErrorContainer: Color(0xffffffff),
surface: Color(0xfff9f9ff),
onSurface: Color(0xff000000),
onSurfaceVariant: Color(0xff000000),
outline: Color(0xff292c33),
outlineVariant: Color(0xff464951),
shadow: Color(0xff000000),
scrim: Color(0xff000000),
inverseSurface: Color(0xff2e3035),
inversePrimary: Color(0xffa6c8ff),
primaryFixed: Color(0xff264a79),
onPrimaryFixed: Color(0xffffffff),
primaryFixedDim: Color(0xff063361),
onPrimaryFixedVariant: Color(0xffffffff),
secondaryFixed: Color(0xff404a5b),
onSecondaryFixed: Color(0xffffffff),
secondaryFixedDim: Color(0xff293343),
onSecondaryFixedVariant: Color(0xffffffff),
tertiaryFixed: Color(0xff584160),
onTertiaryFixed: Color(0xffffffff),
tertiaryFixedDim: Color(0xff402b48),
onTertiaryFixedVariant: Color(0xffffffff),
surfaceDim: Color(0xffb7b8bf),
surfaceBright: Color(0xfff9f9ff),
surfaceContainerLowest: Color(0xffffffff),
surfaceContainerLow: Color(0xfff0f0f7),
surfaceContainer: Color(0xffe1e2e9),
surfaceContainerHigh: Color(0xffd3d4da),
surfaceContainerHighest: Color(0xffc5c6cd),
);
}
ThemeData lightHighContrast() {
return theme(lightHighContrastScheme());
}
// Svrnty Brand Colors - Dark Theme
// Svrnty Brand Colors - Dark Theme (Forest Green with Maximum Contrast)
static ColorScheme darkScheme() {
return const ColorScheme(
brightness: Brightness.dark,
primary: Color(0xffDF2D45), // Svrnty Crimson Red
primary: Color(0xffFF5A6D), // Bright Crimson Red for dark mode
surfaceTint: Color(0xff4ADE80), // Success Green tint
onPrimary: Color(0xffffffff),
onPrimary: Color(0xff000000), // Black text on bright primary
primaryContainer: Color(0xff9C1A29),
onPrimaryContainer: Color(0xffFFE0E5),
secondary: Color(0xff506576), // Svrnty Slate Gray
onSecondary: Color(0xffffffff),
onPrimaryContainer: Color(0xffFFE5E8),
secondary: Color(0xffA5B6C8), // Very light Slate Gray
onSecondary: Color(0xff0C1410),
secondaryContainer: Color(0xff3A4958),
onSecondaryContainer: Color(0xffD0DCE8),
tertiary: Color(0xff4ADE80), // Svrnty Success Green - Light
onTertiary: Color(0xff14532D), // Dark green for contrast
tertiaryContainer: Color(0xff15803D), // Svrnty Forest Dark Green
onTertiaryContainer: Color(0xffDCFCE7), // Light green tint
error: Color(0xffFF6B6B),
onError: Color(0xff4C0707),
errorContainer: Color(0xff93000A),
onErrorContainer: Color(0xffFEE2E2),
surface: Color(0xff0C1410), // Svrnty Dark Green Background
onSurface: Color(0xffF0F0F2),
onSurfaceVariant: Color(0xffBFC3C8),
outline: Color(0xff9CA3AF), // Enhanced contrast for dark mode
outlineVariant: Color(0xff374151),
onSecondaryContainer: Color(0xffF2F6FA),
tertiary: Color(0xff5EE890), // Bright Success Green
onTertiary: Color(0xff003916),
tertiaryContainer: Color(0xff15803D),
onTertiaryContainer: Color(0xffE6FFF0),
error: Color(0xffFF8A80),
onError: Color(0xff000000),
errorContainer: Color(0xffB3261E),
onErrorContainer: Color(0xffFFEDEA),
surface: Color(0xff0C1410), // Dark Forest Green Background
onSurface: Color(0xffFFFFFF), // Pure white for primary text - maximum contrast
onSurfaceVariant: Color(0xffE0E8E4), // Very light gray-green for secondary text (much brighter)
outline: Color(0xffA5B5AB), // Light gray-green outline
outlineVariant: Color(0xff4A5A52),
shadow: Color(0xff000000),
scrim: Color(0xff000000),
inverseSurface: Color(0xffE2E2E6),
inverseSurface: Color(0xffF0F4F2),
inversePrimary: Color(0xffDF2D45),
primaryFixed: Color(0xffFFE0E5),
primaryFixed: Color(0xffFFE5E8),
onPrimaryFixed: Color(0xff3D0009),
primaryFixedDim: Color(0xffFF6B7D),
onPrimaryFixedVariant: Color(0xff9C1A29),
secondaryFixed: Color(0xffD0DCE8),
onSecondaryFixed: Color(0xff06080C),
secondaryFixedDim: Color(0xff506576),
secondaryFixed: Color(0xffE8EEF3),
onSecondaryFixed: Color(0xff0A0F15),
secondaryFixedDim: Color(0xffA5B6C8),
onSecondaryFixedVariant: Color(0xff3A4958),
tertiaryFixed: Color(0xffDCFCE7), // Light green container
onTertiaryFixed: Color(0xff14532D), // Dark green text
tertiaryFixedDim: Color(0xff4ADE80), // Success green
onTertiaryFixedVariant: Color(0xff15803D), // Forest green
surfaceDim: Color(0xff0A110E), // Darker forest green
surfaceBright: Color(0xff1F2D25), // Brighter green tint
surfaceContainerLowest: Color(0xff070D0A), // Deepest green
surfaceContainerLow: Color(0xff141B18), // Low green
tertiaryFixed: Color(0xffE6FFF0),
onTertiaryFixed: Color(0xff003916),
tertiaryFixedDim: Color(0xff5EE890),
onTertiaryFixedVariant: Color(0xff15803D),
surfaceDim: Color(0xff08100D), // Darker forest green
surfaceBright: Color(0xff1F2D25), // Lighter forest green
surfaceContainerLowest: Color(0xff060D0A), // Deepest forest
surfaceContainerLow: Color(0xff141B18), // Low forest
surfaceContainer: Color(0xff18221D), // Mid forest green
surfaceContainerHigh: Color(0xff1D2822), // High green
surfaceContainerHighest: Color(0xff222F29), // Highest green tint
surfaceContainerHigh: Color(0xff1D2822), // High forest
surfaceContainerHighest: Color(0xff2A3832), // Highest forest green (lighter)
);
}
@@ -228,117 +118,116 @@ class MaterialTheme {
return theme(darkScheme());
}
static ColorScheme darkMediumContrastScheme() {
return const ColorScheme(
brightness: Brightness.dark,
primary: Color(0xffcbddff),
surfaceTint: Color(0xffa6c8ff),
onPrimary: Color(0xff00264d),
primaryContainer: Color(0xff7192c6),
onPrimaryContainer: Color(0xff000000),
secondary: Color(0xffd3ddf2),
onSecondary: Color(0xff1c2636),
secondaryContainer: Color(0xff8791a5),
onSecondaryContainer: Color(0xff000000),
tertiary: Color(0xfff1d2f8),
onTertiary: Color(0xff321e3a),
tertiaryContainer: Color(0xffa387aa),
onTertiaryContainer: Color(0xff000000),
error: Color(0xffffd2cc),
onError: Color(0xff540003),
errorContainer: Color(0xffff5449),
onErrorContainer: Color(0xff000000),
surface: Color(0xff111318),
onSurface: Color(0xffffffff),
onSurfaceVariant: Color(0xffdadce5),
outline: Color(0xffafb2bb),
outlineVariant: Color(0xff8d9099),
shadow: Color(0xff000000),
scrim: Color(0xff000000),
inverseSurface: Color(0xffe1e2e9),
inversePrimary: Color(0xff254978),
primaryFixed: Color(0xffd5e3ff),
onPrimaryFixed: Color(0xff001129),
primaryFixedDim: Color(0xffa6c8ff),
onPrimaryFixedVariant: Color(0xff0d3665),
secondaryFixed: Color(0xffd9e3f8),
onSecondaryFixed: Color(0xff071120),
secondaryFixedDim: Color(0xffbdc7dc),
onSecondaryFixedVariant: Color(0xff2d3747),
tertiaryFixed: Color(0xfff8d8ff),
onTertiaryFixed: Color(0xff1c0924),
tertiaryFixedDim: Color(0xffdbbde2),
onTertiaryFixedVariant: Color(0xff442e4c),
surfaceDim: Color(0xff111318),
surfaceBright: Color(0xff42444a),
surfaceContainerLowest: Color(0xff05070c),
surfaceContainerLow: Color(0xff1b1e22),
surfaceContainer: Color(0xff26282d),
surfaceContainerHigh: Color(0xff303338),
surfaceContainerHighest: Color(0xff3b3e43),
TextTheme _buildTextTheme(ColorScheme colorScheme) {
return TextTheme(
displayLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
fontSize: 57,
letterSpacing: -0.5,
color: colorScheme.onSurface,
),
displayMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
fontSize: 45,
letterSpacing: -0.5,
color: colorScheme.onSurface,
),
displaySmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
fontSize: 36,
letterSpacing: -0.25,
color: colorScheme.onSurface,
),
headlineLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 32,
letterSpacing: -0.25,
color: colorScheme.onSurface,
),
headlineMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 28,
letterSpacing: 0,
color: colorScheme.onSurface,
),
headlineSmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 24,
letterSpacing: 0,
color: colorScheme.onSurface,
),
titleLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 22,
letterSpacing: 0,
color: colorScheme.onSurface,
),
titleMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 16,
letterSpacing: 0.15,
color: colorScheme.onSurface,
),
titleSmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 14,
letterSpacing: 0.1,
color: colorScheme.onSurfaceVariant,
),
bodyLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500,
fontSize: 16,
letterSpacing: 0.5,
color: colorScheme.onSurface,
),
bodyMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500,
fontSize: 14,
letterSpacing: 0.25,
color: colorScheme.onSurface,
),
bodySmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500,
fontSize: 12,
letterSpacing: 0.4,
color: colorScheme.onSurfaceVariant,
),
labelLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 14,
letterSpacing: 0.1,
color: colorScheme.onSurface,
),
labelMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 12,
letterSpacing: 0.5,
color: colorScheme.onSurfaceVariant,
),
labelSmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 11,
letterSpacing: 0.5,
color: colorScheme.onSurfaceVariant,
),
);
}
ThemeData darkMediumContrast() {
return theme(darkMediumContrastScheme());
}
static ColorScheme darkHighContrastScheme() {
return const ColorScheme(
brightness: Brightness.dark,
primary: Color(0xffeaf0ff),
surfaceTint: Color(0xffa6c8ff),
onPrimary: Color(0xff000000),
primaryContainer: Color(0xffa3c4fb),
onPrimaryContainer: Color(0xff000b1e),
secondary: Color(0xffeaf0ff),
onSecondary: Color(0xff000000),
secondaryContainer: Color(0xffb9c3d8),
onSecondaryContainer: Color(0xff030b1a),
tertiary: Color(0xfffeeaff),
onTertiary: Color(0xff000000),
tertiaryContainer: Color(0xffd7b9de),
onTertiaryContainer: Color(0xff16041e),
error: Color(0xffffece9),
onError: Color(0xff000000),
errorContainer: Color(0xffffaea4),
onErrorContainer: Color(0xff220001),
surface: Color(0xff111318),
onSurface: Color(0xffffffff),
onSurfaceVariant: Color(0xffffffff),
outline: Color(0xffedf0f9),
outlineVariant: Color(0xffc0c2cb),
shadow: Color(0xff000000),
scrim: Color(0xff000000),
inverseSurface: Color(0xffe1e2e9),
inversePrimary: Color(0xff254978),
primaryFixed: Color(0xffd5e3ff),
onPrimaryFixed: Color(0xff000000),
primaryFixedDim: Color(0xffa6c8ff),
onPrimaryFixedVariant: Color(0xff001129),
secondaryFixed: Color(0xffd9e3f8),
onSecondaryFixed: Color(0xff000000),
secondaryFixedDim: Color(0xffbdc7dc),
onSecondaryFixedVariant: Color(0xff071120),
tertiaryFixed: Color(0xfff8d8ff),
onTertiaryFixed: Color(0xff000000),
tertiaryFixedDim: Color(0xffdbbde2),
onTertiaryFixedVariant: Color(0xff1c0924),
surfaceDim: Color(0xff111318),
surfaceBright: Color(0xff4e5055),
surfaceContainerLowest: Color(0xff000000),
surfaceContainerLow: Color(0xff1d2024),
surfaceContainer: Color(0xff2e3035),
surfaceContainerHigh: Color(0xff393b41),
surfaceContainerHighest: Color(0xff45474c),
);
}
ThemeData darkHighContrast() {
return theme(darkHighContrastScheme());
}
ThemeData theme(ColorScheme colorScheme) {
return ThemeData(
useMaterial3: true,
@@ -347,101 +236,7 @@ class MaterialTheme {
fontFamily: 'Montserrat',
scaffoldBackgroundColor: colorScheme.surface,
canvasColor: colorScheme.surface,
textTheme: const TextTheme(
displayLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
fontSize: 57,
letterSpacing: -0.5,
),
displayMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
fontSize: 45,
letterSpacing: -0.5,
),
displaySmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
fontSize: 36,
letterSpacing: -0.25,
),
headlineLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 32,
letterSpacing: -0.25,
),
headlineMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 28,
letterSpacing: 0,
),
headlineSmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 24,
letterSpacing: 0,
),
titleLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
fontSize: 22,
letterSpacing: 0,
),
titleMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500,
fontSize: 16,
letterSpacing: 0.15,
),
titleSmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500,
fontSize: 14,
letterSpacing: 0.1,
),
bodyLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w400,
fontSize: 16,
letterSpacing: 0.5,
),
bodyMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w400,
fontSize: 14,
letterSpacing: 0.25,
),
bodySmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w400,
fontSize: 12,
letterSpacing: 0.4,
),
labelLarge: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500,
fontSize: 14,
letterSpacing: 0.1,
),
labelMedium: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500,
fontSize: 12,
letterSpacing: 0.5,
),
labelSmall: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500,
fontSize: 11,
letterSpacing: 0.5,
),
).apply(
bodyColor: colorScheme.onSurface,
displayColor: colorScheme.onSurface,
),
textTheme: _buildTextTheme(colorScheme),
// Component Themes
cardTheme: ComponentThemes.cardTheme(colorScheme),
appBarTheme: ComponentThemes.appBarTheme(colorScheme),
@@ -461,43 +256,4 @@ class MaterialTheme {
sliderTheme: ComponentThemes.sliderTheme(colorScheme),
);
}
List<ExtendedColor> get extendedColors => [
];
}
class ExtendedColor {
final Color seed, value;
final ColorFamily light;
final ColorFamily lightHighContrast;
final ColorFamily lightMediumContrast;
final ColorFamily dark;
final ColorFamily darkHighContrast;
final ColorFamily darkMediumContrast;
const ExtendedColor({
required this.seed,
required this.value,
required this.light,
required this.lightHighContrast,
required this.lightMediumContrast,
required this.dark,
required this.darkHighContrast,
required this.darkMediumContrast,
});
}
class ColorFamily {
const ColorFamily({
required this.color,
required this.onColor,
required this.colorContainer,
required this.onColorContainer,
});
final Color color;
final Color onColor;
final Color colorContainer;
final Color onColorContainer;
}
+320
View File
@@ -0,0 +1,320 @@
# Svrnty Theme System - Quick Reference
## Standard Color Access Pattern
**ALWAYS use:**
```dart
final colorScheme = Theme.of(context).colorScheme;
// Primary UI
colorScheme.primary // Primary brand color (Crimson Red)
colorScheme.onPrimary // Text on primary (white/black depending on theme)
// Secondary UI
colorScheme.secondary // Secondary brand color (Slate Blue)
colorScheme.onSecondary // Text on secondary
// Text
colorScheme.onSurface // Primary text
colorScheme.onSurfaceVariant // Secondary text
// Backgrounds
colorScheme.surface // Page background
colorScheme.surfaceContainer // Card background
// Status (specialized)
StatusColorScheme.getStatusColor(status)
StatusColorScheme.getStatusColorFromTheme(status, colorScheme) // Theme-aware (preferred)
```
**NEVER use:**
```dart
Colors.white // FORBIDDEN - use colorScheme.onPrimary or onSurface
Colors.black // FORBIDDEN - use colorScheme.onSurface or scrim
Color(0xFFXXXXXX) // FORBIDDEN in components (except in theme files)
SvrntyColors.crimsonRed // FORBIDDEN - use colorScheme.primary instead
```
## Theme Variants
The app provides **2 theme variants**:
### Light Theme
- **Background:** White (#FCFCFC)
- **Primary:** Crimson Red (#C91F37) - high contrast variant
- **Secondary:** Dark Slate (#2D3843) - high contrast variant
- **Text:** Very dark gray (#1A1C1E) - 16.5:1 contrast (WCAG AAA)
- **Secondary Text:** Dark gray (#3E4A56) - 7:1 contrast (WCAG AAA)
### Dark Theme (Forest Green)
- **Background:** Dark Forest Green (#0C1410) - unique branding
- **Primary:** Bright Crimson (#FF5A6D) - optimized for dark backgrounds
- **Secondary:** Light Slate Gray (#A5B6C8)
- **Text:** Pure white (#FFFFFF) - 18.2:1 contrast (WCAG AAA)
- **Secondary Text:** Light gray-green (#E0E8E4) - 14.1:1 contrast (WCAG AAA)
All text colors are WCAG AAA compliant (minimum 7:1 contrast for normal text, 4.5:1 for large text).
## Color Access Examples
### Good Examples
```dart
// Text on colored background (e.g., status badge, avatar)
Container(
color: Theme.of(context).colorScheme.primary,
child: Text(
'Label',
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),
),
)
// Primary text on page background
Text(
'Hello',
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
)
// Secondary/muted text
Text(
'Description',
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
)
// Shadow color
BoxShadow(
color: Theme.of(context).colorScheme.scrim.withValues(alpha: 0.2),
)
```
### Bad Examples (DON'T DO THIS)
```dart
// WRONG - hardcoded white
Text('Label', style: TextStyle(color: Colors.white))
// WRONG - hardcoded black
BoxShadow(color: Colors.black.withValues(alpha: 0.2))
// WRONG - hardcoded color value
Container(color: Color(0xFFFF9800))
// WRONG - using SvrntyColors directly for UI elements
Container(color: SvrntyColors.crimsonRed) // Use colorScheme.primary instead
```
## Status Colors
For delivery status indicators, use the `StatusColorScheme` utility:
```dart
import '../theme/status_colors.dart';
// Get status color (hardcoded, consistent across themes)
final color = StatusColorScheme.getStatusColor('completed');
// Get status color from theme (preferred - adapts to theme)
final themeColor = StatusColorScheme.getStatusColorFromTheme(
'completed',
Theme.of(context).colorScheme,
);
// Get background and text colors
final bgColor = StatusColorScheme.getStatusBackground('completed');
final textColor = StatusColorScheme.getStatusText('completed');
// Use pre-built widgets
StatusBadgeWidget(status: 'completed')
StatusAccentBar(status: 'in_transit')
```
**Supported Status Values:**
- `pending` - Amber (attention needed)
- `in_transit`, `in_progress`, `processing` - Slate blue (active)
- `completed`, `delivered`, `done` - Green (success)
- `failed`, `error` - Red (problem)
- `cancelled`, `skipped`, `rejected` - Gray (inactive)
- `on_hold`, `paused`, `waiting` - Slate (informational)
## Progress Gradient Colors
For progress indicators (e.g., route completion):
```dart
import '../theme/color_system.dart';
// Progress colors (0-100%)
SvrntyColors.progressLow // Orange - 0-40%
SvrntyColors.progressMedium // Amber - 40-70%
SvrntyColors.progressHigh // Green - 70-100%
// Example: color interpolation
Color progressColor = Color.lerp(
SvrntyColors.progressLow,
SvrntyColors.progressMedium,
progressPercent,
)!;
```
## Modifying Colors
### Changing Brand Colors
Edit the ColorScheme definitions in `/lib/theme.dart`:
```dart
// Light theme
static ColorScheme lightScheme() {
return const ColorScheme(
brightness: Brightness.light,
primary: Color(0xffC91F37), // Change this for new primary color
secondary: Color(0xff2D3843), // Change this for new secondary color
// ... rest of colors
);
}
// Dark theme
static ColorScheme darkScheme() {
return const ColorScheme(
brightness: Brightness.dark,
primary: Color(0xffFF5A6D), // Change this for new primary color
secondary: Color(0xffA5B6C8), // Change this for new secondary color
// ... rest of colors
);
}
```
### Adding New Semantic Colors
Add to `/lib/theme/color_system.dart`:
```dart
class SvrntyColors {
// ... existing colors
/// New semantic color
static const Color myNewColor = Color(0xFFXXXXXX);
}
```
Then use it in components:
```dart
Container(color: SvrntyColors.myNewColor)
```
### Changing Status Colors
Edit `/lib/theme/status_colors.dart`:
```dart
class StatusColorScheme {
static const Color completed = Color(0xFF22C55E); // Change this
static const Color completedBackground = Color(0xFFD1FAE5); // Change this
static const Color completedText = Color(0xFF065F46); // Change this
// ... rest of colors
}
```
## Text Theme
The app uses **Montserrat** font family for all text with explicit color assignments.
**Font Weights:**
- 300 (Light) - Unused in current design
- 400 (Regular) - Body text
- 500 (Medium) - Labels, buttons
- 600 (SemiBold) - Headings, titles
- 700 (Bold) - Display text, emphasis
**Text Styles:**
```dart
Theme.of(context).textTheme.displayLarge // 57px, bold, onSurface
Theme.of(context).textTheme.headlineMedium // 28px, semibold, onSurface
Theme.of(context).textTheme.titleLarge // 22px, semibold, onSurface
Theme.of(context).textTheme.bodyMedium // 14px, regular, onSurface
Theme.of(context).textTheme.labelSmall // 11px, medium, onSurfaceVariant
```
All text styles automatically adapt to light/dark themes with proper contrast.
## Accessibility
All color combinations meet **WCAG AAA** standards (7:1 contrast for normal text, 4.5:1 for large text):
**Light Theme:**
- Primary text on background: 16.5:1 (WCAG AAA)
- Secondary text on background: 7:1 (WCAG AAA)
- Text on primary color: 6.2:1 (WCAG AA Large)
**Dark Theme:**
- Primary text on background: 18.2:1 (WCAG AAA)
- Secondary text on background: 14.1:1 (WCAG AAA)
- Text on primary color: 11.8:1 (WCAG AAA)
## Component Themes
Component-specific theme configurations are in `/lib/theme/component_themes.dart`:
- CardTheme - Elevated cards with subtle shadows
- AppBarTheme - Navigation bars
- ButtonTheme - Filled, outlined, elevated buttons
- InputDecorationTheme - Text fields
- SnackBarTheme - Toast messages
- DialogTheme - Modal dialogs
- BottomNavigationBarTheme - Bottom navigation
- ChipTheme - Status chips
- ProgressIndicatorTheme - Loading indicators
- FloatingActionButtonTheme - FAB
- SliderTheme - Range inputs
All component themes use `ColorScheme` properties for consistency.
## Migration Guide
If you need to migrate old hardcoded colors to the new theme system:
### Step 1: Find Hardcoded Colors
```bash
# Search for hardcoded colors in your components
grep -r "Colors\.white\|Colors\.black\|Color(0x" lib/components/ lib/pages/
```
### Step 2: Replace with Theme Colors
| Old Pattern | New Pattern |
|------------|-------------|
| `Colors.white` on colored bg | `colorScheme.onPrimary` |
| `Colors.white` on page | `colorScheme.surface` |
| `Colors.black` for text | `colorScheme.onSurface` |
| `Colors.black` for shadow | `colorScheme.scrim` |
| `Color(0xFFXXXXXX)` | Use `colorScheme` or `SvrntyColors` |
### Step 3: Test Both Themes
- Run app in light mode, verify all text is visible
- Run app in dark mode, verify all text is visible
- Check color contrast with browser dev tools
## Troubleshooting
**Text not visible in dark mode:**
- Ensure you're using `colorScheme.onSurface` or `onSurfaceVariant` for text colors
- Don't use hardcoded `Colors.black` or dark colors for text
- Check that TextTheme is properly applied with `_buildTextTheme()`
**Colors not updating when switching themes:**
- Use `Theme.of(context).colorScheme` instead of `SvrntyColors` constants
- Ensure widget rebuilds when theme changes (use `ConsumerWidget` for Riverpod)
- Avoid caching ColorScheme outside of build method
**Wrong colors in components:**
- Verify component uses `colorScheme` parameter correctly
- Check that custom theme is applied in MaterialApp
- Use `theme()` method to generate ThemeData
## Resources
- **Material Design 3:** https://m3.material.io/
- **WCAG Contrast Checker:** https://webaim.org/resources/contrastchecker/
- **Flutter Theme Guide:** https://docs.flutter.dev/cookbook/design/themes
- **ColorScheme Docs:** https://api.flutter.dev/flutter/material/ColorScheme-class.html
+13 -84
View File
@@ -44,6 +44,19 @@ class SvrntyColors {
/// Error - Red for errors, failures, and destructive actions
static const Color error = Color(0xFFEF4444);
// ============================================
// PROGRESS GRADIENT COLORS
// ============================================
/// Progress Low - Orange for low progress (0-40%)
static const Color progressLow = Color(0xFFFF9800);
/// Progress Medium - Amber for medium progress (40-70%)
static const Color progressMedium = Color(0xFFFFC107);
/// Progress High - Green for high progress (70-100%)
static const Color progressHigh = Color(0xFF4CAF50);
// ============================================
// DELIVERY STATUS COLORS (OPTIMIZED SVRNTY MAPPING)
// ============================================
@@ -97,88 +110,4 @@ class SvrntyColors {
/// Surface Subdued - Subdued light surface
static const Color surfaceSubdued = Color(0xFFE8EAEE);
// ============================================
// EXTENDED COLOR FAMILIES - SUCCESS (GREEN)
// ============================================
/// Success color light theme
static const Color successLight = Color(0xFF22C55E);
/// Success on color light theme
static const Color onSuccessLight = Color(0xFFFFFFFF);
/// Success container light theme
static const Color successContainerLight = Color(0xFFDCFCE7);
/// Success on container light theme
static const Color onSuccessContainerLight = Color(0xFF14532D);
/// Success color dark theme
static const Color successDark = Color(0xFF4ADE80);
/// Success on color dark theme
static const Color onSuccessDark = Color(0xFF14532D);
/// Success container dark theme
static const Color successContainerDark = Color(0xFF15803D);
/// Success on container dark theme
static const Color onSuccessContainerDark = Color(0xFFDCFCE7);
// ============================================
// EXTENDED COLOR FAMILIES - WARNING (AMBER)
// ============================================
/// Warning color light theme
static const Color warningLight = Color(0xFFF59E0B);
/// Warning on color light theme
static const Color onWarningLight = Color(0xFFFFFFFF);
/// Warning container light theme
static const Color warningContainerLight = Color(0xFFFEF3C7);
/// Warning on container light theme
static const Color onWarningContainerLight = Color(0xFF78350F);
/// Warning color dark theme
static const Color warningDark = Color(0xFFFBBF24);
/// Warning on color dark theme
static const Color onWarningDark = Color(0xFF78350F);
/// Warning container dark theme
static const Color warningContainerDark = Color(0xFFD97706);
/// Warning on container dark theme
static const Color onWarningContainerDark = Color(0xFFFEF3C7);
// ============================================
// EXTENDED COLOR FAMILIES - INFO (BLUE)
// ============================================
/// Info color light theme
static const Color infoLight = Color(0xFF3B82F6);
/// Info on color light theme
static const Color onInfoLight = Color(0xFFFFFFFF);
/// Info container light theme
static const Color infoContainerLight = Color(0xFFDEEEFF);
/// Info on container light theme
static const Color onInfoContainerLight = Color(0xFF003DA1);
/// Info color dark theme
static const Color infoDark = Color(0xFF90CAF9);
/// Info on color dark theme
static const Color onInfoDark = Color(0xFF003DA1);
/// Info container dark theme
static const Color infoContainerDark = Color(0xFF0D47A1);
/// Info on container dark theme
static const Color onInfoContainerDark = Color(0xFFDEEEFF);
}
+30
View File
@@ -63,6 +63,36 @@ class StatusColorScheme {
}
}
/// Get status color from ColorScheme (preferred over hardcoded)
/// This method returns status colors that better integrate with the current theme
static Color getStatusColorFromTheme(String status, ColorScheme colorScheme) {
switch (status.toLowerCase()) {
case 'completed':
case 'delivered':
case 'done':
return colorScheme.tertiary; // Use theme green
case 'failed':
case 'error':
return colorScheme.error; // Use theme error
case 'pending':
return SvrntyColors.warning; // Keep custom warning
case 'in_transit':
case 'in_progress':
case 'processing':
return colorScheme.secondary; // Use theme secondary
case 'cancelled':
case 'skipped':
case 'rejected':
return colorScheme.onSurfaceVariant; // Use theme variant
case 'on_hold':
case 'paused':
case 'waiting':
return colorScheme.outline; // Use theme outline
default:
return getStatusColor(status); // Fallback to hardcoded
}
}
/// Get status background color by status type
static Color getStatusBackground(String status) {
switch (status.toLowerCase()) {
+7 -3
View File
@@ -48,14 +48,18 @@ class ToastHelper {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final topPadding = MediaQuery.of(context).padding.top;
final bottomPadding = MediaQuery.of(context).padding.bottom;
final toastWidth = screenWidth * 0.5; // 50% of screen width
final horizontalMargin = (screenWidth - toastWidth) / 2;
// Position toast very close to top (10px into safe area)
final topMargin = topPadding - 10;
// Position toast at top with safe padding
final topMargin = topPadding + 10;
const toastHeight = 60.0;
final bottomMargin = screenHeight - topMargin - toastHeight;
// Ensure bottom margin is at least the safe area padding
final safeBottomMargin = bottomMargin > bottomPadding ? bottomMargin : bottomPadding + 10;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
@@ -72,7 +76,7 @@ class ToastHelper {
top: topMargin,
left: horizontalMargin,
right: horizontalMargin,
bottom: bottomMargin,
bottom: safeBottomMargin,
),
),
);