Files
tatlock-ui/docs/UI_LAYOUT.md
T
Jeroen SchweitzerandClaude Opus 4.5 cbf6ca5338 docs: add Nav Panel section headers mockup
Document conditional section headers for grouping nav items:
- Single section: headers hidden
- Multiple sections: headers visible with left accent bar
- Route configuration driven data model
- Updated Control Room wireframes with current/future state

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 16:38:29 +01:00

32 KiB

UI Layout Specification

This document defines the layout structure, responsive behavior, and component constraints for Tatlock UI. It implements the "Rooms of the Estate" metaphor from PHILOSOPHY.md.

Navigation Structure

The UI uses a tabbed room navigation in the header rather than a traditional sidebar. Each "room" represents a distinct usage context, reducing cognitive clutter.

Room Purpose Chat Dock Default
Front Hall Dashboard - estate overview, quick access, activity Expanded
Control Room Infrastructure - containers, stacks, monitoring Collapsed
Parlor Housekeeping - home automation, devices, scenes Collapsed
Library Knowledge - docs, notes, bookmarks (future) Collapsed

Hidden for now:

  • Study - Secretarial tasks (email, calendaring) - future implementation

Layout Structure

┌─────────────────────────────────────────────────────────────────────────────┐
│  HEADER BAR (56px, with logo bulge overlay)                                 │
│  [◯Logo◯] [Room Icons] ─────────────────────────────────── [Profile Menu]  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────┐ ┌─────────────────────────────────┐ ┌───────────────────┐  │
│  │ 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          │  │
│                  └─────────────────────────────────┘ └───────────────────┘  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

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.

Nav Panel Sections

Nav items can be organized into sections. Section headers are conditionally visible - they only appear when multiple sections exist.

Single Section (headers hidden)

When all items belong to one section, no headers are shown:

┌─────────────────────────┐
│  [≡] Sections     [···] │  ← Panel header
├─────────────────────────┤
│  ▸ Containers           │
│    Networks             │
│    Volumes              │
│    Images               │
│                         │
└─────────────────────────┘

Multiple Sections (headers visible)

When items span multiple sections, section headers appear:

┌─────────────────────────┐
│  [≡] Sections     [···] │  ← Panel header
├─────────────────────────┤
│ ▌Portainer              │  ← Section header (subtle bg, left accent)
│  ▸ Containers           │
│    Networks             │
│    Volumes              │
│    Images               │
│                         │
│ ▌NPM                    │  ← Section header
│    Proxy Hosts          │
│    Redirections         │
│    Streams              │
│                         │
│ ▌Authentik              │  ← Section header
│    Users                │
│    Groups               │
│    Applications         │
│                         │
└─────────────────────────┘

Section Header Styling

┌─────────────────────────┐
│▌SECTION NAME            │  ← Left accent bar (2px, primary color)
└─────────────────────────┘   ← Background: surfaceContainerHigh
                              ← Text: labelSmall, onSurfaceVariant
                              ← Padding: 8px horizontal, 6px vertical
                              ← All caps, letter-spacing: 0.5

Data Model

/// NavItem model (in nav_panel.dart)
class NavItem {
  final String id;
  final String label;
  final IconData icon;
  final String? section;  // null = ungrouped
}

// Section headers auto-generate from unique section values
// Visibility: items.map((i) => i.section).toSet().length > 1

Route Configuration Driven

Sections are defined in the feature's route configuration, not the widget:

/// In lib/features/control_room/router.dart

enum ControlRoomNav {
  // Portainer section
  containers('containers', 'Containers', Icons.dns, 'Portainer'),
  networks('networks', 'Networks', Icons.hub, 'Portainer'),
  volumes('volumes', 'Volumes', Icons.storage, 'Portainer'),
  images('images', 'Images', Icons.photo_library, 'Portainer'),
  // NPM section (future)
  proxyHosts('proxy-hosts', 'Proxy Hosts', Icons.public, 'NPM'),
  redirections('redirections', 'Redirections', Icons.alt_route, 'NPM'),
  // Authentik section (future)
  users('users', 'Users', Icons.people, 'Authentik'),
  groups('groups', 'Groups', Icons.group_work, 'Authentik');

  const ControlRoomNav(this.id, this.label, this.icon, this.section);

  final String id;
  final String label;
  final IconData icon;
  final String section;

  NavItem toNavItem() => NavItem(
        id: id,
        label: label,
        icon: icon,
        section: section,
      );
}

The NavPanel widget receives items and auto-generates section headers based on unique section values in the list. No section logic lives in the widget - it just renders what the route config provides.


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 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

Responsive Breakpoints

Wide (>= 1200px)

