import 'package:flutter/material.dart'; import '../../../models/network/network_device.dart'; import 'network_device_icon.dart'; /// A styled node representing one network device on the topology canvas. /// /// Accepts an explicit [nodeSize] so role-based sizing can be applied by the /// canvas without this widget needing to know the layout strategy. class DeviceNode extends StatelessWidget { const DeviceNode({ super.key, required this.device, this.portCount, this.isSelected = false, this.isHighlighted = false, this.nodeSize = const Size(160, 110), this.onTap, }); final NetworkDevice device; final int? portCount; final bool isSelected; final bool isHighlighted; final Size nodeSize; final VoidCallback? onTap; Color _accentForRole(ColorScheme cs, NetworkDeviceRole? role) { switch (role) { case NetworkDeviceRole.core: return cs.error; case NetworkDeviceRole.distribution: return cs.tertiary; case NetworkDeviceRole.access: return cs.primary; case NetworkDeviceRole.edge: return cs.secondary; case NetworkDeviceRole.endpoint: case null: return cs.outline; } } Color? _statusColor(ColorScheme cs, NetworkDeviceStatus status) { switch (status) { case NetworkDeviceStatus.online: return const Color(0xFF34A853); // Google green — universally understood case NetworkDeviceStatus.offline: return cs.error; case NetworkDeviceStatus.warning: return const Color(0xFFFBBC04); // Amber case NetworkDeviceStatus.unknown: return null; // No dot shown } } @override Widget build(BuildContext context) { 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 bgColor = isSelected ? cs.primaryContainer : isHighlighted ? cs.secondaryContainer.withValues(alpha: 0.6) : cs.surfaceContainerHigh; final borderColor = isSelected ? cs.primary : isHighlighted ? cs.primary.withValues(alpha: 0.8) : accent.withValues(alpha: 0.6); final borderWidth = isSelected ? 2.0 : (isHighlighted ? 1.5 : 1.0); return AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.easeOut, decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(16), border: Border.all(color: borderColor, width: borderWidth), boxShadow: (isHighlighted || isSelected) ? [ BoxShadow( color: cs.primary.withValues(alpha: 0.18), blurRadius: 12, offset: const Offset(0, 2), ), ] : null, ), child: Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Stack( children: [ Container( width: nodeSize.width, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ NetworkDeviceIcon( kind: device.kind, color: accent, size: 20, ), const SizedBox(width: 6), Expanded( child: Text( device.kind.label, style: tt.labelSmall?.copyWith(color: accent), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 4), Text( device.name, style: tt.bodyMedium?.copyWith(fontWeight: FontWeight.w600), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (device.vendor != null || device.model != null) ...[ const SizedBox(height: 2), Text( [ if (device.vendor != null) device.vendor, if (device.model != null) device.model, ].whereType().join(' • '), style: tt.labelSmall?.copyWith( color: cs.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], if (portCount != null) ...[ const SizedBox(height: 4), Row( children: [ Icon( Icons.cable_outlined, size: 12, color: cs.onSurfaceVariant, ), const SizedBox(width: 4), Text( '$portCount ports', style: tt.labelSmall?.copyWith( color: cs.onSurfaceVariant, ), ), ], ), ], ], ), ), // Status dot — top-right, only shown when status is not unknown if (statusColor != null) Positioned( top: 8, right: 8, child: Container( width: 8, height: 8, decoration: BoxDecoration( color: statusColor, shape: BoxShape.circle, border: Border.all( color: bgColor, width: 1.5, ), ), ), ), ], ), ), ), ); } }