Files
tasq/lib/widgets/app_status_summary.dart
redz1029 9f764150a2 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>
2026-09-26 15:42:26 +08:00

127 lines
3.6 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/app_status_colors.dart';
import '../theme/app_surfaces.dart';
/// One entry in an [AppStatusSummaryRow].
class AppStatusSummaryItem {
const AppStatusSummaryItem({
required this.label,
required this.count,
required this.kind,
this.selected = false,
this.onTap,
});
final String label;
final int count;
final StatusKind kind;
/// Emphasized (filled) when true — used for the active filter.
final bool selected;
/// When set, the chip is tappable (e.g. to filter the list by this status).
final VoidCallback? onTap;
}
/// A horizontally-scrollable row of status-count chips — the single shared
/// "count-by-status" summary used above entity lists (unifies the separate
/// tickets / tasks / ISR summary-chip implementations).
class AppStatusSummaryRow extends StatelessWidget {
const AppStatusSummaryRow({
super.key,
required this.items,
this.padding = EdgeInsets.zero,
});
final List<AppStatusSummaryItem> items;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
if (items.isEmpty) return const SizedBox.shrink();
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: padding,
child: Row(
children: [
for (var i = 0; i < items.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
AppStatusSummaryChip(item: items[i]),
],
],
),
);
}
}
/// A single status-count chip: a status dot + bold count + label. Filled with
/// the status tone when [AppStatusSummaryItem.selected], subtle otherwise.
class AppStatusSummaryChip extends StatelessWidget {
const AppStatusSummaryChip({super.key, required this.item});
final AppStatusSummaryItem item;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
final swatch = AppStatusColors.of(context).byKind(item.kind);
final radius = AppSurfaces.of(context).chipRadius;
final bg = item.selected ? swatch.container : cs.surfaceContainerHighest;
final labelColor =
item.selected ? swatch.onContainer : cs.onSurfaceVariant;
final countColor = item.selected ? swatch.onContainer : cs.onSurface;
final chip = AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: item.selected ? Colors.transparent : cs.outlineVariant,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: swatch.color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
item.count.toString(),
style: tt.labelLarge?.copyWith(
color: countColor,
fontWeight: FontWeight.w700,
),
),
const SizedBox(width: 6),
Text(
item.label.toUpperCase(),
style: tt.labelSmall?.copyWith(
color: labelColor,
fontWeight: FontWeight.w600,
letterSpacing: 0.4,
),
),
],
),
);
if (item.onTap == null) return chip;
return InkWell(
onTap: item.onTap,
borderRadius: BorderRadius.circular(radius),
child: chip,
);
}
}