UI Phase 2 (L0): shared layout primitives + chrome standardization

Extract four shared layout widgets from the best existing implementations and
adopt them across list/overview screens, deleting duplicated inline code.
Layout/consistency only — no behavior change.

New primitives (lib/widgets/):
- app_section_header.dart  — AppSectionHeader (azure accent bar + uppercase
  telemetry label; promoted from the dashboard's _sectionTitle).
- app_metric_card.dart     — AppMetricCard (uppercase label + mono value on a
  flat hairline surface; unifies KPI/stat tiles).
- app_status_summary.dart  — AppStatusSummaryRow/Item/Chip (dot + count + label,
  selectable; semantic AppStatusColors).
- app_filter_bar.dart      — AppFilterBar (responsive filter-row chrome).

Adopted / de-duplicated:
- Dashboard: _MetricCard now wraps AppMetricCard; section titles use
  AppSectionHeader (removed local _sectionTitle).
- Tickets / Tasks / ISR: three separate summary-chip implementations
  (_StatusSummaryCard x2, _SummaryChip) collapsed into AppStatusSummaryRow.
- Network Map overview: Sites/Devices tiles -> AppMetricCard (removed local
  _SummaryCard).
- Workforce: "Duty Schedules" header -> AppSectionHeader with trailing chip.

Verified: flutter analyze lib clean; flutter build web green; visual QA (tickets
+ ISR summary chips) in dark.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-26 15:42:26 +08:00
parent 57377b1b21
commit 9f764150a2
10 changed files with 428 additions and 325 deletions
@@ -18,6 +18,7 @@ import '../../theme/m3_motion.dart';
import '../../utils/app_time.dart';
import '../../utils/snackbar.dart';
import '../../theme/app_status_colors.dart';
import '../../widgets/app_status_summary.dart';
import '../../widgets/m3_card.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/reconnect_overlay.dart';
@@ -535,8 +536,6 @@ 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;
@@ -554,88 +553,38 @@ class _StatusSummaryRow extends StatelessWidget {
.where((r) => r.status == ItServiceRequestStatus.completed)
.length;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
_SummaryChip(
label: 'Pending',
count: pending,
color: status.warning.color,
selected: selectedStatus == ItServiceRequestStatus.pendingApproval,
onTap: () => onStatusTap(ItServiceRequestStatus.pendingApproval),
),
const SizedBox(width: 8),
_SummaryChip(
label: 'Scheduled',
count: scheduled,
color: status.info.color,
selected: selectedStatus == ItServiceRequestStatus.scheduled,
onTap: () => onStatusTap(ItServiceRequestStatus.scheduled),
),
const SizedBox(width: 8),
_SummaryChip(
label: 'In Progress',
count: inProgress,
color: cs.secondary,
selected: selectedStatus == ItServiceRequestStatus.inProgress,
onTap: () => onStatusTap(ItServiceRequestStatus.inProgress),
),
const SizedBox(width: 8),
_SummaryChip(
label: 'Completed',
count: completed,
color: status.success.color,
selected: selectedStatus == ItServiceRequestStatus.completed,
onTap: () => onStatusTap(ItServiceRequestStatus.completed),
),
],
),
);
}
}
class _SummaryChip extends StatelessWidget {
const _SummaryChip({
required this.label,
required this.count,
required this.color,
required this.selected,
required this.onTap,
});
final String label;
final int count;
final Color color;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return M3Card.filled(
onTap: onTap,
color: selected
? color.withValues(alpha: 0.2)
: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
count.toString(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(width: 8),
Text(label, style: Theme.of(context).textTheme.bodySmall),
],
return AppStatusSummaryRow(
padding: const EdgeInsets.symmetric(vertical: 8),
items: [
AppStatusSummaryItem(
label: 'Pending',
count: pending,
kind: StatusKind.warning,
selected: selectedStatus == ItServiceRequestStatus.pendingApproval,
onTap: () => onStatusTap(ItServiceRequestStatus.pendingApproval),
),
),
AppStatusSummaryItem(
label: 'Scheduled',
count: scheduled,
kind: StatusKind.neutral,
selected: selectedStatus == ItServiceRequestStatus.scheduled,
onTap: () => onStatusTap(ItServiceRequestStatus.scheduled),
),
AppStatusSummaryItem(
label: 'In Progress',
count: inProgress,
kind: StatusKind.info,
selected: selectedStatus == ItServiceRequestStatus.inProgress,
onTap: () => onStatusTap(ItServiceRequestStatus.inProgress),
),
AppStatusSummaryItem(
label: 'Completed',
count: completed,
kind: StatusKind.success,
selected: selectedStatus == ItServiceRequestStatus.completed,
onTap: () => onStatusTap(ItServiceRequestStatus.completed),
),
],
);
}
}