┌───────────┬─────────────────────────────────────────────┬────────────────────┐
│ 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
  • Nav panel visible with full labels
  • Filter panel visible (where applicable)
  • Full data grid columns

Medium (>= 800px, < 1200px)

┌───────────┬───────────────────────────────────────────────────────────┬────┐
│ NAV PANEL │  [Filter Panel]  [═══════════ Data Grid ═══════════]      │ 💬 │
│ (280px)   │  (280px)         (flex: 1)                                │48px│
└───────────┴───────────────────────────────────────────────────────────┴────┘
  • 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 ═══════════════════════]        │ 💬 │
│48px│  Filter panel becomes top bar or collapsible                      │48px│
└────┴───────────────────────────────────────────────────────────────────┴────┘
  • 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)

┌────────────────────────────────────────────────────────────────────────┐
│  HEADER: [≡] [Room Icons scroll] ─────────────────────────────── [💬] │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│  [═══════════════════════ Content ══════════════════════]              │
│  Filter as expandable section at top                                   │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘
  • Single column layout
  • 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

Component Constraints

Component Min Width Max Width Collapse Behavior
Chat Dock (expanded) 280px 33vw Collapses to icon rail
Chat Dock (collapsed) 48px 48px -
Context Sidebar 200px - Collapses to 56px icon rail, then drawer
Stat Card 180px 1fr Grid reflows
Service Card 120px 1fr Grid reflows
Data Grid 400px - Horizontal scroll if needed

Room Wireframes

Front Hall (Dashboard)

Primary landing page. Chat dock expanded by default.

┌─────────────────────────────────────────────────────────────┬─────────────────┐
│                                                             │                 │
│  STAT CARDS (auto-fit grid, min 180px)                      │  TATLOCK CHAT   │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────┐ │                 │
│  │ CPU         │ │ Memory      │ │ Disk        │ │Containe│ │  [conversation] │
│  │ 45%         │ │ 62%         │ │ 78%         │ │ 34/40  │ │                 │
│  │ ████████░░░ │ │ ██████████░ │ │ ███████████ │ │ ██████ │ │                 │
│  └─────────────┘ └─────────────┘ └─────────────┘ └────────┘ │                 │
│                                                             │                 │
│  QUICK ACCESS (auto-fill grid, min 120px)                   │                 │
│  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐    │                 │
│  │ 🎬     │ │ 🔍     │ │ 📚     │ │ 🎮     │ │ 🌐     │    │                 │
│  │Jellyfin│ │SearXNG │ │ Wiki   │ │ AMP    │ │Requests│    │                 │
│  │● Online│ │● Online│ │● Online│ │● Online│ │● Online│    │                 │
│  └────────┘ └────────┘ └────────┘ └────────┘ └────────┘    │                 │
│                                                             │                 │
│  RECENT ACTIVITY                                            │  [input field]  │
│  ┌────────────────────────────────────────────────────────┐ │                 │
│  │ 📦 Container 'ollama' restarted            15 min ago  │ │                 │
│  │ 🏠 Living room set to evening mode          1 hr ago   │ │                 │
│  │ 🔄 System backup completed                  2 hrs ago  │ │                 │
│  └────────────────────────────────────────────────────────┘ │                 │
│                                                             │                 │
└─────────────────────────────────────────────────────────────┴─────────────────┘

Components:

  • Stat Cards: CPU, Memory, Disk, Containers
  • Quick Access: Service cards grid (external links)
  • Recent Activity: Timeline of system events

Control Room (Infrastructure)

Nav panel with grouped section navigation. Chat collapsed.

Current state (single grouper - headers hidden):

┌───────────────┬───────────────────────────────────────────────────────┬──────┐
│               │                                                       │      │
│  SECTIONS     │  CONTAINERS                        [Search] [+ New]   │  💬  │
│               │  ─────────────────────────────────────────────────── │      │
│  ▸ Containers │  ☑ NAME          STATUS    CPU    MEM    IMAGE    ⋮  │      │
│    Networks   │  ☐ jellyfin      ● Run     2.3%   1.2GB  latest   ⋮  │      │
│    Volumes    │  ☐ ollama        ● Run     45%    8.0GB  0.1.32   ⋮  │      │
│    Images     │  ☐ postgres      ● Run     1.1%   512MB  16-alp   ⋮  │      │
│               │  ☐ redis         ● Run     0.2%   128MB  7-alp    ⋮  │      │
│               │  ─────────────────────────────────────────────────── │      │
│               │  Showing 5 of 40                    < 1 2 3 4 5 >    │      │
│               │                                                       │      │
└───────────────┴───────────────────────────────────────────────────────┴──────┘

Future state (multiple groupers - headers visible):

