import 'package:flutter/material.dart'; import '../../../models/network/network_device.dart'; /// A styled node representing one network device on the topology canvas. /// /// Designed to feel solid at ~120-160dp wide so it remains legible at the /// "good for 200 nodes" zoom level called out in the V1 spec. /// /// The node has three visual states beyond its default rest: /// - [isSelected]: stronger primary-tinted background + 2dp primary border. /// - [isHighlighted]: softer secondary tint + primary outline, used when an /// edge touching this device is hovered on the canvas. /// - [isSelected] takes precedence over [isHighlighted]. class DeviceNode extends StatelessWidget { const DeviceNode({ super.key, required this.device, this.portCount, this.isSelected = false, this.isHighlighted = false, this.onTap, }); final NetworkDevice device; final int? portCount; final bool isSelected; final bool isHighlighted; final VoidCallback? onTap; IconData _iconForKind(NetworkDeviceKind kind) { switch (kind) { case NetworkDeviceKind.router: return Icons.router_outlined; case NetworkDeviceKind.switchDevice: return Icons.hub_outlined; case NetworkDeviceKind.ap: return Icons.wifi_outlined; case NetworkDeviceKind.firewall: return Icons.security_outlined; case NetworkDeviceKind.server: return Icons.dns_outlined; case NetworkDeviceKind.endpoint: return Icons.devices_outlined; case NetworkDeviceKind.patchPanel: return Icons.view_module_outlined; case NetworkDeviceKind.other: return Icons.device_unknown_outlined; } } 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; } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final tt = Theme.of(context).textTheme; final accent = _accentForRole(cs, device.role); 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: Container( width: 160, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Icon(_iconForKind(device.kind), size: 18, color: accent), 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, ), ), ], ), ], ], ), ), ), ), ); } }