feat: add reusable panel system with FilterPanel widget
- Update UI_LAYOUT.md with comprehensive panel taxonomy - Document Nav Panel, Filter Panel, Detail Panel, Chat Dock specs - Add panel header behavior (bottom-docked for left panels) - Add responsive breakpoints with panel folding summary - Create PanelHeader widget with dockToBottom option - Create FilterPanel widget for data filtering sidebars - Refactor Control Room to use FilterPanel - Remove external links from sidebar (cleanup) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
de739e8f7a
commit
948e104ce5
+183
-42
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Widget>? 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!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user