import 'package:flutter/material.dart'; import 'panel_header.dart'; /// A navigation item in the NavPanel. class NavItem { const NavItem({ required this.id, required this.label, required this.icon, this.section, this.badge, }); /// Unique identifier for routing. final String id; /// Display label. final String label; /// Item icon. final IconData icon; /// Optional section grouping (e.g., 'Portainer', 'NPM', 'Authentik'). /// Section headers are shown only when multiple sections exist. final String? section; /// Optional badge (e.g., item count). final String? badge; } /// Left-side navigation panel for room-level section navigation. /// /// Shows a list of items within the current room (e.g., Containers, Networks /// for Control Room). Items can be grouped into sections with headers. /// Section headers only appear when multiple sections exist. /// /// Header content is docked to bottom to accommodate the logo bulge overlay. /// /// See UI_LAYOUT.md for panel taxonomy and layout specifications. class NavPanel extends StatelessWidget { const NavPanel({ super.key, required this.title, required this.icon, required this.items, required this.selectedId, required this.onItemSelected, this.width = 280, this.trailing, }); /// Panel title displayed in header. final String title; /// Leading icon for the header. final IconData icon; /// List of navigation items. final List items; /// Currently selected item ID. final String selectedId; /// Callback when an item is tapped. final ValueChanged onItemSelected; /// Panel width (default 280px per UI_LAYOUT.md spec). final double width; /// Optional trailing widget below items (e.g., external links). final Widget? trailing; /// Returns true if section headers should be shown. bool get _showSectionHeaders { final sections = items.map((i) => i.section).where((s) => s != null).toSet(); return sections.length > 1; } /// Groups items by section, preserving order. List<(String?, List)> get _groupedItems { final groups = >{}; final order = []; for (final item in items) { if (!groups.containsKey(item.section)) { groups[item.section] = []; order.add(item.section); } groups[item.section]!.add(item); } return order.map((section) => (section, groups[section]!)).toList(); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return SizedBox( width: width, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ PanelHeader( title: title, icon: icon, dockToBottom: true, // Left-side panel ), Expanded( child: ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: _buildItemList(context), ), ), if (trailing != null) ...[ Divider(height: 1, color: colorScheme.outlineVariant), trailing!, ], ], ), ); } List _buildItemList(BuildContext context) { final widgets = []; final showHeaders = _showSectionHeaders; for (final (section, sectionItems) in _groupedItems) { // Add section header if multiple sections exist if (showHeaders && section != null) { widgets.add(_SectionHeader(title: section)); } // Add items for (final item in sectionItems) { widgets.add( _NavTile( item: item, isSelected: item.id == selectedId, onTap: () => onItemSelected(item.id), ), ); } } return widgets; } } /// Section header widget with left accent bar. class _SectionHeader extends StatelessWidget { const _SectionHeader({required this.title}); final String title; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Container( margin: const EdgeInsets.only(top: 8, bottom: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: colorScheme.surfaceContainerHigh, border: Border( left: BorderSide( color: colorScheme.primary, width: 2, ), ), ), child: Text( title.toUpperCase(), style: textTheme.labelSmall?.copyWith( color: colorScheme.onSurfaceVariant, letterSpacing: 0.5, fontWeight: FontWeight.w600, ), ), ); } } class _NavTile extends StatelessWidget { const _NavTile({ required this.item, required this.isSelected, required this.onTap, }); final NavItem item; final bool isSelected; final VoidCallback onTap; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), child: Material( color: isSelected ? colorScheme.primaryContainer.withValues(alpha: 0.4) : Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ Icon( item.icon, size: 20, color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, ), const SizedBox(width: 12), Expanded( child: Text( item.label, style: textTheme.bodyMedium?.copyWith( color: isSelected ? colorScheme.primary : colorScheme.onSurface, fontWeight: isSelected ? FontWeight.w600 : null, ), ), ), if (item.badge != null) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: isSelected ? colorScheme.primary.withValues(alpha: 0.2) : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Text( item.badge!, style: textTheme.labelSmall?.copyWith( color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, ), ), ), ], ), ), ), ), ); } }