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
+8 -78
View File
@@ -40,7 +40,9 @@ import 'package:skeletonizer/skeletonizer.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/app_metric_card.dart';
import '../../widgets/app_page_header.dart';
import '../../widgets/app_section_header.dart';
import '../../utils/app_time.dart';
class DashboardMetrics {
@@ -673,13 +675,13 @@ class _DashboardScreenState extends State<DashboardScreen> {
builder: (context, constraints) {
final sections = <Widget>[
const SizedBox(height: 16),
_sectionTitle(context, 'IT Staff Pulse'),
const AppSectionHeader('IT Staff Pulse'),
const _StaffTable(),
const SizedBox(height: 12),
_sectionTitle(context, 'Team Activity'),
const AppSectionHeader('Team Activity'),
const _StaffActivityChart(),
const SizedBox(height: 20),
_sectionTitle(context, 'Core Daily KPIs'),
const AppSectionHeader('Core Daily KPIs'),
_cardGrid(context, [
_MetricCard(
title: 'New tickets today',
@@ -695,7 +697,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'Task Flow'),
const AppSectionHeader('Task Flow'),
_cardGrid(context, [
_MetricCard(
title: 'Tasks created',
@@ -713,7 +715,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'TAT / Response'),
const AppSectionHeader('TAT / Response'),
_cardGrid(context, [
_MetricCard(
title: 'Avg response',
@@ -776,33 +778,6 @@ class _DashboardScreenState extends State<DashboardScreen> {
);
}
Widget _sectionTitle(BuildContext context, String title) {
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
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,
),
),
],
),
);
}
Widget _cardGrid(BuildContext context, List<Widget> cards) {
return LayoutBuilder(
@@ -921,7 +896,6 @@ class _MetricCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cs = Theme.of(context).colorScheme;
// Only watch the single string value for this card so unrelated metric
// updates don't rebuild the whole card. This makes updates feel much
// smoother and avoids full-page refreshes.
@@ -934,51 +908,7 @@ 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: 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.toUpperCase(),
style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.6,
color: cs.onSurfaceVariant,
),
),
const SizedBox(height: 12),
// Animate only the metric text (not the whole card) for a
// subtle, smooth update.
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (child, anim) =>
FadeTransition(opacity: anim, child: child),
child: MonoText(
value,
key: ValueKey(value),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: cs.onSurface,
),
),
),
],
),
);
return AppMetricCard(title: title, value: value);
}
}