diff --git a/lib/screens/admin/geofence_test_screen.dart b/lib/screens/admin/geofence_test_screen.dart index 6c1e3a16..ccf403a7 100644 --- a/lib/screens/admin/geofence_test_screen.dart +++ b/lib/screens/admin/geofence_test_screen.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; + +import '../../theme/app_status_colors.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -111,8 +113,8 @@ class _GeofenceTestScreenState extends ConsumerState { .check_circle : Icons.cancel, color: inside - ? Colors.green - : Colors.red, + ? AppStatusColors.of(context).success.color + : AppStatusColors.of(context).danger.color, ), const SizedBox( width: 8, @@ -166,8 +168,8 @@ class _GeofenceTestScreenState extends ConsumerState { ? Icons.check_circle : Icons.cancel, color: inside - ? Colors.green - : Colors.red, + ? AppStatusColors.of(context).success.color + : AppStatusColors.of(context).danger.color, ), const SizedBox(width: 8), Text( @@ -275,10 +277,10 @@ class _GeofenceTestScreenState extends ConsumerState { width: 40, height: 40, // `child` is used by newer flutter_map Marker API. - child: const Icon( + child: Icon( Icons.person_pin_circle, size: 36, - color: Colors.blue, + color: Theme.of(context).colorScheme.primary, ), ), ); @@ -289,10 +291,12 @@ class _GeofenceTestScreenState extends ConsumerState { polygons: [ Polygon( points: polygonPoints, - color: Colors.green.withValues( - alpha: 0.15, - ), - borderColor: Colors.green, + color: AppStatusColors.of(context) + .success + .color + .withValues(alpha: 0.15), + borderColor: + AppStatusColors.of(context).success.color, borderStrokeWidth: 2, ), ], @@ -304,9 +308,10 @@ class _GeofenceTestScreenState extends ConsumerState { circles: [ CircleMarker( point: LatLng(cfg!.lat!, cfg.lng!), - color: Colors.green.withValues( - alpha: 0.10, - ), + color: AppStatusColors.of(context) + .success + .color + .withValues(alpha: 0.10), borderStrokeWidth: 2, useRadiusInMeter: true, radius: cfg.radiusMeters ?? 100, @@ -387,11 +392,15 @@ class _GeofenceTestScreenState extends ConsumerState { width: 12, height: 12, decoration: BoxDecoration( - color: Colors.green.withValues( + color: AppStatusColors.of( + context, + ).success.color.withValues( alpha: 0.25, ), border: Border.all( - color: Colors.green, + color: AppStatusColors.of( + context, + ).success.color, ), ), ), @@ -420,14 +429,16 @@ class _GeofenceTestScreenState extends ConsumerState { top: 6.0, ), child: Row( - children: const [ + children: [ Icon( Icons.person_pin_circle, size: 14, - color: Colors.blue, + color: Theme.of( + context, + ).colorScheme.primary, ), - SizedBox(width: 8), - Text('You'), + const SizedBox(width: 8), + const Text('You'), ], ), ), diff --git a/lib/screens/attendance/attendance_screen.dart b/lib/screens/attendance/attendance_screen.dart index eacd2fc4..82e8aafe 100644 --- a/lib/screens/attendance/attendance_screen.dart +++ b/lib/screens/attendance/attendance_screen.dart @@ -35,6 +35,7 @@ import 'logbook_day_activity.dart'; import 'work_log_tab.dart'; import '../../theme/m3_motion.dart'; import '../../theme/app_surfaces.dart'; +import '../../theme/app_status_colors.dart'; import '../../utils/app_time.dart'; import '../../utils/device_security.dart'; import '../../utils/location_permission.dart'; @@ -787,7 +788,7 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> { } else if (diff.inMinutes <= 5) { timeColor = colors.error; } else if (diff.inMinutes <= 15) { - timeColor = Colors.orange; + timeColor = AppStatusColors.of(context).warning.color; } else if (diff.inMinutes <= 30) { timeColor = colors.tertiary; } @@ -847,7 +848,7 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> { : Icons.cancel, size: 16, color: _insideGeofence || hasGeofenceOverride - ? Colors.green + ? AppStatusColors.of(context).success.color : colors.error, ), const SizedBox(width: 6), @@ -859,7 +860,7 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> { : 'Outside geofence', style: theme.textTheme.bodySmall?.copyWith( color: _insideGeofence || hasGeofenceOverride - ? Colors.green + ? AppStatusColors.of(context).success.color : colors.error, fontWeight: FontWeight.w500, ), @@ -1863,11 +1864,11 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> { bg = colors.secondaryContainer; fg = colors.onSecondaryContainer; case 'Early Out': - bg = Colors.orange.shade100; - fg = Colors.orange.shade900; + bg = AppStatusColors.of(context).warning.container; + fg = AppStatusColors.of(context).warning.onContainer; case 'Noon Break': - bg = Colors.blue.shade100; - fg = Colors.blue.shade900; + bg = AppStatusColors.of(context).info.container; + fg = AppStatusColors.of(context).info.onContainer; case 'Absent': bg = colors.errorContainer; fg = colors.onErrorContainer; @@ -2841,13 +2842,14 @@ class _DateGroupTile extends StatelessWidget { final colors = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; + final status = AppStatusColors.of(context); final statusColor = group.status == 'Absent' - ? Colors.red + ? status.danger.color : group.status == 'On duty' - ? Colors.orange + ? status.warning.color : group.status == 'On Leave' - ? Colors.teal - : Colors.green; + ? status.info.color + : status.success.color; return Card( margin: const EdgeInsets.fromLTRB(12, 8, 12, 0), @@ -2960,10 +2962,10 @@ class _DateGroupTile extends StatelessWidget { String statusLabel; switch (slip.status) { case 'approved': - statusColor = Colors.green; + statusColor = AppStatusColors.of(context).success.color; statusLabel = 'Approved'; case 'pending': - statusColor = Colors.orange; + statusColor = AppStatusColors.of(context).warning.color; statusLabel = 'Pending'; case 'rejected': statusColor = colors.error; @@ -3081,7 +3083,7 @@ class _DateGroupTile extends StatelessWidget { ? Chip( label: Text(entry.isLeave ? 'On Leave' : 'Absent'), backgroundColor: entry.isLeave - ? Colors.teal.withValues(alpha: 0.15) + ? AppStatusColors.of(context).info.container : colors.errorContainer, ) : entry.status == 'On duty' @@ -3107,7 +3109,7 @@ class _DateGroupTile extends StatelessWidget { switch (entry.verificationStatus) { case 'verified': icon = Icons.verified; - color = Colors.green; + color = AppStatusColors.of(context).success.color; tooltip = 'Verified'; case 'unverified' || 'skipped': icon = Icons.warning_amber_rounded; @@ -3115,7 +3117,7 @@ class _DateGroupTile extends StatelessWidget { tooltip = 'Unverified'; default: icon = Icons.hourglass_bottom; - color = Colors.orange; + color = AppStatusColors.of(context).warning.color; tooltip = 'Pending'; } final badge = Tooltip( @@ -3526,7 +3528,7 @@ class _CheckInOutMapTab extends StatelessWidget { title: 'Check In', lat: entry.checkInLat, lng: entry.checkInLng, - markerColor: Colors.green, + markerColor: AppStatusColors.of(context).success.color, ), ), const SizedBox(width: 12), @@ -3536,7 +3538,7 @@ class _CheckInOutMapTab extends StatelessWidget { title: 'Check Out', lat: entry.checkOutLat, lng: entry.checkOutLng, - markerColor: Colors.red, + markerColor: AppStatusColors.of(context).danger.color, ), ), ], @@ -5830,7 +5832,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { IconData statusIcon; switch (slip.status) { case 'approved': - statusColor = Colors.green; + statusColor = AppStatusColors.of(context).success.color; statusIcon = Icons.check_circle; case 'rejected': statusColor = colors.error; @@ -5839,7 +5841,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { statusColor = colors.primary; statusIcon = Icons.task_alt; default: - statusColor = Colors.orange; + statusColor = AppStatusColors.of(context).warning.color; statusIcon = Icons.hourglass_top; } @@ -6248,7 +6250,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { IconData statusIcon; switch (leave.status) { case 'approved': - statusColor = Colors.teal; + statusColor = AppStatusColors.of(context).success.color; statusIcon = Icons.check_circle; case 'rejected': statusColor = colors.error; @@ -6257,7 +6259,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { statusColor = colors.onSurfaceVariant; statusIcon = Icons.remove_circle_outline; default: - statusColor = Colors.orange; + statusColor = AppStatusColors.of(context).warning.color; statusIcon = Icons.hourglass_top; } diff --git a/lib/screens/attendance/logbook_day_activity.dart b/lib/screens/attendance/logbook_day_activity.dart index 77a02a1e..9364f76d 100644 --- a/lib/screens/attendance/logbook_day_activity.dart +++ b/lib/screens/attendance/logbook_day_activity.dart @@ -7,6 +7,7 @@ import '../../models/it_service_request.model.dart'; import '../../models/pass_slip.model.dart'; import '../../models/task.model.dart'; import '../../models/ticket.model.dart'; +import '../../theme/app_surfaces.dart'; import '../../theme/m3_motion.dart'; import '../../utils/app_time.dart'; @@ -427,7 +428,7 @@ class _SummaryChip extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius), ), child: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/auth/signup_screen.dart b/lib/screens/auth/signup_screen.dart index f3922ce6..df82a974 100644 --- a/lib/screens/auth/signup_screen.dart +++ b/lib/screens/auth/signup_screen.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import '../../providers/auth_provider.dart'; import '../../providers/tickets_provider.dart'; +import '../../theme/app_status_colors.dart'; import '../../theme/m3_motion.dart'; import '../../utils/snackbar.dart'; @@ -563,23 +564,24 @@ class _SignUpScreenState extends ConsumerState } final normalized = (score / 6).clamp(0.0, 1.0); + final s = AppStatusColors.of(context); String label; Color color; if (normalized <= 0.2) { label = 'Very weak'; - color = Colors.red; + color = s.danger.color; } else if (normalized <= 0.4) { label = 'Weak'; - color = Colors.deepOrange; + color = s.danger.color; } else if (normalized <= 0.6) { label = 'Fair'; - color = Colors.orange; + color = s.warning.color; } else if (normalized <= 0.8) { label = 'Strong'; - color = Colors.green; + color = s.info.color; } else { label = 'Excellent'; - color = Colors.teal; + color = s.success.color; } setState(() { diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index e7ab558f..10a48086 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -38,6 +38,7 @@ import '../../widgets/app_breakpoints.dart'; import '../../providers/realtime_controller.dart'; import 'package:skeletonizer/skeletonizer.dart'; import '../../theme/app_surfaces.dart'; +import '../../theme/app_status_colors.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/app_page_header.dart'; import '../../utils/app_time.dart'; @@ -776,14 +777,29 @@ class _DashboardScreenState extends State { } Widget _sectionTitle(BuildContext context, String title) { + final cs = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(bottom: 12), - child: Text( - title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + child: Row( + children: [ + Container( + width: 3, + height: 16, + decoration: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 10), + Text( + title.toUpperCase(), + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.8, + color: cs.onSurface, + ), + ), + ], ), ); } @@ -918,21 +934,26 @@ class _MetricCard extends ConsumerWidget { ), ), ); + final isDark = cs.brightness == Brightness.dark; return AnimatedContainer( duration: const Duration(milliseconds: 400), curve: Curves.easeOutCubic, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: cs.surfaceContainerLow, - borderRadius: BorderRadius.circular(16), + color: isDark ? cs.surfaceContainer : cs.surfaceContainerLowest, + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).cardRadius, + ), + border: Border.all(color: cs.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - title, - style: Theme.of(context).textTheme.labelLarge?.copyWith( + title.toUpperCase(), + style: Theme.of(context).textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w600, + letterSpacing: 0.6, color: cs.onSurfaceVariant, ), ), @@ -967,14 +988,16 @@ class _StaffTable extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - // M3 Expressive: tonal surface container, 28 dp radius for large containers. + final isDark = cs.brightness == Brightness.dark; + // Cyber/IT panel: flat tonal fill + hairline outline. return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: cs.surfaceContainerLow, + color: isDark ? cs.surfaceContainer : cs.surfaceContainerLowest, borderRadius: BorderRadius.circular( AppSurfaces.of(context).containerRadius, ), + border: Border.all(color: cs.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1059,18 +1082,23 @@ class _StaffTableBody extends ConsumerWidget { ); if (providerState == 'loading') { - return const Text('Loading staff...'); + return _panelHint(context, Icons.hourglass_empty_rounded, 'Loading staff…'); } if (providerState.startsWith('error:')) { final err = providerState.substring(6); - return Text('Failed to load staff: $err'); + return _panelHint( + context, + Icons.error_outline_rounded, + 'Failed to load staff: $err', + ); } if (staffRows.isEmpty) { - return Text( + return _panelHint( + context, + Icons.groups_outlined, 'No IT staff available.', - style: Theme.of(context).textTheme.bodySmall, ); } @@ -1142,20 +1170,24 @@ class _StaffRow extends StatelessWidget { Expanded( flex: 4, child: Center( - child: Text( - row.whereabouts, - style: valueStyle?.copyWith( - color: row.whereabouts == 'In premise' - ? Colors.green - : row.whereabouts == 'Outside premise' - ? Colors.grey - : row.whereabouts == 'Tracking off' - ? Colors.grey - : row.whereabouts.startsWith('Last seen') - ? Colors.grey - : null, - fontWeight: FontWeight.w600, - ), + child: Builder( + builder: (context) { + final status = AppStatusColors.of(context); + final Color? color = row.whereabouts == 'In premise' + ? status.success.color + : (row.whereabouts == 'Outside premise' || + row.whereabouts == 'Tracking off' || + row.whereabouts.startsWith('Last seen')) + ? status.neutral.color + : null; + return Text( + row.whereabouts, + style: valueStyle?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ); + }, ), ), ), @@ -1202,21 +1234,24 @@ class _StaffActivityChart extends ConsumerWidget { final cs = Theme.of(context).colorScheme; + final isDark = cs.brightness == Brightness.dark; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: cs.surfaceContainerLow, + color: isDark ? cs.surfaceContainer : cs.surfaceContainerLowest, borderRadius: BorderRadius.circular( AppSurfaces.of(context).containerRadius, ), + border: Border.all(color: cs.outlineVariant), ), child: metricsAsync.when( data: (metrics) { final staffRows = metrics.staffRows; if (staffRows.isEmpty) { - return Text( + return _panelHint( + context, + Icons.bar_chart_outlined, 'No staff activity data available.', - style: Theme.of(context).textTheme.bodySmall, ); } @@ -1393,16 +1428,16 @@ class _StaffActivityChart extends ConsumerWidget { vertical: 2, ), decoration: BoxDecoration( - color: Colors.black.withAlpha( - (0.35 * 255).round(), + color: cs.inverseSurface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).chipRadius, ), - borderRadius: BorderRadius.circular(8), ), - child: Text( + child: MonoText( total.toString(), style: Theme.of(context).textTheme.bodySmall ?.copyWith( - color: Colors.white, + color: cs.onInverseSurface, fontWeight: FontWeight.w700, ), ), @@ -1534,8 +1569,13 @@ class _StaffActivityChart extends ConsumerWidget { }, ); }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Text('Failed to load activity: $error'), + loading: () => + _panelHint(context, Icons.hourglass_empty_rounded, 'Loading activity…'), + error: (error, stack) => _panelHint( + context, + Icons.error_outline_rounded, + 'Failed to load activity: $error', + ), ), ); } @@ -1556,29 +1596,28 @@ class _PulseStatusPill extends StatelessWidget { @override Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final s = AppStatusColors.of(context); final (Color bg, Color fg) = switch (label.toLowerCase()) { - 'arrival' => (Colors.amber.shade100, Colors.amber.shade900), - 'late' => (Colors.red.shade100, Colors.red.shade900), - 'noon break' => (Colors.blue.shade100, Colors.blue.shade900), - 'vacant' => (Colors.green.shade100, Colors.green.shade900), - 'on task' => (Colors.purple.shade100, Colors.purple.shade900), - 'on event' => (Colors.purple.shade100, Colors.purple.shade900), - 'in triage' => (Colors.orange.shade100, Colors.orange.shade900), - 'early out' => (Colors.deepOrange.shade100, Colors.deepOrange.shade900), - 'on leave' => (Colors.teal.shade100, Colors.teal.shade900), - 'absent' => (Colors.red.shade200, Colors.red.shade900), - 'off duty' => (Colors.grey.shade200, Colors.grey.shade700), - _ => ( - Theme.of(context).colorScheme.tertiaryContainer, - Theme.of(context).colorScheme.onTertiaryContainer, - ), + 'arrival' => (s.success.container, s.success.onContainer), + 'late' => (s.warning.container, s.warning.onContainer), + 'noon break' => (s.info.container, s.info.onContainer), + 'vacant' => (s.neutral.container, s.neutral.onContainer), + 'on task' => (cs.primaryContainer, cs.onPrimaryContainer), + 'on event' => (cs.tertiaryContainer, cs.onTertiaryContainer), + 'in triage' => (s.warning.container, s.warning.onContainer), + 'early out' => (s.warning.container, s.warning.onContainer), + 'on leave' => (s.info.container, s.info.onContainer), + 'absent' => (s.danger.container, s.danger.onContainer), + 'off duty' => (s.neutral.container, s.neutral.onContainer), + _ => (cs.tertiaryContainer, cs.onTertiaryContainer), }; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: bg, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius), ), child: Text( label, @@ -1591,6 +1630,31 @@ class _PulseStatusPill extends StatelessWidget { } } +/// Compact, muted, centered hint for empty/loading/error states inside a +/// bounded dashboard panel (too small for the full-screen AppEmptyView). +Widget _panelHint(BuildContext context, IconData icon, String message) { + final cs = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 18, color: cs.onSurfaceVariant), + const SizedBox(width: 8), + Flexible( + child: Text( + message, + textAlign: TextAlign.center, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant), + ), + ), + ], + ), + ); +} + Duration? _averageDuration(List durations) { if (durations.isEmpty) { return null; diff --git a/lib/screens/it_service_requests/it_service_request_detail_screen.dart b/lib/screens/it_service_requests/it_service_request_detail_screen.dart index b795650a..940e0197 100644 --- a/lib/screens/it_service_requests/it_service_request_detail_screen.dart +++ b/lib/screens/it_service_requests/it_service_request_detail_screen.dart @@ -29,6 +29,7 @@ import '../../widgets/gemini_animated_text_field.dart'; import '../../widgets/m3_card.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; +import '../../theme/app_status_colors.dart'; import '../../widgets/status_pill.dart'; import 'it_service_request_pdf.dart'; @@ -411,13 +412,13 @@ class _ItServiceRequestDetailScreenState ), ) : _hasRecentSave - ? const Padding( - key: ValueKey('isr_saved'), - padding: EdgeInsets.symmetric(horizontal: 8), + ? Padding( + key: const ValueKey('isr_saved'), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Icon( Icons.check_circle, size: 18, - color: Colors.green, + color: AppStatusColors.of(context).success.color, ), ) : const SizedBox(key: ValueKey('isr_idle')), @@ -555,7 +556,10 @@ class _ItServiceRequestDetailScreenState if (request.requestNumber != null) MonoText(request.requestNumber!), const SizedBox(width: 12), - StatusPill(label: ItServiceRequestStatus.label(request.status)), + StatusPill.kind( + _isrStatusKind(request.status), + label: ItServiceRequestStatus.label(request.status), + ), const Spacer(), Text( 'Created ${AppTime.formatDate(request.createdAt)}', @@ -1231,10 +1235,10 @@ class _ItServiceRequestDetailScreenState child: const Icon(Icons.save, size: 14), ) : isSaved - ? const Icon( + ? Icon( Icons.check_circle, - key: ValueKey('label_saved'), - color: Colors.green, + key: const ValueKey('label_saved'), + color: AppStatusColors.of(context).success.color, size: 14, ) : const SizedBox(key: ValueKey('label_idle')), @@ -1584,9 +1588,6 @@ class _ItServiceRequestDetailScreenState final controller = TextEditingController(); return AlertDialog( title: const Text('Cancellation Reason'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), - ), content: TextField( controller: controller, decoration: const InputDecoration( @@ -1836,7 +1837,6 @@ class _StaffPickerDialogState extends State<_StaffPickerDialog> { .toList(); return AlertDialog( title: const Text('Select IT Staff'), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), content: SizedBox( width: 350, height: 400, @@ -2245,3 +2245,16 @@ class _EvidenceSectionState extends ConsumerState<_EvidenceSection> { ); } } + +/// Maps an IT service request status to a semantic [StatusKind] +/// (amber=pending approval, blue=scheduled/in-progress, green=completed, +/// grey=cancelled). +StatusKind _isrStatusKind(String status) => switch (status) { + ItServiceRequestStatus.pendingApproval => StatusKind.warning, + ItServiceRequestStatus.scheduled => StatusKind.info, + ItServiceRequestStatus.inProgress => StatusKind.info, + ItServiceRequestStatus.inProgressDryRun => StatusKind.info, + ItServiceRequestStatus.completed => StatusKind.success, + ItServiceRequestStatus.cancelled => StatusKind.neutral, + _ => StatusKind.neutral, +}; diff --git a/lib/screens/it_service_requests/it_service_requests_list_screen.dart b/lib/screens/it_service_requests/it_service_requests_list_screen.dart index 9b65c482..6f19199a 100644 --- a/lib/screens/it_service_requests/it_service_requests_list_screen.dart +++ b/lib/screens/it_service_requests/it_service_requests_list_screen.dart @@ -17,6 +17,7 @@ import '../../providers/tickets_provider.dart'; import '../../theme/m3_motion.dart'; import '../../utils/app_time.dart'; import '../../utils/snackbar.dart'; +import '../../theme/app_status_colors.dart'; import '../../widgets/m3_card.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/reconnect_overlay.dart'; @@ -381,9 +382,6 @@ class _ItServiceRequestsListScreenState builder: (ctx, setDialogState) { return AlertDialog( title: const Text('New IT Service Request'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), - ), content: SizedBox( width: 400, child: SingleChildScrollView( @@ -538,6 +536,7 @@ class _StatusSummaryRow extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final status = AppStatusColors.of(context); final pending = requests .where((r) => r.status == ItServiceRequestStatus.pendingApproval) .length; @@ -563,7 +562,7 @@ class _StatusSummaryRow extends StatelessWidget { _SummaryChip( label: 'Pending', count: pending, - color: cs.tertiary, + color: status.warning.color, selected: selectedStatus == ItServiceRequestStatus.pendingApproval, onTap: () => onStatusTap(ItServiceRequestStatus.pendingApproval), ), @@ -571,7 +570,7 @@ class _StatusSummaryRow extends StatelessWidget { _SummaryChip( label: 'Scheduled', count: scheduled, - color: cs.primary, + color: status.info.color, selected: selectedStatus == ItServiceRequestStatus.scheduled, onTap: () => onStatusTap(ItServiceRequestStatus.scheduled), ), @@ -587,7 +586,7 @@ class _StatusSummaryRow extends StatelessWidget { _SummaryChip( label: 'Completed', count: completed, - color: Colors.green, + color: status.success.color, selected: selectedStatus == ItServiceRequestStatus.completed, onTap: () => onStatusTap(ItServiceRequestStatus.completed), ), diff --git a/lib/screens/network_map/network_map_device_edit_screen.dart b/lib/screens/network_map/network_map_device_edit_screen.dart index baa6a2d0..67d36468 100644 --- a/lib/screens/network_map/network_map_device_edit_screen.dart +++ b/lib/screens/network_map/network_map_device_edit_screen.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import '../../models/network/network_device.dart'; import '../../models/network/network_location.dart'; +import '../../theme/app_status_colors.dart'; import '../../providers/network_map/network_devices_provider.dart'; import '../../providers/network_map/network_sites_provider.dart'; import '../../widgets/app_state_view.dart'; @@ -231,11 +232,19 @@ class _NetworkMapDeviceEditScreenState final cs = Theme.of(context).colorScheme; switch (_status) { case NetworkDeviceStatus.online: - return const Icon(Icons.circle, size: 14, color: Color(0xFF388E3C)); + return Icon( + Icons.circle, + size: 14, + color: AppStatusColors.of(context).success.color, + ); case NetworkDeviceStatus.offline: return Icon(Icons.circle, size: 14, color: cs.error); case NetworkDeviceStatus.warning: - return const Icon(Icons.circle, size: 14, color: Color(0xFFF57C00)); + return Icon( + Icons.circle, + size: 14, + color: AppStatusColors.of(context).warning.color, + ); case NetworkDeviceStatus.unknown: return Icon(Icons.circle_outlined, size: 14, color: cs.onSurfaceVariant); } diff --git a/lib/screens/network_map/widgets/device_node.dart b/lib/screens/network_map/widgets/device_node.dart index af59b244..e94ac4b0 100644 --- a/lib/screens/network_map/widgets/device_node.dart +++ b/lib/screens/network_map/widgets/device_node.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../../models/network/network_device.dart'; +import '../../../theme/app_status_colors.dart'; import 'network_device_icon.dart'; /// A styled node representing one network device on the topology canvas. @@ -41,14 +42,15 @@ class DeviceNode extends StatelessWidget { } } - Color? _statusColor(ColorScheme cs, NetworkDeviceStatus status) { + Color? _statusColor(BuildContext context, NetworkDeviceStatus status) { + final s = AppStatusColors.of(context); switch (status) { case NetworkDeviceStatus.online: - return const Color(0xFF34A853); // Google green — universally understood + return s.success.color; case NetworkDeviceStatus.offline: - return cs.error; + return Theme.of(context).colorScheme.error; case NetworkDeviceStatus.warning: - return const Color(0xFFFBBC04); // Amber + return s.warning.color; case NetworkDeviceStatus.unknown: return null; // No dot shown } @@ -59,7 +61,7 @@ class DeviceNode extends StatelessWidget { final cs = Theme.of(context).colorScheme; final tt = Theme.of(context).textTheme; final accent = _accentForRole(cs, device.role); - final statusColor = _statusColor(cs, device.status); + final statusColor = _statusColor(context, device.status); final bgColor = isSelected ? cs.primaryContainer diff --git a/lib/screens/network_map/widgets/topology_legend.dart b/lib/screens/network_map/widgets/topology_legend.dart index ee6d6100..b45df1fe 100644 --- a/lib/screens/network_map/widgets/topology_legend.dart +++ b/lib/screens/network_map/widgets/topology_legend.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../../models/network/network_device.dart'; import '../../../models/network/network_link.dart'; +import '../../../theme/app_surfaces.dart'; /// Collapsible legend overlay explaining device role border colours and link /// type colours. Positioned at the bottom-left of the topology canvas view. @@ -85,11 +86,15 @@ class _TopologyLegendState extends State // Expand/collapse toggle chip Material( color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).containerRadius, + ), elevation: 2, child: InkWell( onTap: _toggle, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).containerRadius, + ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( diff --git a/lib/screens/reports/widgets/request_distribution_charts.dart b/lib/screens/reports/widgets/request_distribution_charts.dart index 82b46324..33fb5269 100644 --- a/lib/screens/reports/widgets/request_distribution_charts.dart +++ b/lib/screens/reports/widgets/request_distribution_charts.dart @@ -109,7 +109,7 @@ class _RequestTypeChartState extends ConsumerState { radius: isTouched ? 60 : 50, color: colorFor(i, e.name), borderSide: isTouched - ? const BorderSide(color: Colors.white, width: 2) + ? BorderSide(color: Theme.of(context).colorScheme.surface, width: 2) : BorderSide.none, ); }).toList(), @@ -240,7 +240,7 @@ class _RequestCategoryChartState extends ConsumerState { radius: isTouched ? 60 : 50, color: colorFor(i, e.name), borderSide: isTouched - ? const BorderSide(color: Colors.white, width: 2) + ? BorderSide(color: Theme.of(context).colorScheme.surface, width: 2) : BorderSide.none, ); }).toList(), diff --git a/lib/screens/reports/widgets/status_charts.dart b/lib/screens/reports/widgets/status_charts.dart index d4cc4365..422cd249 100644 --- a/lib/screens/reports/widgets/status_charts.dart +++ b/lib/screens/reports/widgets/status_charts.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../providers/reports_provider.dart'; +import '../../../theme/app_status_colors.dart'; import 'report_card_wrapper.dart'; /// Donut chart ticket counts per status with hover animation. @@ -84,7 +85,7 @@ class _TicketsByStatusChartState extends ConsumerState { radius: isTouched ? 60 : 50, color: _ticketStatusColor(context, e.status), borderSide: isTouched - ? const BorderSide(color: Colors.white, width: 2) + ? BorderSide(color: Theme.of(context).colorScheme.surface, width: 2) : BorderSide.none, ); }).toList(), @@ -116,16 +117,18 @@ class _TicketsByStatusChartState extends ConsumerState { } Color _ticketStatusColor(BuildContext context, String status) { - final colors = Theme.of(context).colorScheme; + final s = AppStatusColors.of(context); switch (status) { + case 'critical': + return s.danger.color; case 'pending': - return colors.tertiary; + return s.warning.color; case 'promoted': - return colors.secondary; + return s.info.color; case 'closed': - return colors.primary; + return s.success.color; default: - return colors.outlineVariant; + return s.neutral.color; } } } @@ -208,7 +211,7 @@ class _TasksByStatusChartState extends ConsumerState { radius: isTouched ? 60 : 50, color: _taskStatusColor(context, e.status), borderSide: isTouched - ? const BorderSide(color: Colors.white, width: 2) + ? BorderSide(color: Theme.of(context).colorScheme.surface, width: 2) : BorderSide.none, ); }).toList(), @@ -240,18 +243,20 @@ class _TasksByStatusChartState extends ConsumerState { } Color _taskStatusColor(BuildContext context, String status) { - final colors = Theme.of(context).colorScheme; + final s = AppStatusColors.of(context); switch (status) { + case 'critical': + return s.danger.color; case 'queued': - return colors.surfaceContainerHighest; + return s.neutral.color; case 'in_progress': - return colors.secondary; + return s.info.color; case 'completed': - return colors.primary; + return s.success.color; case 'cancelled': - return colors.error; + return s.danger.color; default: - return colors.outlineVariant; + return s.neutral.color; } } diff --git a/lib/screens/shared/mobile_verification_screen.dart b/lib/screens/shared/mobile_verification_screen.dart index c03919ff..6afac364 100644 --- a/lib/screens/shared/mobile_verification_screen.dart +++ b/lib/screens/shared/mobile_verification_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/verification_session_provider.dart'; import '../../services/face_verification.dart' as face; +import '../../theme/app_status_colors.dart'; /// Screen opened on mobile when the user scans a QR code for cross-device /// face verification. Performs liveness detection and uploads the result. @@ -144,7 +145,11 @@ class _MobileVerificationScreenState return Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.check_circle, size: 64, color: Colors.green), + Icon( + Icons.check_circle, + size: 64, + color: AppStatusColors.of(context).success.color, + ), const SizedBox(height: 16), Text('Verification Complete', style: theme.textTheme.headlineSmall), const SizedBox(height: 8), diff --git a/lib/screens/shared/permissions_screen.dart b/lib/screens/shared/permissions_screen.dart index 78ff05e8..d048f3b8 100644 --- a/lib/screens/shared/permissions_screen.dart +++ b/lib/screens/shared/permissions_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../theme/app_status_colors.dart'; import '../../theme/m3_motion.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -67,7 +68,10 @@ class _PermissionsScreenState extends ConsumerState { title: Text(info.label), subtitle: Text(status?.toString() ?? 'unknown'), trailing: granted - ? const Icon(Icons.check, color: Colors.green) + ? Icon( + Icons.check, + color: AppStatusColors.of(context).success.color, + ) : Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/screens/tasks/it_job_checklist_tab.dart b/lib/screens/tasks/it_job_checklist_tab.dart index f2a99c11..8cca094e 100644 --- a/lib/screens/tasks/it_job_checklist_tab.dart +++ b/lib/screens/tasks/it_job_checklist_tab.dart @@ -13,6 +13,8 @@ import '../../providers/teams_provider.dart'; import '../../utils/app_time.dart'; import '../../utils/snackbar.dart'; import '../../widgets/app_breakpoints.dart'; +import '../../theme/app_status_colors.dart'; +import '../../theme/app_surfaces.dart'; import '../../widgets/m3_card.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/profile_avatar.dart'; @@ -231,24 +233,25 @@ class _ItJobChecklistTabState extends ConsumerState { } Widget _buildSkeletonTile(BuildContext context, bool isWide) { + final bone = Theme.of(context).colorScheme.surfaceContainerHighest; return M3Card.outlined( child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ - Container(width: 60, height: 14, color: Colors.grey), + Container(width: 60, height: 14, color: bone), const SizedBox(width: 12), Expanded( - child: Container(height: 14, color: Colors.grey), + child: Container(height: 14, color: bone), ), if (isWide) ...[ const SizedBox(width: 12), - Container(width: 80, height: 22, color: Colors.grey), + Container(width: 80, height: 22, color: bone), const SizedBox(width: 12), - Container(width: 120, height: 14, color: Colors.grey), + Container(width: 120, height: 14, color: bone), ], const SizedBox(width: 12), - Container(width: 24, height: 24, color: Colors.grey), + Container(width: 24, height: 24, color: bone), ], ), ), @@ -292,20 +295,21 @@ class _StatsCard extends StatelessWidget { ), ); + final status = AppStatusColors.of(context); final submittedChip = _StatChip( icon: Icons.check_circle_outline, label: 'Submitted', count: submitted, - color: cs.primary, - bgColor: cs.primaryContainer, + color: status.success.onContainer, + bgColor: status.success.container, ); final pendingChip = _StatChip( icon: Icons.pending_outlined, label: 'Pending', count: pending, - color: cs.tertiary, - bgColor: cs.tertiaryContainer, + color: status.warning.onContainer, + bgColor: status.warning.container, ); return M3Card.filled( @@ -399,7 +403,7 @@ class _StatChip extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: bgColor, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -878,11 +882,14 @@ class _ItJobTileState extends ConsumerState<_ItJobTile> { _isChecked ? cs.primaryContainer.withValues(alpha: 0.18) : null; // Status badge + final statusSwatch = _isChecked + ? AppStatusColors.of(context).success + : AppStatusColors.of(context).warning; final statusBadge = Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( - color: _isChecked ? cs.primaryContainer : cs.tertiaryContainer, - borderRadius: BorderRadius.circular(20), + color: statusSwatch.container, + borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -892,13 +899,13 @@ class _ItJobTileState extends ConsumerState<_ItJobTile> { ? Icons.check_circle_outline : Icons.hourglass_empty_outlined, size: 12, - color: _isChecked ? cs.primary : cs.tertiary, + color: statusSwatch.onContainer, ), const SizedBox(width: 4), Text( _isChecked ? 'Submitted' : 'Pending', style: tt.labelSmall?.copyWith( - color: _isChecked ? cs.primary : cs.tertiary, + color: statusSwatch.onContainer, fontWeight: FontWeight.w600, ), ), diff --git a/lib/screens/tasks/task_detail_screen.dart b/lib/screens/tasks/task_detail_screen.dart index b6979c0a..ddd89d94 100644 --- a/lib/screens/tasks/task_detail_screen.dart +++ b/lib/screens/tasks/task_detail_screen.dart @@ -35,6 +35,7 @@ import '../../widgets/office_picker.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; import '../../theme/app_surfaces.dart'; +import '../../theme/app_status_colors.dart'; import '../../widgets/sync_pending_badge.dart'; import '../../widgets/task_assignment_section.dart'; import '../../widgets/typing_dots.dart'; @@ -621,9 +622,9 @@ class _TaskDetailScreenState extends ConsumerState const SizedBox(height: 12), Row( children: [ - const Icon( + Icon( Icons.warning_amber_rounded, - color: Colors.orange, + color: AppStatusColors.of(context).warning.color, ), const SizedBox(width: 6), Expanded( @@ -2686,16 +2687,24 @@ class _TaskDetailScreenState extends ConsumerState final files = _attachments ?? []; if (files.isEmpty) { - return const Center( + return Center( child: Padding( - padding: EdgeInsets.all( + padding: + const EdgeInsets.all( 16.0, ), child: Text( 'No attachments yet', - style: TextStyle( - color: Colors.grey, - ), + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of( + context, + ) + .colorScheme + .onSurfaceVariant, + ), ), ), ); @@ -2722,9 +2731,11 @@ class _TaskDetailScreenState extends ConsumerState ).textTheme.bodySmall, ), trailing: IconButton( - icon: const Icon( + icon: Icon( Icons.delete, - color: Colors.red, + color: Theme.of( + context, + ).colorScheme.error, ), onPressed: () => _deleteTaskAttachment( @@ -2873,7 +2884,11 @@ class _TaskDetailScreenState extends ConsumerState .colorScheme .surfaceContainerHighest, borderRadius: - BorderRadius.circular(16), + BorderRadius.circular( + AppSurfaces.of( + context, + ).cardRadius, + ), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -4883,39 +4898,21 @@ class _TaskStatusChip extends StatelessWidget { @override Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - final (icon, bg, fg) = switch (status) { - 'queued' => ( - Icons.inbox_rounded, - cs.secondaryContainer, - cs.onSecondaryContainer, - ), - 'in_progress' => ( - Icons.play_arrow_rounded, - cs.primaryContainer, - cs.onPrimaryContainer, - ), - 'completed' => ( - Icons.check_circle_rounded, - cs.tertiaryContainer, - cs.onTertiaryContainer, - ), - 'cancelled' => ( - Icons.cancel_rounded, - cs.errorContainer, - cs.onErrorContainer, - ), - _ => ( - Icons.help_outline_rounded, - cs.surfaceContainerHighest, - cs.onSurface, - ), + final s = AppStatusColors.of(context); + final (IconData icon, StatusSwatch swatch) = switch (status) { + 'queued' => (Icons.inbox_rounded, s.neutral), + 'in_progress' => (Icons.play_arrow_rounded, s.info), + 'completed' => (Icons.check_circle_rounded, s.success), + 'cancelled' => (Icons.cancel_rounded, s.danger), + _ => (Icons.help_outline_rounded, s.neutral), }; + final bg = swatch.container; + final fg = swatch.onContainer; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: bg, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius), ), child: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/tasks/tasks_list_screen.dart b/lib/screens/tasks/tasks_list_screen.dart index 0cf816e7..4a9222ba 100644 --- a/lib/screens/tasks/tasks_list_screen.dart +++ b/lib/screens/tasks/tasks_list_screen.dart @@ -29,6 +29,7 @@ import '../../widgets/responsive_body.dart'; import '../../widgets/tasq_adaptive_list.dart'; import '../../widgets/typing_dots.dart'; import '../../theme/app_surfaces.dart'; +import '../../theme/app_status_colors.dart'; import '../../utils/snackbar.dart'; import '../../widgets/app_breakpoints.dart'; import '../../widgets/app_page_header.dart'; @@ -523,10 +524,10 @@ class _TasksListScreenState extends ConsumerState if (task.status == 'completed' && task.hasIncompleteDetails) ...[ const SizedBox(width: 4), - const Icon( + Icon( Icons.warning_amber_rounded, size: 16, - color: Colors.orange, + color: AppStatusColors.of(context).warning.color, ), ], ], @@ -620,10 +621,10 @@ class _TasksListScreenState extends ConsumerState if (task.status == 'completed' && task.hasIncompleteDetails) ...[ const SizedBox(width: 4), - const Icon( + Icon( Icons.warning_amber_rounded, size: 16, - color: Colors.orange, + color: AppStatusColors.of(context).warning.color, ), ], if (showTyping) ...[ @@ -1353,26 +1354,14 @@ class _StatusSummaryCard extends StatelessWidget { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; - final background = switch (status) { - 'critical' => scheme.errorContainer, - 'queued' => scheme.surfaceContainerHighest, - 'in_progress' => scheme.secondaryContainer, - 'completed' => scheme.primaryContainer, - _ => scheme.surfaceContainerHigh, - }; - final foreground = switch (status) { - 'critical' => scheme.onErrorContainer, - 'queued' => scheme.onSurfaceVariant, - 'in_progress' => scheme.onSecondaryContainer, - 'completed' => scheme.onPrimaryContainer, - _ => scheme.onSurfaceVariant, - }; + final swatch = AppStatusColors.of(context).byKind(_taskStatusKind(status)); + final background = swatch.container; + final foreground = swatch.onContainer; final label = status.replaceAll('_', ' ').toUpperCase(); return Material( color: background, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( @@ -1401,6 +1390,16 @@ class _StatusSummaryCard extends StatelessWidget { } } +/// Maps a task status to a semantic [StatusKind] (red=critical, blue=in +/// progress, green=completed, grey=queued). +StatusKind _taskStatusKind(String status) => switch (status) { + 'critical' => StatusKind.danger, + 'in_progress' => StatusKind.info, + 'completed' => StatusKind.success, + 'queued' => StatusKind.neutral, + _ => StatusKind.neutral, +}; + String _assignedAgent(Map profileById, String? userId) { if (userId == null || userId.isEmpty) { return 'Unassigned'; @@ -1422,21 +1421,9 @@ class _StatusBadge extends StatelessWidget { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; - final background = switch (status) { - 'critical' => scheme.errorContainer, - 'queued' => scheme.surfaceContainerHighest, - 'in_progress' => scheme.secondaryContainer, - 'completed' => scheme.primaryContainer, - _ => scheme.surfaceContainerHighest, - }; - final foreground = switch (status) { - 'critical' => scheme.onErrorContainer, - 'queued' => scheme.onSurfaceVariant, - 'in_progress' => scheme.onSecondaryContainer, - 'completed' => scheme.onPrimaryContainer, - _ => scheme.onSurfaceVariant, - }; + final swatch = AppStatusColors.of(context).byKind(_taskStatusKind(status)); + final background = swatch.container; + final foreground = swatch.onContainer; return Badge( backgroundColor: background, diff --git a/lib/screens/tickets/ticket_detail_screen.dart b/lib/screens/tickets/ticket_detail_screen.dart index fd909176..204c54c3 100644 --- a/lib/screens/tickets/ticket_detail_screen.dart +++ b/lib/screens/tickets/ticket_detail_screen.dart @@ -22,6 +22,7 @@ import '../../widgets/responsive_body.dart'; import '../../widgets/task_assignment_section.dart'; import '../../widgets/typing_dots.dart'; import '../../theme/app_surfaces.dart'; +import '../../theme/app_status_colors.dart'; class TicketDetailScreen extends ConsumerStatefulWidget { const TicketDetailScreen({super.key, required this.ticketId}); @@ -424,7 +425,9 @@ class _TicketDetailScreenState extends ConsumerState { color: Theme.of( context, ).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).cardRadius, + ), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -467,7 +470,9 @@ class _TicketDetailScreenState extends ConsumerState { color: Theme.of(context) .colorScheme .surfaceContainerHighest, - borderRadius: BorderRadius.circular(28), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).containerRadius, + ), ), padding: const EdgeInsets.only( left: 16, @@ -1357,34 +1362,20 @@ class _TicketStatusChip extends StatelessWidget { @override Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - final (icon, bg, fg) = switch (status) { - 'pending' => ( - Icons.hourglass_empty_rounded, - cs.secondaryContainer, - cs.onSecondaryContainer, - ), - 'promoted' => ( - Icons.trending_up_rounded, - cs.primaryContainer, - cs.onPrimaryContainer, - ), - 'closed' => ( - Icons.lock_rounded, - cs.tertiaryContainer, - cs.onTertiaryContainer, - ), - _ => ( - Icons.help_outline_rounded, - cs.surfaceContainerHighest, - cs.onSurface, - ), + final s = AppStatusColors.of(context); + final (IconData icon, StatusSwatch swatch) = switch (status) { + 'pending' => (Icons.hourglass_empty_rounded, s.warning), + 'promoted' => (Icons.trending_up_rounded, s.info), + 'closed' => (Icons.lock_rounded, s.success), + _ => (Icons.help_outline_rounded, s.neutral), }; + final bg = swatch.container; + final fg = swatch.onContainer; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: bg, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius), ), child: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/tickets/tickets_list_screen.dart b/lib/screens/tickets/tickets_list_screen.dart index 9816219e..76bbb81e 100644 --- a/lib/screens/tickets/tickets_list_screen.dart +++ b/lib/screens/tickets/tickets_list_screen.dart @@ -23,6 +23,7 @@ import '../../widgets/responsive_body.dart'; import '../../widgets/tasq_adaptive_list.dart'; import '../../widgets/typing_dots.dart'; import '../../theme/app_surfaces.dart'; +import '../../theme/app_status_colors.dart'; import '../../utils/snackbar.dart'; import '../../widgets/app_breakpoints.dart'; import '../../widgets/app_page_header.dart'; @@ -739,26 +740,14 @@ class _StatusSummaryCard extends StatelessWidget { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; - final background = switch (status) { - 'critical' => scheme.errorContainer, - 'pending' => scheme.tertiaryContainer, - 'promoted' => scheme.secondaryContainer, - 'closed' => scheme.primaryContainer, - _ => scheme.surfaceContainerHigh, - }; - final foreground = switch (status) { - 'critical' => scheme.onErrorContainer, - 'pending' => scheme.onTertiaryContainer, - 'promoted' => scheme.onSecondaryContainer, - 'closed' => scheme.onPrimaryContainer, - _ => scheme.onSurfaceVariant, - }; + final swatch = AppStatusColors.of(context).byKind(_ticketStatusKind(status)); + final background = swatch.container; + final foreground = swatch.onContainer; final label = status.replaceAll('_', ' ').toUpperCase(); return Material( color: background, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( @@ -787,6 +776,16 @@ class _StatusSummaryCard extends StatelessWidget { } } +/// Maps a ticket status to a semantic [StatusKind] so its color carries meaning +/// (red=critical, amber=pending, blue=promoted/escalated, green=closed). +StatusKind _ticketStatusKind(String status) => switch (status) { + 'critical' => StatusKind.danger, + 'pending' => StatusKind.warning, + 'promoted' => StatusKind.info, + 'closed' => StatusKind.success, + _ => StatusKind.neutral, +}; + /// First 8 chars of the UUID, like a short git hash; the full id is in the /// tooltip. Tickets have no human-friendly number. String _shortId(String id) => @@ -813,21 +812,9 @@ class _StatusBadge extends StatelessWidget { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; - final background = switch (status) { - 'critical' => scheme.errorContainer, - 'pending' => scheme.tertiaryContainer, - 'promoted' => scheme.secondaryContainer, - 'closed' => scheme.primaryContainer, - _ => scheme.surfaceContainerHighest, - }; - final foreground = switch (status) { - 'critical' => scheme.onErrorContainer, - 'pending' => scheme.onTertiaryContainer, - 'promoted' => scheme.onSecondaryContainer, - 'closed' => scheme.onPrimaryContainer, - _ => scheme.onSurfaceVariant, - }; + final swatch = AppStatusColors.of(context).byKind(_ticketStatusKind(status)); + final background = swatch.container; + final foreground = swatch.onContainer; return Badge( backgroundColor: background, diff --git a/lib/screens/whereabouts/whereabouts_screen.dart b/lib/screens/whereabouts/whereabouts_screen.dart index 254c907f..4ecc4cb0 100644 --- a/lib/screens/whereabouts/whereabouts_screen.dart +++ b/lib/screens/whereabouts/whereabouts_screen.dart @@ -12,6 +12,7 @@ import '../../providers/profile_provider.dart'; import '../../providers/whereabouts_provider.dart'; import '../../providers/workforce_provider.dart'; import '../../theme/app_surfaces.dart'; +import '../../theme/app_status_colors.dart'; import '../../widgets/app_page_header.dart'; import '../../widgets/responsive_body.dart'; import '../../utils/app_time.dart'; @@ -232,8 +233,10 @@ class _WhereaboutsMap extends StatelessWidget { polygons: [ Polygon( points: points, - color: Colors.blue.withValues(alpha: 0.1), - borderColor: Colors.blue, + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.12), + borderColor: Theme.of(context).colorScheme.primary, borderStrokeWidth: 2.0, ), ], @@ -313,11 +316,20 @@ class _StaffLegendPanel extends StatelessWidget { ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), ), const Spacer(), - _RoleBadge(color: Colors.blue.shade700, label: 'Admin'), + _RoleBadge( + color: Theme.of(context).colorScheme.primary, + label: 'Admin', + ), const SizedBox(width: 12), - _RoleBadge(color: Colors.green.shade700, label: 'IT Staff'), + _RoleBadge( + color: Theme.of(context).colorScheme.secondary, + label: 'IT Staff', + ), const SizedBox(width: 12), - _RoleBadge(color: Colors.orange.shade700, label: 'Dispatcher'), + _RoleBadge( + color: Theme.of(context).colorScheme.tertiary, + label: 'Dispatcher', + ), ], ), ), @@ -416,6 +428,7 @@ class _StaffLegendTile extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final status = AppStatusColors.of(context); final roleColor = _roleColor(profile.role, cs); final hasPosition = position != null; @@ -445,30 +458,30 @@ class _StaffLegendTile extends StatelessWidget { ? 'In premise (Tracking off) \u00b7 $timeLabel' : 'Outside premise (Tracking off) \u00b7 $timeLabel'; statusColor = isStale - ? Colors.grey - : (isInPremise ? Colors.green : Colors.grey); + ? status.neutral.color + : (isInPremise ? status.success.color : status.neutral.color); } else if (inferredInPremise) { statusText = 'In premise (Checked in)'; - statusColor = Colors.green; + statusColor = status.success.color; } else { statusText = 'Tracking off'; - statusColor = Colors.grey; + statusColor = status.neutral.color; } } else if (isStale && hasPosition) { final timeLabel = _timeAgo(position!.updatedAt); statusText = isInPremise ? 'Last seen in premise \u00b7 $timeLabel' : 'Last seen outside premise \u00b7 $timeLabel'; - statusColor = Colors.grey; + statusColor = status.neutral.color; } else if (isActive) { statusText = 'In premise \u00b7 ${_timeAgo(position!.updatedAt)}'; - statusColor = Colors.green; + statusColor = status.success.color; } else if (hasPosition) { statusText = 'Outside premise \u00b7 ${_timeAgo(position!.updatedAt)}'; - statusColor = Colors.grey; + statusColor = status.neutral.color; } else { statusText = 'No location data'; - statusColor = Colors.grey; + statusColor = status.neutral.color; } final IconData statusIcon; diff --git a/lib/theme/app_status_colors.dart b/lib/theme/app_status_colors.dart new file mode 100644 index 00000000..4a66f5d7 --- /dev/null +++ b/lib/theme/app_status_colors.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; + +/// Semantic status categories shared across status pills, chips and badges. +enum StatusKind { success, warning, danger, info, neutral } + +/// A single semantic status swatch: a vivid [color] for icons/dots/text, plus a +/// tonal [container] / [onContainer] pair for filled pills and badges. +@immutable +class StatusSwatch { + const StatusSwatch({ + required this.color, + required this.container, + required this.onContainer, + }); + + /// Vivid accent — use for status dots, icons, borders and emphasized text. + final Color color; + + /// Tonal fill for pills/badges/callout surfaces. + final Color container; + + /// Foreground (text/icon) drawn on top of [container]. + final Color onContainer; + + StatusSwatch copyWith({Color? color, Color? container, Color? onContainer}) { + return StatusSwatch( + color: color ?? this.color, + container: container ?? this.container, + onContainer: onContainer ?? this.onContainer, + ); + } + + static StatusSwatch lerp(StatusSwatch a, StatusSwatch b, double t) { + return StatusSwatch( + color: Color.lerp(a.color, b.color, t) ?? a.color, + container: Color.lerp(a.container, b.container, t) ?? a.container, + onContainer: Color.lerp(a.onContainer, b.onContainer, t) ?? a.onContainer, + ); + } +} + +/// Cyber/IT semantic status tokens. +/// +/// These replace scattered raw `Colors.green/orange/red/grey` usages so status +/// meaning is consistent and legible in **both** light and dark themes. Access +/// via [AppStatusColors.of] and pick a swatch by meaning: +/// +/// ```dart +/// final status = AppStatusColors.of(context); +/// Icon(Icons.circle, size: 10, color: status.success.color); // online / up +/// ``` +@immutable +class AppStatusColors extends ThemeExtension { + const AppStatusColors({ + required this.success, + required this.warning, + required this.danger, + required this.info, + required this.neutral, + }); + + /// Resolved, present, online, device-up, healthy. + final StatusSwatch success; + + /// In-progress, pending, warning, degraded. + final StatusSwatch warning; + + /// Urgent, critical, absent, error, device-down. + final StatusSwatch danger; + + /// Informational / low-priority highlight. + final StatusSwatch info; + + /// Idle, offline, disabled, muted. + final StatusSwatch neutral; + + // ── Dark (cyber-first) ────────────────────────────────────────────────── + static const AppStatusColors dark = AppStatusColors( + success: StatusSwatch( + color: Color(0xFF34D399), + container: Color(0xFF0C4A34), + onContainer: Color(0xFF8FF0C4), + ), + warning: StatusSwatch( + color: Color(0xFFFBBF24), + container: Color(0xFF4A3506), + onContainer: Color(0xFFFCE199), + ), + danger: StatusSwatch( + color: Color(0xFFF87171), + container: Color(0xFF5A1A1A), + onContainer: Color(0xFFFFC9C4), + ), + info: StatusSwatch( + color: Color(0xFF60A5FA), + container: Color(0xFF0E3A5F), + onContainer: Color(0xFFCFE7FF), + ), + neutral: StatusSwatch( + color: Color(0xFF94A3B8), + container: Color(0xFF26313D), + onContainer: Color(0xFFCBD5E1), + ), + ); + + // ── Light ─────────────────────────────────────────────────────────────── + static const AppStatusColors light = AppStatusColors( + success: StatusSwatch( + color: Color(0xFF15803D), + container: Color(0xFFCDEFD8), + onContainer: Color(0xFF052E16), + ), + warning: StatusSwatch( + color: Color(0xFFB45309), + container: Color(0xFFFCEBC7), + onContainer: Color(0xFF3A2606), + ), + danger: StatusSwatch( + color: Color(0xFFDC2626), + container: Color(0xFFFBD9D6), + onContainer: Color(0xFF450A0A), + ), + info: StatusSwatch( + color: Color(0xFF2563EB), + container: Color(0xFFD6E4FF), + onContainer: Color(0xFF0A2A6B), + ), + neutral: StatusSwatch( + color: Color(0xFF64748B), + container: Color(0xFFE2E8F0), + onContainer: Color(0xFF1E293B), + ), + ); + + static AppStatusColors of(BuildContext context) { + return Theme.of(context).extension() ?? dark; + } + + /// Resolve the swatch for a semantic [StatusKind]. + StatusSwatch byKind(StatusKind kind) { + switch (kind) { + case StatusKind.success: + return success; + case StatusKind.warning: + return warning; + case StatusKind.danger: + return danger; + case StatusKind.info: + return info; + case StatusKind.neutral: + return neutral; + } + } + + @override + AppStatusColors copyWith({ + StatusSwatch? success, + StatusSwatch? warning, + StatusSwatch? danger, + StatusSwatch? info, + StatusSwatch? neutral, + }) { + return AppStatusColors( + success: success ?? this.success, + warning: warning ?? this.warning, + danger: danger ?? this.danger, + info: info ?? this.info, + neutral: neutral ?? this.neutral, + ); + } + + @override + AppStatusColors lerp(ThemeExtension? other, double t) { + if (other is! AppStatusColors) return this; + return AppStatusColors( + success: StatusSwatch.lerp(success, other.success, t), + warning: StatusSwatch.lerp(warning, other.warning, t), + danger: StatusSwatch.lerp(danger, other.danger, t), + info: StatusSwatch.lerp(info, other.info, t), + neutral: StatusSwatch.lerp(neutral, other.neutral, t), + ); + } +} diff --git a/lib/theme/app_surfaces.dart b/lib/theme/app_surfaces.dart index 91116280..02e62095 100644 --- a/lib/theme/app_surfaces.dart +++ b/lib/theme/app_surfaces.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; -/// M3 Expressive surface tokens. +/// Cyber/IT surface tokens. /// -/// Cards now use **tonal elevation** (color tints) instead of drop-shadows. -/// Large containers adopt the M3 standard 28 dp corner radius; compact items -/// use 16 dp; small chips/badges use 12 dp. +/// Cards use **tonal elevation** (color tints) plus a hairline outline instead +/// of drop-shadows. The shape language is tightened to a precise 12–16 dp +/// technical feel: standard cards 12 dp, compact items 10 dp, large containers +/// 16 dp, dialogs/sheets 20 dp, and chips/badges 8 dp. @immutable class AppSurfaces extends ThemeExtension { const AppSurfaces({ @@ -15,19 +16,19 @@ class AppSurfaces extends ThemeExtension { required this.chipRadius, }); - /// Standard card radius – 16 dp (M3 medium shape). + /// Standard card radius – 12 dp (cyber/IT medium shape). final double cardRadius; - /// Compact card radius for dense list tiles – 12 dp. + /// Compact card radius for dense list tiles – 10 dp. final double compactCardRadius; - /// Large container radius – 28 dp (M3 Expressive). + /// Large container radius – 16 dp. final double containerRadius; - /// Dialog / bottom-sheet radius – 28 dp. + /// Dialog / bottom-sheet radius – 20 dp. final double dialogRadius; - /// Chip / badge radius – 12 dp. + /// Chip / badge radius – 8 dp. final double chipRadius; // convenience shapes @@ -46,11 +47,11 @@ class AppSurfaces extends ThemeExtension { final ext = Theme.of(context).extension(); return ext ?? const AppSurfaces( - cardRadius: 16, - compactCardRadius: 12, - containerRadius: 28, - dialogRadius: 28, - chipRadius: 12, + cardRadius: 12, + compactCardRadius: 10, + containerRadius: 16, + dialogRadius: 20, + chipRadius: 8, ); } diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 5b694126..43a4f33d 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -3,57 +3,140 @@ import 'package:google_fonts/google_fonts.dart'; import 'app_typography.dart'; import 'app_surfaces.dart'; +import 'app_status_colors.dart'; -/// M3 Expressive theme for TasQ. +/// Refined enterprise **Cyber/IT** theme for TasQ, built on Material 3. /// -/// Key differences from the previous Hybrid M2/M3 theme: -/// * Cards use **tonal elevation** (surfaceTint color overlays) instead of -/// drop-shadows, giving surfaces an organic, seed-tinted look. -/// * Large containers use the M3 standard **28 dp** corner radius. -/// * Buttons follow the M3 hierarchy: FilledButton (primary), Tonal, Elevated, -/// Outlined, and Text. -/// * NavigationBar / NavigationRail use pill-shaped indicators with the -/// secondary-container tonal color. -/// * Spring-physics inspired durations: transitions default to 400 ms with an -/// emphasized easing curve. +/// Identity: +/// * Deep slate surfaces (dark) / cool near-white (light) with an electric +/// **azure** primary, **teal** secondary and **violet** tertiary. +/// * Cards use a flat tonal fill plus a hairline outline (no drop shadow) for a +/// precise, technical feel. +/// * A tightened 12–16 dp shape language (see [AppSurfaces]). +/// * Monospace ([AppMonoText]) reserved for technical data — IDs, IPs, ports, +/// timestamps — and chip labels. +/// * Semantic status tokens ([AppStatusColors]) drive all status/priority color. class AppTheme { - /// The seed color drives M3's entire tonal palette generation. - static const Color _seed = Color(0xFF4A6FA5); + const AppTheme._(); + + /// Brand azure — anchors M3's neutral tonal ramp generation. + static const Color _seed = Color(0xFF2D7FF9); + + static const AppSurfaces _surfaces = AppSurfaces( + cardRadius: 12, + compactCardRadius: 10, + containerRadius: 16, + dialogRadius: 20, + chipRadius: 8, + ); + + // ──────────────────────────────────────────────────────────── + // COLOR SCHEMES (hand-tuned cyber palette) + // ──────────────────────────────────────────────────────────── + static ColorScheme _darkScheme() { + return ColorScheme.fromSeed( + seedColor: _seed, + brightness: Brightness.dark, + ).copyWith( + primary: const Color(0xFF4EA8FF), + onPrimary: const Color(0xFF04263F), + primaryContainer: const Color(0xFF0E3A5F), + onPrimaryContainer: const Color(0xFFCFE7FF), + secondary: const Color(0xFF2DD4BF), + onSecondary: const Color(0xFF00201C), + secondaryContainer: const Color(0xFF0C4B45), + onSecondaryContainer: const Color(0xFFB8F5EC), + tertiary: const Color(0xFFA78BFA), + onTertiary: const Color(0xFF1C0E3F), + tertiaryContainer: const Color(0xFF3A2A6B), + onTertiaryContainer: const Color(0xFFE7DDFF), + error: const Color(0xFFF87171), + onError: const Color(0xFF2A0A0A), + errorContainer: const Color(0xFF5A1A1A), + onErrorContainer: const Color(0xFFFFC9C4), + surface: const Color(0xFF0E141B), + onSurface: const Color(0xFFE3E9F0), + onSurfaceVariant: const Color(0xFFA7B4C2), + surfaceContainerLowest: const Color(0xFF080C11), + surfaceContainerLow: const Color(0xFF141B23), + surfaceContainer: const Color(0xFF17202A), + surfaceContainerHigh: const Color(0xFF1E2833), + surfaceContainerHighest: const Color(0xFF26313D), + surfaceDim: const Color(0xFF0B0F14), + surfaceBright: const Color(0xFF2A3542), + outline: const Color(0xFF3A4A5A), + outlineVariant: const Color(0xFF283440), + surfaceTint: const Color(0xFF4EA8FF), + inverseSurface: const Color(0xFFE3E9F0), + onInverseSurface: const Color(0xFF111820), + inversePrimary: const Color(0xFF0B6BCB), + ); + } + + static ColorScheme _lightScheme() { + return ColorScheme.fromSeed( + seedColor: _seed, + brightness: Brightness.light, + ).copyWith( + primary: const Color(0xFF0B6BCB), + onPrimary: const Color(0xFFFFFFFF), + primaryContainer: const Color(0xFFD3E7FF), + onPrimaryContainer: const Color(0xFF00294A), + secondary: const Color(0xFF0F766E), + onSecondary: const Color(0xFFFFFFFF), + secondaryContainer: const Color(0xFFB8F1E8), + onSecondaryContainer: const Color(0xFF00201C), + tertiary: const Color(0xFF6D28D9), + onTertiary: const Color(0xFFFFFFFF), + tertiaryContainer: const Color(0xFFE9DDFF), + onTertiaryContainer: const Color(0xFF23005C), + error: const Color(0xFFDC2626), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFFFBD9D6), + onErrorContainer: const Color(0xFF450A0A), + surface: const Color(0xFFF6F8FB), + onSurface: const Color(0xFF111820), + onSurfaceVariant: const Color(0xFF42505F), + surfaceContainerLowest: const Color(0xFFFFFFFF), + surfaceContainerLow: const Color(0xFFF0F4F9), + surfaceContainer: const Color(0xFFEDF1F6), + surfaceContainerHigh: const Color(0xFFE4EAF2), + surfaceContainerHighest: const Color(0xFFDCE4EE), + surfaceDim: const Color(0xFFD7DEE8), + surfaceBright: const Color(0xFFF6F8FB), + outline: const Color(0xFF73808F), + outlineVariant: const Color(0xFFC3CDDA), + surfaceTint: const Color(0xFF0B6BCB), + inverseSurface: const Color(0xFF111820), + onInverseSurface: const Color(0xFFEFF3F8), + inversePrimary: const Color(0xFFA6C8FF), + ); + } // ──────────────────────────────────────────────────────────── // LIGHT // ──────────────────────────────────────────────────────────── static ThemeData light() { - final base = ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: _seed, - brightness: Brightness.light, - ), - useMaterial3: true, + return _apply( + ThemeData(colorScheme: _lightScheme(), useMaterial3: true), + AppStatusColors.light, ); - - return _apply(base); } // ──────────────────────────────────────────────────────────── // DARK // ──────────────────────────────────────────────────────────── static ThemeData dark() { - final base = ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: _seed, - brightness: Brightness.dark, - ), - useMaterial3: true, + return _apply( + ThemeData(colorScheme: _darkScheme(), useMaterial3: true), + AppStatusColors.dark, ); - - return _apply(base); } // ──────────────────────────────────────────────────────────── // SHARED BUILDER // ──────────────────────────────────────────────────────────── - static ThemeData _apply(ThemeData base) { + static ThemeData _apply(ThemeData base, AppStatusColors status) { final cs = base.colorScheme; final isDark = cs.brightness == Brightness.dark; @@ -68,18 +151,17 @@ class AppTheme { const TextStyle(letterSpacing: 0.2), ); - const surfaces = AppSurfaces( - cardRadius: 16, - compactCardRadius: 12, - containerRadius: 28, - dialogRadius: 28, - chipRadius: 12, - ); + final surfaces = _surfaces; + + // Card fill: flat tonal surface + hairline outline (cyber "panel" look). + final cardColor = isDark ? cs.surfaceContainer : cs.surfaceContainerLowest; return base.copyWith( textTheme: textTheme, - scaffoldBackgroundColor: isDark ? cs.surface : cs.surfaceContainerLowest, - extensions: [mono, surfaces], + scaffoldBackgroundColor: isDark + ? const Color(0xFF0B0F14) + : cs.surfaceContainerLowest, + extensions: [mono, surfaces, status], // ── AppBar ────────────────────────────────────────────── appBarTheme: AppBarTheme( @@ -99,22 +181,27 @@ class AppTheme { ), ), - // ── Cards — M3 Elevated (tonal surface tint, no hard shadow) ── + // ── Cards — flat tonal fill + hairline outline (no hard shadow) ── cardTheme: CardThemeData( - color: isDark ? cs.surfaceContainer : cs.surfaceContainerLow, - elevation: 1, + color: cardColor, + elevation: 0, margin: EdgeInsets.zero, shadowColor: Colors.transparent, - surfaceTintColor: cs.surfaceTint, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(surfaces.cardRadius), + side: BorderSide(color: cs.outlineVariant), + ), ), - // ── Chips ─────────────────────────────────────────────── + // ── Chips — mono label, tight radius ──────────────────── chipTheme: ChipThemeData( backgroundColor: cs.surfaceContainerHighest, - side: BorderSide.none, - labelStyle: textTheme.labelSmall, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + side: BorderSide(color: cs.outlineVariant), + labelStyle: mono.label, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(surfaces.chipRadius), + ), ), // ── Dividers ──────────────────────────────────────────── @@ -123,17 +210,17 @@ class AppTheme { // ── Input Fields ──────────────────────────────────────── inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: cs.surfaceContainerLow, + fillColor: isDark ? cs.surfaceContainerLow : cs.surfaceContainer, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(surfaces.cardRadius), borderSide: BorderSide(color: cs.outlineVariant), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(surfaces.cardRadius), borderSide: BorderSide(color: cs.outlineVariant), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(surfaces.cardRadius), borderSide: BorderSide(color: cs.primary, width: 2), ), contentPadding: const EdgeInsets.symmetric( @@ -142,87 +229,84 @@ class AppTheme { ), ), - // ── Buttons — M3 Expressive hierarchy ─────────────────── + // ── Buttons — M3 hierarchy, tightened shapes ──────────── filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(surfaces.cardRadius), ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - textStyle: textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, - ), + textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), ), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - backgroundColor: cs.surfaceContainerLow, + backgroundColor: cs.surfaceContainerHigh, foregroundColor: cs.primary, - elevation: 1, + elevation: 0, shadowColor: Colors.transparent, - surfaceTintColor: cs.surfaceTint, + surfaceTintColor: Colors.transparent, + side: BorderSide(color: cs.outlineVariant), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(surfaces.cardRadius), ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - textStyle: textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, - ), + textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(surfaces.cardRadius), ), side: BorderSide(color: cs.outline), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), - textStyle: textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, - ), + textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(surfaces.cardRadius), ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - textStyle: textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, - ), + textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), ), ), iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(surfaces.compactCardRadius), ), ), ), segmentedButtonTheme: SegmentedButtonThemeData( style: ButtonStyle( shape: WidgetStateProperty.all( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(surfaces.cardRadius), + ), ), ), ), - // ── FAB — M3 Expressive ────────────────────────────────── + // ── FAB ────────────────────────────────────────────────── floatingActionButtonTheme: FloatingActionButtonThemeData( backgroundColor: cs.primaryContainer, foregroundColor: cs.onPrimaryContainer, elevation: 3, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(surfaces.containerRadius), + ), ), - // ── Navigation — M3 Expressive pill indicators ────────── + // ── Navigation — pill indicators ──────────────────────── navigationDrawerTheme: NavigationDrawerThemeData( backgroundColor: cs.surface, indicatorColor: cs.secondaryContainer, tileHeight: 56, indicatorShape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), + borderRadius: BorderRadius.circular(surfaces.containerRadius), ), ), navigationRailTheme: NavigationRailThemeData( @@ -238,14 +322,14 @@ class AppTheme { ), indicatorColor: cs.secondaryContainer, indicatorShape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), + borderRadius: BorderRadius.circular(surfaces.containerRadius), ), ), navigationBarTheme: NavigationBarThemeData( backgroundColor: cs.surfaceContainer, indicatorColor: cs.secondaryContainer, indicatorShape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(surfaces.cardRadius), ), labelTextStyle: WidgetStateProperty.resolveWith((states) { final selected = states.contains(WidgetState.selected); @@ -260,15 +344,19 @@ class AppTheme { // ── List Tiles ────────────────────────────────────────── listTileTheme: ListTileThemeData( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(surfaces.cardRadius), + ), tileColor: Colors.transparent, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), ), // ── Dialogs ───────────────────────────────────────────── dialogTheme: DialogThemeData( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), - surfaceTintColor: cs.surfaceTint, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(surfaces.dialogRadius), + ), + surfaceTintColor: Colors.transparent, backgroundColor: isDark ? cs.surfaceContainerHigh : cs.surfaceContainerLowest, @@ -276,20 +364,24 @@ class AppTheme { // ── Bottom Sheets ─────────────────────────────────────── bottomSheetTheme: BottomSheetThemeData( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(surfaces.dialogRadius), + ), ), backgroundColor: isDark ? cs.surfaceContainerHigh : cs.surfaceContainerLowest, - surfaceTintColor: cs.surfaceTint, + surfaceTintColor: Colors.transparent, showDragHandle: true, ), // ── Snackbar ──────────────────────────────────────────── snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(surfaces.compactCardRadius), + ), ), // ── Search Bar ────────────────────────────────────────── @@ -297,7 +389,9 @@ class AppTheme { elevation: WidgetStateProperty.all(0), backgroundColor: WidgetStateProperty.all(cs.surfaceContainerHigh), shape: WidgetStateProperty.all( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(surfaces.containerRadius), + ), ), ), @@ -305,7 +399,7 @@ class AppTheme { tooltipTheme: TooltipThemeData( decoration: BoxDecoration( color: cs.inverseSurface, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(surfaces.chipRadius), ), textStyle: textTheme.bodySmall?.copyWith(color: cs.onInverseSurface), ), @@ -323,8 +417,10 @@ class AppTheme { // ── PopupMenu ─────────────────────────────────────────── popupMenuTheme: PopupMenuThemeData( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - surfaceTintColor: cs.surfaceTint, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(surfaces.compactCardRadius), + ), + surfaceTintColor: Colors.transparent, color: isDark ? cs.surfaceContainerHigh : cs.surfaceContainerLowest, ), ); diff --git a/lib/widgets/face_verification_overlay.dart b/lib/widgets/face_verification_overlay.dart index b34d9253..5375c892 100644 --- a/lib/widgets/face_verification_overlay.dart +++ b/lib/widgets/face_verification_overlay.dart @@ -9,6 +9,7 @@ import '../providers/attendance_provider.dart'; import '../providers/profile_provider.dart'; import '../services/face_verification.dart' as face; import '../services/image_compress_service.dart'; +import '../theme/app_status_colors.dart'; import '../theme/m3_motion.dart'; import '../widgets/qr_verification_dialog.dart'; @@ -302,7 +303,7 @@ class _FaceVerificationOverlayState textAlign: TextAlign.center, style: textTheme.titleMedium?.copyWith( color: _phase == _Phase.success - ? Colors.green + ? AppStatusColors.of(context).success.color : _phase == _Phase.failed ? colors.error : _phase == _Phase.saving @@ -337,7 +338,7 @@ class _FaceVerificationOverlayState final Color ringColor; if (isSuccess) { - ringColor = Colors.green; + ringColor = AppStatusColors.of(context).success.color; } else if (isFailed) { ringColor = colors.error; } else if (isSaving) { @@ -461,7 +462,7 @@ class _FaceVerificationOverlayState Color dotColor; if (isSuccess_) { - dotColor = Colors.green; + dotColor = AppStatusColors.of(context).success.color; } else if (isDone) { dotColor = colors.error.withValues(alpha: 0.5); } else if (isCurrent) { diff --git a/lib/widgets/m3_card.dart b/lib/widgets/m3_card.dart index 76eac398..ca16d0e4 100644 --- a/lib/widgets/m3_card.dart +++ b/lib/widgets/m3_card.dart @@ -1,26 +1,27 @@ import 'package:flutter/material.dart'; -/// M3 Expressive Card variants. +import '../theme/app_surfaces.dart'; + +enum _M3CardVariant { elevated, filled, outlined } + +/// Cyber/IT Card variants (Material 3). /// /// Use these factory constructors to build semantically correct cards: -/// - [M3Card.elevated] — default, uses tonal surface tint (no hard shadow). -/// - [M3Card.filled] — uses surfaceContainerHighest for emphasis. -/// - [M3Card.outlined] — transparent fill with a subtle outline border. - +/// - [M3Card.elevated] — default panel: tonal fill + hairline outline (flat). +/// - [M3Card.filled] — high-emphasis fill (caller passes [color]); no border. +/// - [M3Card.outlined] — transparent fill with a hairline outline border. class M3Card extends StatelessWidget { - const M3Card._({ + const M3Card._( + this._variant, { required this.child, this.color, - this.elevation, - this.shadowColor, - this.surfaceTintColor, this.shape, this.margin, this.clipBehavior = Clip.none, this.onTap, }); - /// Elevated card — tonal surface tint, minimal shadow. + /// Elevated card — default panel with a tonal fill + hairline outline. /// Best for primary content surfaces (metric cards, detail panels). factory M3Card.elevated({ required Widget child, @@ -31,9 +32,8 @@ class M3Card extends StatelessWidget { VoidCallback? onTap, }) { return M3Card._( + _M3CardVariant.elevated, color: color, - elevation: 1, - shadowColor: Colors.transparent, shape: shape, margin: margin, clipBehavior: clipBehavior, @@ -42,8 +42,8 @@ class M3Card extends StatelessWidget { ); } - /// Filled card — uses surfaceContainerHighest for high emphasis. - /// Best for summary cards, status counts, callout panels. + /// Filled card — a high-emphasis fill (caller passes a semantic [color] or a + /// surfaceContainer tone). No border. Best for summary/callout panels. factory M3Card.filled({ required Widget child, Color? color, @@ -53,10 +53,8 @@ class M3Card extends StatelessWidget { VoidCallback? onTap, }) { return M3Card._( - color: color, // caller passes surfaceContainerHighest or semantic color - elevation: 0, - shadowColor: Colors.transparent, - surfaceTintColor: Colors.transparent, + _M3CardVariant.filled, + color: color, shape: shape, margin: margin, clipBehavior: clipBehavior, @@ -65,7 +63,7 @@ class M3Card extends StatelessWidget { ); } - /// Outlined card — transparent fill with outline border. + /// Outlined card — transparent fill with a hairline outline border. /// Best for list items, form sections, grouped content. factory M3Card.outlined({ required Widget child, @@ -76,10 +74,8 @@ class M3Card extends StatelessWidget { VoidCallback? onTap, }) { return M3Card._( + _M3CardVariant.outlined, color: color, - elevation: 0, - shadowColor: Colors.transparent, - surfaceTintColor: Colors.transparent, shape: shape, margin: margin, clipBehavior: clipBehavior, @@ -88,11 +84,9 @@ class M3Card extends StatelessWidget { ); } + final _M3CardVariant _variant; final Widget child; final Color? color; - final double? elevation; - final Color? shadowColor; - final Color? surfaceTintColor; final ShapeBorder? shape; final EdgeInsetsGeometry? margin; final Clip clipBehavior; @@ -101,36 +95,51 @@ class M3Card extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final radius = AppSurfaces.of(context).cardRadius; + final isDark = cs.brightness == Brightness.dark; + + final Color resolvedColor; + final bool bordered; + switch (_variant) { + case _M3CardVariant.elevated: + resolvedColor = + color ?? + (isDark ? cs.surfaceContainer : cs.surfaceContainerLowest); + bordered = true; + case _M3CardVariant.filled: + resolvedColor = color ?? cs.surfaceContainerHighest; + bordered = false; + case _M3CardVariant.outlined: + resolvedColor = color ?? Colors.transparent; + bordered = true; + } - // For outlined, we need the border side final resolvedShape = shape ?? - (elevation == 0 && surfaceTintColor == Colors.transparent - ? RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: (shadowColor == Colors.transparent && color == null) - ? BorderSide(color: cs.outlineVariant) - : BorderSide.none, - ) - : null); + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius), + side: bordered + ? BorderSide(color: cs.outlineVariant) + : BorderSide.none, + ); - final card = Card( - color: color, - elevation: elevation, - shadowColor: shadowColor, - surfaceTintColor: surfaceTintColor, + return Card( + color: resolvedColor, + elevation: 0, + shadowColor: Colors.transparent, + surfaceTintColor: Colors.transparent, shape: resolvedShape, margin: margin ?? EdgeInsets.zero, - clipBehavior: clipBehavior, + clipBehavior: onTap != null && clipBehavior == Clip.none + ? Clip.antiAlias + : clipBehavior, child: onTap != null ? InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(radius), child: child, ) : child, ); - - return card; } } diff --git a/lib/widgets/status_pill.dart b/lib/widgets/status_pill.dart index ebd6a65c..c6ad9473 100644 --- a/lib/widgets/status_pill.dart +++ b/lib/widgets/status_pill.dart @@ -1,32 +1,60 @@ import 'package:flutter/material.dart'; -/// M3 Expressive status pill — uses tonal container colors with a -/// smooth, spring-physics-inspired animation. +import '../theme/app_status_colors.dart'; +import '../theme/app_surfaces.dart'; + +/// Cyber/IT status pill — a compact, uppercase, mono-friendly badge whose color +/// carries meaning via [AppStatusColors]. +/// +/// Two constructors: +/// * [StatusPill.kind] (preferred) — pass a semantic [StatusKind] and it pulls +/// the correct tonal container/foreground from the theme, consistent in both +/// light and dark. +/// * The default constructor keeps the older `isEmphasized` behaviour for +/// back-compat (primaryContainer vs. tertiaryContainer). class StatusPill extends StatelessWidget { - const StatusPill({super.key, required this.label, this.isEmphasized = false}); + const StatusPill({super.key, required this.label, this.isEmphasized = false}) + : kind = null; + + /// Semantic variant — the recommended way to build a status pill. + const StatusPill.kind( + this.kind, { + super.key, + required this.label, + }) : isEmphasized = false; final String label; final bool isEmphasized; + final StatusKind? kind; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; - // isEmphasized uses primaryContainer (higher tonal weight) to visually - // distinguish high-priority statuses from routine ones. - final background = isEmphasized - ? scheme.primaryContainer - : scheme.tertiaryContainer; - final foreground = isEmphasized - ? scheme.onPrimaryContainer - : scheme.onTertiaryContainer; + final radius = AppSurfaces.of(context).chipRadius; + + late final Color background; + late final Color foreground; + if (kind != null) { + final swatch = AppStatusColors.of(context).byKind(kind!); + background = swatch.container; + foreground = swatch.onContainer; + } else { + // Legacy path: emphasis maps to primary vs. tertiary containers. + background = isEmphasized + ? scheme.primaryContainer + : scheme.tertiaryContainer; + foreground = isEmphasized + ? scheme.onPrimaryContainer + : scheme.onTertiaryContainer; + } return AnimatedContainer( duration: const Duration(milliseconds: 400), curve: Curves.easeOutCubic, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: background, - borderRadius: BorderRadius.circular(28), + borderRadius: BorderRadius.circular(radius), ), child: Text( label.toUpperCase(), diff --git a/lib/widgets/sync_pending_badge.dart b/lib/widgets/sync_pending_badge.dart index f4051ee6..c4f69e42 100644 --- a/lib/widgets/sync_pending_badge.dart +++ b/lib/widgets/sync_pending_badge.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/connectivity_provider.dart'; +import '../theme/app_status_colors.dart'; /// Two-state "this item is not yet synced" indicator. /// @@ -38,6 +39,7 @@ class SyncPendingBadge extends ConsumerWidget { final isOnline = ref.watch(isOnlineProvider); final scheme = Theme.of(context).colorScheme; + final status = AppStatusColors.of(context); final Color bg; final Color fg; @@ -53,9 +55,9 @@ class SyncPendingBadge extends ConsumerWidget { text = label ?? 'Syncing…'; spin = true; } else { - // Queued offline: amber, static. - bg = Colors.amber.shade100; - fg = Colors.amber.shade900; + // Queued offline: warning tone, static. + bg = status.warning.container; + fg = status.warning.onContainer; icon = Icons.sync; text = label ?? 'Pending sync'; spin = false; diff --git a/lib/widgets/tech_chip.dart b/lib/widgets/tech_chip.dart new file mode 100644 index 00000000..5ae5bb0e --- /dev/null +++ b/lib/widgets/tech_chip.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +import '../theme/app_status_colors.dart'; +import '../theme/app_surfaces.dart'; +import '../theme/app_typography.dart'; + +/// A compact, monospace "code" chip for technical identifiers — ticket/task +/// numbers, IP/MAC addresses, ports, VLAN ids, device codes, short timestamps. +/// +/// Reinforces the Cyber/IT identity by rendering machine data in Roboto Mono on +/// a subtle tonal surface with a hairline outline. Optionally tint it with a +/// semantic [StatusKind] for at-a-glance meaning. +class TechChip extends StatelessWidget { + const TechChip( + this.text, { + super.key, + this.icon, + this.status, + this.onTap, + this.dense = false, + }); + + final String text; + + /// Optional leading glyph (e.g. `Icons.tag`, `Icons.lan_outlined`). + final IconData? icon; + + /// When set, tints the chip with the matching [AppStatusColors] swatch. + final StatusKind? status; + + final VoidCallback? onTap; + + /// Tighter padding + smaller glyph for inline use within dense rows. + final bool dense; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final radius = AppSurfaces.of(context).chipRadius; + final swatch = status == null + ? null + : AppStatusColors.of(context).byKind(status!); + + final background = swatch?.container ?? cs.surfaceContainerHighest; + final foreground = swatch?.onContainer ?? cs.onSurfaceVariant; + final border = swatch == null ? cs.outlineVariant : Colors.transparent; + + final labelStyle = AppMonoText.of(context).label.copyWith( + color: foreground, + fontWeight: FontWeight.w600, + height: 1.0, + ); + + final content = Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon(icon, size: dense ? 12 : 14, color: foreground), + SizedBox(width: dense ? 4 : 6), + ], + Flexible( + child: Text( + text, + style: labelStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + + final chip = Container( + padding: EdgeInsets.symmetric( + horizontal: dense ? 6 : 8, + vertical: dense ? 2 : 4, + ), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(radius), + border: Border.all(color: border), + ), + child: content, + ); + + if (onTap == null) return chip; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(radius), + child: chip, + ); + } +}