┌───────────────┬───────────────────────────────────────────────────────┬──────┐
│               │                                                       │      │
│  SECTIONS     │  CONTAINERS                        [Search] [+ New]   │  💬  │
│               │  ─────────────────────────────────────────────────── │      │
│ ▌PORTAINER    │  ☑ NAME          STATUS    CPU    MEM    IMAGE    ⋮  │      │
│  ▸ Containers │  ☐ jellyfin      ● Run     2.3%   1.2GB  latest   ⋮  │      │
│    Networks   │  ☐ ollama        ● Run     45%    8.0GB  0.1.32   ⋮  │      │
│    Volumes    │  ─────────────────────────────────────────────────── │      │
│    Images     │  Showing 3 of 40                    < 1 2 3 4 5 >    │      │
│               │                                                       │      │
│ ▌NPM          │                                                       │      │
│    Proxy Hosts│                                                       │      │
│    Redirects  │                                                       │      │
│               │                                                       │      │
│ ▌AUTHENTIK    │                                                       │      │
│    Users      │                                                       │      │
│    Groups     │                                                       │      │
│               │                                                       │      │
└───────────────┴───────────────────────────────────────────────────────┴──────┘

Components:

  • Nav Panel: Grouped section navigation (grouper headers conditional)
  • Filter Panel: Stack/item filtering within section
  • DataGrid: Container list with bulk actions
  • Detail Panel: Opens on row selection (replaces grid or slides in)

Parlor (Housekeeping)

Context sidebar with areas and scenes. Chat collapsed.

┌───────────────┬───────────────────────────────────────────────────────┬──────┐
│               │                                                       │      │
│  AREAS        │  LIVING ROOM                                          │  💬  │
│               │  ─────────────────────────────────────────────────── │      │
│  ▸ Living Rm  │                                                       │      │
│    Bedroom    │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │      │
│    Kitchen    │  │ 💡       │ │ 📺       │ │ 🌡️       │ │ 🔌       │ │      │
│    Office     │  │ Ceiling  │ │ Samsung  │ │ Thermo   │ │ Outlet 1 │ │      │
│    Garage     │  │ 75%      │ │ OFF      │ │ 72°F     │ │ ON       │ │      │
│               │  │ ○─────●  │ │ [    ]   │ │ [-] [+]  │ │ [    ]   │ │      │
│  ──────────── │  └──────────┘ └──────────┘ └──────────┘ └──────────┘ │      │
│  SCENES       │                                                       │      │
│    🌅 Morning │  SCENES                                               │      │
│    ☀️ Day     │  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐       │      │
│    🌆 Evening │  │ 🌅   │ │ ☀️   │ │ 🌆   │ │ 🌙   │ │ 🎬   │       │      │
│    🌙 Night   │  │ Morn │ │ Day  │ │ Eve  │ │Night │ │Movie │       │      │
│               │  └──────┘ └──────┘ └──────┘ └──────┘ └──────┘       │      │
└───────────────┴───────────────────────────────────────────────────────┴──────┘

Components:

  • Context Sidebar: Area list + scene shortcuts
  • Device Grid: Control tiles for current area
  • Scene Bar: Quick activation buttons

Grid Specifications

Stat Cards

grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
Viewport Width Cards per Row
>= 900px 4
>= 720px 3
>= 540px 2
< 540px 1

Service Cards

grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 0.75rem;

Reflows naturally based on available width.


Chat Dock Behavior

The Tatlock chat assistant is omnipresent - accessible from any room.

State Width Trigger
Expanded intrinsic (min 280px, max 33vw) Default on Front Hall, click icon elsewhere
Collapsed 48px (icon rail) Default on other rooms, viewport < 1200px
Overlay 90vw or 400px max Expanded on medium viewports
Bottom Sheet 100vw, 60vh max Mobile viewports
Fullscreen 100vw, 100vh User-triggered overlay (future)

Persistence: Conversation state persists across room navigation.

Future: Fullscreen mode provides an immersive chat experience as a modal overlay, independent of the current 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

/// 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
Concept Flutter Widget
Breakpoint detection LayoutBuilder, MediaQuery
Main layout Row with Expanded and SizedBox
Collapsible dock AnimatedContainer or AnimatedSize
Grid layouts GridView.builder with SliverGridDelegateWithMaxCrossAxisExtent
Drawer fallback Scaffold.drawer or custom Drawer
Bottom sheet chat showModalBottomSheet or DraggableScrollableSheet

Breakpoint Constants

abstract class Breakpoints {
  static const double mobile = 600;
  static const double compact = 800;
  static const double medium = 1200;
}

Panel Width Constants

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

LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth >= Breakpoints.medium) {
      return WideLayout(...);
    } else if (constraints.maxWidth >= Breakpoints.compact) {
      return MediumLayout(...);
    } else {
      return CompactLayout(...);
    }
  },
)