import 'package:flutter/material.dart'; import '../theme/app_surfaces.dart'; import 'mono_text.dart'; /// Cyber/IT metric / stat card — an uppercase telemetry label above a large /// monospace value, on a flat tonal surface with a hairline outline. /// /// The single shared style for KPI/stat tiles (unifies the Dashboard KPI card, /// the Network Map "Sites/Devices" cards, and the Reports conversion tile). /// Purely presentational: callers pass already-resolved [title]/[value] strings. class AppMetricCard extends StatelessWidget { const AppMetricCard({ super.key, required this.title, required this.value, this.icon, this.onTap, this.trailing, this.animateValue = true, }); final String title; final String value; /// Optional leading accent glyph. final IconData? icon; final VoidCallback? onTap; /// Optional trailing widget (e.g. a chevron for drill-down cards). final Widget? trailing; /// Cross-fade the value when it changes (nice for live-updating metrics). final bool animateValue; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final tt = Theme.of(context).textTheme; final isDark = cs.brightness == Brightness.dark; final radius = AppSurfaces.of(context).cardRadius; final valueWidget = MonoText( value, key: ValueKey(value), style: tt.headlineSmall?.copyWith( fontWeight: FontWeight.w700, color: cs.onSurface, ), maxLines: 1, overflow: TextOverflow.ellipsis, ); final content = Padding( padding: const EdgeInsets.all(20), child: Row( children: [ if (icon != null) ...[ Icon(icon, size: 22, color: cs.primary), const SizedBox(width: 14), ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( title.toUpperCase(), style: tt.labelMedium?.copyWith( fontWeight: FontWeight.w600, letterSpacing: 0.6, color: cs.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 10), if (animateValue) AnimatedSwitcher( duration: const Duration(milliseconds: 400), switchInCurve: Curves.easeOutCubic, switchOutCurve: Curves.easeInCubic, transitionBuilder: (child, anim) => FadeTransition(opacity: anim, child: child), child: valueWidget, ) else valueWidget, ], ), ), if (trailing != null) ...[const SizedBox(width: 8), trailing!], ], ), ); final decorated = AnimatedContainer( duration: const Duration(milliseconds: 400), curve: Curves.easeOutCubic, decoration: BoxDecoration( color: isDark ? cs.surfaceContainer : cs.surfaceContainerLowest, borderRadius: BorderRadius.circular(radius), border: Border.all(color: cs.outlineVariant), ), child: content, ); if (onTap == null) return decorated; return Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(radius), child: decorated, ), ); } }