diff --git a/docs/UI_LAYOUT.md b/docs/UI_LAYOUT.md index a9a7887..93aa0b5 100644 --- a/docs/UI_LAYOUT.md +++ b/docs/UI_LAYOUT.md @@ -22,36 +22,107 @@ The UI uses a **tabbed room navigation** in the header rather than a traditional ``` ┌─────────────────────────────────────────────────────────────────────────────┐ -│ HEADER (intrinsic height) │ -│ [Logo] [═══════ Room Tabs (scrollable) ═══════] [Notifications] [Profile] │ +│ HEADER BAR (56px, with logo bulge overlay) │ +│ [◯Logo◯] [Room Icons] ─────────────────────────────────── [Profile Menu] │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────┐ ┌───────────────────┐ │ -│ │ │ │ │ │ -│ │ MAIN CONTENT │ │ CHAT DOCK │ │ -│ │ flex: 1 │ │ flex: 0 │ │ -│ │ │ │ │ │ -│ │ ┌─────────────┐ ┌────────────────────────────┐ │ │ Tatlock chat │ │ -│ │ │ CONTEXT │ │ PRIMARY │ │ │ assistant, │ │ -│ │ │ SIDEBAR │ │ CONTENT │ │ │ persistent │ │ -│ │ │ │ │ │ │ │ across rooms │ │ -│ │ │ flex: 0 │ │ flex: 1 │ │ │ │ │ -│ │ │ intrinsic │ │ │ │ │ │ │ -│ │ └─────────────┘ └────────────────────────────┘ │ │ │ │ -│ │ │ │ │ │ -│ └─────────────────────────────────────────────────┘ └───────────────────┘ │ +│ ┌─────────────┐ ┌─────────────────────────────────┐ ┌───────────────────┐ │ +│ │ NAV PANEL │ │ PRIMARY CONTENT │ │ CHAT DOCK │ │ +│ │ │ │ │ │ │ │ +│ │ Room-level │ │ ┌──────────┐ ┌──────────────┐ │ │ Tatlock AI │ │ +│ │ navigation │ │ │ FILTER │ │ DATA GRID │ │ │ assistant │ │ +│ │ (sections) │ │ │ PANEL │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ │ Persistent │ │ +│ │ 280px fixed │ │ │ Optional │ │ flex: 1 │ │ │ across rooms │ │ +│ │ │ │ │ 280px │ │ │ │ │ │ │ +│ └─────────────┘ │ └──────────┘ └──────────────┘ │ │ 280px-33vw │ │ +│ │ │ │ │ │ +│ │ flex: 1 │ │ flex: 0 │ │ +│ └─────────────────────────────────┘ └───────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` -### Component Roles +--- + +## Panel Taxonomy + +All panels share common styling patterns but serve different purposes. + +### Panel Types + +| Panel | Position | Purpose | Width | Content Alignment | +|-------|----------|---------|-------|-------------------| +| **Header Bar** | Top | Room nav, profile | 56px height | Logo in bulge, rest at bottom | +| **Nav Panel** | Left | Room-level section nav | 280px fixed | Header docked to bottom | +| **Filter Panel** | Left (inside content) | Data filtering/search | 280px fixed | Header docked to bottom | +| **Detail Panel** | Right (inside content) | Selected item details | 320-400px | Standard header | +| **Chat Dock** | Right | AI assistant | 280px-33vw | Standard header | + +### Panel Header Behavior + +All left-side panels (Nav Panel, Filter Panel) dock their header content to the **bottom** of the header area. This accommodates the logo bulge that overlays into their space. + +``` +┌─────────────────────────┐ +│ │ ← Logo bulge overlays this space +│ (empty) │ +│ │ +│ [Icon] Title [Action]│ ← Content docked to bottom (8px margin) +├─────────────────────────┤ +│ Panel content... │ +``` + +Right-side panels (Detail Panel, Chat Dock) use standard vertically-centered headers since the logo bulge doesn't reach them. + +--- + +## Panel Configurations by Room + +### Front Hall +``` +┌────────────────────────────────────────────────────────────────┬────────────┐ +│ PRIMARY CONTENT (no nav panel) │ CHAT DOCK │ +│ Dashboard cards, activity feed │ (expanded) │ +└────────────────────────────────────────────────────────────────┴────────────┘ +``` + +### Control Room +``` +┌───────────┬──────────────────────────────────────────────────────┬──────────┐ +│ NAV PANEL │ PRIMARY CONTENT │ CHAT │ +│ │ ┌────────────┬────────────────────────────────────┐ │ DOCK │ +│ Sections: │ │ FILTER │ DATA GRID │ │ │ +│ • Contai. │ │ PANEL │ Container/Stack list │ │ (collap- │ +│ • Stacks │ │ │ │ │ sed) │ +│ • Network │ │ Stack list │ │ │ │ +│ • Volumes │ │ + search │ │ │ │ +└───────────┴──┴────────────┴────────────────────────────────────┴─┴──────────┘ +``` + +### Parlor +``` +┌───────────┬────────────────────────────────────────────────────┬────────────┐ +│ NAV PANEL │ PRIMARY CONTENT │ CHAT DOCK │ +│ │ Device controls, scenes │ (collapsed)│ +│ Areas: │ │ │ +│ • Living │ │ │ +│ • Bedroom │ │ │ +│ • Kitchen │ │ │ +└───────────┴────────────────────────────────────────────────────┴────────────┘ +``` + +--- + +## Component Roles | Component | Flex | Description | |-----------|------|-------------| -| **Header** | intrinsic | Logo, room tabs, action icons | -| **Main Content** | `flex: 1` | Fills remaining horizontal space | -| **Context Sidebar** | `flex: 0`, intrinsic | Room-specific navigation (Control Room, Parlor) | -| **Primary Content** | `flex: 1` | Room's main working area | +| **Header Bar** | intrinsic (56px) | Logo bulge, room icons, profile menu | +| **Nav Panel** | `flex: 0`, 280px | Room-level section navigation | +| **Filter Panel** | `flex: 0`, 280px | Data filtering within a section | +| **Primary Content** | `flex: 1` | Main working area | +| **Detail Panel** | `flex: 0`, 320-400px | Selected item details (optional) | | **Chat Dock** | `flex: 0`, intrinsic | Tatlock assistant, collapsible | --- @@ -61,55 +132,76 @@ The UI uses a **tabbed room navigation** in the header rather than a traditional ### Wide (>= 1200px) ``` -┌────────────────────────────────────────────────────────┬────────────────────┐ -│ [Sidebar] [══════════ Content ══════════] │ Chat (expanded) │ -└────────────────────────────────────────────────────────┴────────────────────┘ +┌───────────┬─────────────────────────────────────────────┬────────────────────┐ +│ NAV PANEL │ [Filter Panel] [═══ Data Grid ═══] │ CHAT DOCK │ +│ (280px) │ (280px) (flex: 1) │ (expanded, 320px) │ +└───────────┴─────────────────────────────────────────────┴────────────────────┘ ``` +- All panels visible - Chat dock expanded by default on Front Hall -- Context sidebar visible with labels +- Nav panel visible with full labels +- Filter panel visible (where applicable) - Full data grid columns ### Medium (>= 800px, < 1200px) ``` -┌────────────────────────────────────────────────────────────────────────┬────┐ -│ [Sidebar] [══════════════════ Content ══════════════════] │ 💬 │ -└────────────────────────────────────────────────────────────────────────┴────┘ +┌───────────┬───────────────────────────────────────────────────────────┬────┐ +│ NAV PANEL │ [Filter Panel] [═══════════ Data Grid ═══════════] │ 💬 │ +│ (280px) │ (280px) (flex: 1) │48px│ +└───────────┴───────────────────────────────────────────────────────────┴────┘ ``` -- Chat dock collapsed to icon rail -- Click to expand as overlay -- Context sidebar still visible +- Chat dock collapsed to icon rail (48px) +- Click chat icon to expand as overlay +- Nav panel still visible +- Filter panel still visible +- Data grid may hide some columns ### Compact (>= 600px, < 800px) ``` -┌────────────────────────────────────────────────────────────────────────┬────┐ -│ [≡] [════════════════════ Content ════════════════════] │ 💬 │ -└────────────────────────────────────────────────────────────────────────┴────┘ +┌────┬───────────────────────────────────────────────────────────────────┬────┐ +│ ≡ │ [═══════════════════════ Content ═══════════════════════] │ 💬 │ +│48px│ Filter panel becomes top bar or collapsible │48px│ +└────┴───────────────────────────────────────────────────────────────────┴────┘ ``` -- Context sidebar becomes drawer (hamburger menu) -- Chat dock collapsed -- Reduced data grid columns +- Nav panel collapses to icon rail (48px), expands as drawer on tap +- Filter panel moves to top of content or becomes collapsible +- Chat dock remains collapsed (48px) +- Data grid shows essential columns only ### Mobile (< 600px) ``` ┌────────────────────────────────────────────────────────────────────────┐ -│ [≡] [Tabs scroll horizontally] [💬] │ +│ HEADER: [≡] [Room Icons scroll] ─────────────────────────────── [💬] │ ├────────────────────────────────────────────────────────────────────────┤ │ │ -│ [══════════════════════ Content ══════════════════════] │ +│ [═══════════════════════ Content ══════════════════════] │ +│ Filter as expandable section at top │ │ │ └────────────────────────────────────────────────────────────────────────┘ ``` - Single column layout -- Room tabs scroll horizontally -- Chat opens as bottom sheet -- Context sidebar as full-screen drawer +- Nav panel opens as full-screen drawer (hamburger menu) +- Filter panel becomes expandable section at top of content +- Chat opens as bottom sheet (60vh max) +- Data grid becomes card list or single-column table + +--- + +## Panel Folding Summary + +| Panel | Wide (≥1200) | Medium (≥800) | Compact (≥600) | Mobile (<600) | +|-------|--------------|---------------|----------------|---------------| +| **Nav Panel** | 280px visible | 280px visible | 48px rail → drawer | Hidden → drawer | +| **Filter Panel** | 280px visible | 280px visible | Collapsed/top bar | Expandable section | +| **Chat Dock** | 320px expanded | 48px rail → overlay | 48px rail → overlay | Icon → bottom sheet | +| **Detail Panel** | 320-400px slide-in | 320px slide-in | Full-width overlay | Full-screen | --- @@ -275,6 +367,42 @@ The Tatlock chat assistant is **omnipresent** - accessible from any room. ## Flutter Implementation Notes +### Panel Widget Library + +All panels are built from shared components in `lib/shared/layouts/widgets/`: + +| Widget | File | Purpose | +|--------|------|---------| +| `PanelContainer` | `panel_container.dart` | Base container for all panels | +| `PanelHeader` | `panel_header.dart` | Configurable header (bottom-docked or centered) | +| `NavPanel` | `nav_panel.dart` | Left-side room navigation | +| `FilterPanel` | `filter_panel.dart` | Left-side data filtering | +| `DetailPanel` | `detail_panel.dart` | Right-side item details | +| `ChatDock` | `chat_dock.dart` | Right-side AI assistant | + +### PanelHeader Configuration + +```dart +/// Panel header with configurable content alignment. +class PanelHeader extends StatelessWidget { + const PanelHeader({ + required this.title, + required this.icon, + this.actions, + this.dockToBottom = false, // true for left-side panels + }); +} +``` + +**Left-side panels** (Nav, Filter) use `dockToBottom: true` to accommodate the logo bulge: +- Header height: 56px (matches app header) +- Content aligned to bottom with 8px margin +- Empty space at top allows logo bulge overlay + +**Right-side panels** (Detail, Chat) use `dockToBottom: false`: +- Standard vertically-centered content +- No accommodation needed for logo bulge + ### Recommended Widgets | Concept | Flutter Widget | @@ -296,6 +424,19 @@ abstract class Breakpoints { } ``` +### Panel Width Constants + +```dart +abstract class PanelWidths { + static const double navPanel = 280; + static const double filterPanel = 280; + static const double detailPanel = 360; + static const double chatDockExpanded = 320; + static const double chatDockCollapsed = 48; + static const double railWidth = 48; +} +``` + ### Layout Builder Pattern ```dart diff --git a/lib/features/control_room/presentation/pages/control_room_page.dart b/lib/features/control_room/presentation/pages/control_room_page.dart index 6c3f9df..0cb41c6 100644 --- a/lib/features/control_room/presentation/pages/control_room_page.dart +++ b/lib/features/control_room/presentation/pages/control_room_page.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart' hide Stack; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../../../../core/config/app_config.dart'; -import '../../containers/presentation/pages/containers_list_page.dart'; -import '../../stacks/data/repositories/stack_repository_impl.dart'; -import '../../stacks/domain/entities/stack.dart'; -import '../../stacks/presentation/pages/stack_detail_page.dart'; -import '../../stacks/presentation/providers/stacks_provider.dart'; -import '../../stacks/presentation/widgets/stack_list_tile.dart'; +import 'package:tatlock_ui/features/control_room/containers/presentation/pages/containers_list_page.dart'; +import 'package:tatlock_ui/features/control_room/stacks/data/repositories/stack_repository_impl.dart'; +import 'package:tatlock_ui/features/control_room/stacks/domain/entities/stack.dart'; +import 'package:tatlock_ui/features/control_room/stacks/presentation/pages/stack_detail_page.dart'; +import 'package:tatlock_ui/features/control_room/stacks/presentation/providers/stacks_provider.dart'; +import 'package:tatlock_ui/features/control_room/stacks/presentation/widgets/stack_list_tile.dart'; +import 'package:tatlock_ui/shared/layouts/widgets/filter_panel.dart'; /// Main Control Room page with stacks sidebar and containers view. class ControlRoomPage extends ConsumerWidget { @@ -41,7 +39,7 @@ class ControlRoomPage extends ConsumerWidget { } } -/// Sidebar showing stacks list. +/// Sidebar showing stacks list (Filter Panel per UI_LAYOUT.md). class _StacksSidebar extends ConsumerWidget { const _StacksSidebar(); @@ -51,46 +49,18 @@ class _StacksSidebar extends ConsumerWidget { final selectedStack = ref.watch(selectedStackProvider); final colorScheme = Theme.of(context).colorScheme; - return SizedBox( - width: 280, + return FilterPanel( + title: 'Stacks', + icon: Icons.layers, + onRefresh: () => ref.invalidate(stacksProvider), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - border: Border( - bottom: BorderSide(color: colorScheme.outlineVariant), - ), - ), - child: Row( - children: [ - Icon( - Icons.layers, - color: colorScheme.primary, - ), - const SizedBox(width: 12), - Text( - 'Stacks', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - const Spacer(), - IconButton( - icon: const Icon(Icons.refresh, size: 20), - tooltip: 'Refresh stacks', - onPressed: () => ref.invalidate(stacksProvider), - ), - ], - ), - ), // All containers option ListTile( selected: selectedStack == null, - selectedTileColor: colorScheme.primaryContainer.withValues(alpha: 0.3), + selectedTileColor: + colorScheme.primaryContainer.withValues(alpha: 0.3), leading: Icon( Icons.all_inbox, color: selectedStack == null @@ -136,44 +106,6 @@ class _StacksSidebar extends ConsumerWidget { ), ), ), - // External links - const Divider(height: 1), - Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'External', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: colorScheme.outline, - ), - ), - const SizedBox(height: 4), - Wrap( - spacing: 4, - children: [ - _ExternalLinkChip( - label: 'Portainer', - icon: Icons.dashboard, - onTap: () => launchUrl( - Uri.parse(AppConfig.portainerUrl), - mode: LaunchMode.externalApplication, - ), - ), - _ExternalLinkChip( - label: 'Netdata', - icon: Icons.analytics, - onTap: () => launchUrl( - Uri.parse(AppConfig.netdataUrl), - mode: LaunchMode.externalApplication, - ), - ), - ], - ), - ], - ), - ), ], ), ); @@ -358,23 +290,3 @@ class _StacksListState extends State<_StacksList> { } } -class _ExternalLinkChip extends StatelessWidget { - const _ExternalLinkChip({ - required this.label, - required this.icon, - required this.onTap, - }); - - final String label; - final IconData icon; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return ActionChip( - avatar: Icon(icon, size: 16), - label: Text(label), - onPressed: onTap, - ); - } -} diff --git a/lib/shared/layouts/widgets/filter_panel.dart b/lib/shared/layouts/widgets/filter_panel.dart new file mode 100644 index 0000000..0e375d0 --- /dev/null +++ b/lib/shared/layouts/widgets/filter_panel.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import 'panel_header.dart'; + +/// Left-side filter panel for data filtering and selection. +/// +/// Used within primary content area to filter data grids (e.g., stack list +/// in Control Room). Header content is docked to bottom to accommodate +/// the logo bulge overlay. +/// +/// See UI_LAYOUT.md for panel taxonomy and layout specifications. +class FilterPanel extends StatelessWidget { + const FilterPanel({ + super.key, + required this.title, + required this.icon, + required this.child, + this.width = 280, + this.onRefresh, + }); + + /// Panel title displayed in header. + final String title; + + /// Leading icon for the header. + final IconData icon; + + /// Panel content (e.g., list, search field, filters). + final Widget child; + + /// Panel width (default 280px per UI_LAYOUT.md spec). + final double width; + + /// Optional refresh callback - adds refresh button to header. + final VoidCallback? onRefresh; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + PanelHeader( + title: title, + icon: icon, + dockToBottom: true, // Left-side panel + actions: [ + if (onRefresh != null) + IconButton( + icon: const Icon(Icons.refresh, size: 20), + tooltip: 'Refresh', + onPressed: onRefresh, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + Expanded(child: child), + ], + ), + ); + } +} diff --git a/lib/shared/layouts/widgets/panel_header.dart b/lib/shared/layouts/widgets/panel_header.dart new file mode 100644 index 0000000..6fea177 --- /dev/null +++ b/lib/shared/layouts/widgets/panel_header.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +/// Panel header with configurable content alignment. +/// +/// Left-side panels (Nav, Filter) should use [dockToBottom: true] to +/// accommodate the logo bulge overlay. Right-side panels use default centering. +/// +/// See UI_LAYOUT.md for panel taxonomy and layout specifications. +class PanelHeader extends StatelessWidget { + const PanelHeader({ + super.key, + required this.title, + required this.icon, + this.actions, + this.dockToBottom = false, + this.height = 56.0, + }); + + /// Panel title text. + final String title; + + /// Leading icon for the panel. + final IconData icon; + + /// Optional action widgets (e.g., refresh button). + final List? actions; + + /// Whether to dock content to bottom (for left-side panels under logo bulge). + final bool dockToBottom; + + /// Header height (should match app header height). + final double height; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Container( + height: height, + padding: EdgeInsets.only( + left: 16, + right: 8, + bottom: dockToBottom ? 8 : 0, + ), + alignment: dockToBottom ? Alignment.bottomCenter : Alignment.center, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + border: Border( + bottom: BorderSide(color: colorScheme.outlineVariant), + ), + ), + child: Row( + crossAxisAlignment: + dockToBottom ? CrossAxisAlignment.end : CrossAxisAlignment.center, + children: [ + Icon(icon, color: colorScheme.primary), + const SizedBox(width: 12), + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + if (actions != null) ...actions!, + ], + ), + ); + } +}