# Chanora Adaptive 3-Panel Layout Design **Date:** 2026-06-05 **Status:** Draft **Scope:** Desktop adaptive layout for ≥1024dp three-panel mode, centralized breakpoint system, and chat panel integration. --- ## 1. Problem Statement Chanora's current responsive layout uses a single breakpoint (`_wideBreakpoint = 600dp`) scattered across 9 files in 8 duplicatable clusters (5 `LayoutBuilder` sites, 7 `MediaQuery.sizeOf` sites). The desktop layout is a 2-panel split (VoiceBar 320px + SnapshotView flex) with no persistent chat surface. Research across Discord, Mattermost, Rocket.Chat, Element, and hardware resolution data shows: - **1024dp** is the industry-standard threshold where a third panel becomes viable (Discord member list, Mattermost RHS, Rocket.Chat contextual bar all use this value). - At 1024dp, Chanora's math works: `320 + 12 + 300 + 12 + 380 = 1024` — minimum viable for VoicePanel + ChannelTree + ChatPanel. - Production apps use **push/replace navigation** for chat on constrained widths, reserving persistent panels for ≥1024dp. - Centralized breakpoint logic is standard practice (Rocket.Chat `LayoutProvider`, Mattermost `WindowSizes`). ## 2. Design Decisions | Decision | Choice | Rationale | |---|---|---| | 3-panel activation threshold | **1024dp** | Industry consensus (Discord, Mattermost, Rocket.Chat). Chanora math: center pane = 300dp minimum. | | Chat behavior at 600–1023dp | **Push route (unchanged)** | Research validates current pattern. Overlays are for contextual info, not primary conversation. | | Chat behavior at ≥1024dp | **Inline panel** | Chat renders in a 380dp right panel alongside the channel tree. No route push. | | Centralized breakpoints | **New `ChanoraBreakpoints` + `ViewportInfo`** | Replaces 8 duplicated responsive clusters with single source of truth. | | Architecture approach | **Adaptive Scaffold Shell** | Extends existing widget tree with centralized layout logic. Not a full rewrite. | | AdaptiveScaffold package | **Not used** | Package discontinued (flutter/flutter#162965). Manual layout gives better control for voice-first UX. | ## 3. Breakpoint System ### 3.1 Layout Classes Three tiers, aligned with Material 3 adaptive guidance: | Class | Width Range | Primary Behavior | |---|---|---| | `compact` | < 600dp | Single column. VoiceStatusChip at bottom. Chat as pushed route. | | `medium` | 600–1023dp | 2-panel row (VoicePanel 320px + SnapshotView flex). Chat as pushed route. | | `expanded` | ≥ 1024dp | 3-panel row (VoicePanel 320px + SnapshotView flex + ChatPanel 380px). Chat inline. | ### 3.2 New Files **`lib/design/breakpoints.dart`** — canonical breakpoint tokens: ```dart class ChanoraBreakpoints { static const double compact = 0; static const double medium = 600; static const double expanded = 1024; static const double voicePanelWidth = 320; static const double chatPanelWidth = 380; static const double panelGap = 12; } enum LayoutClass { compact, medium, expanded } LayoutClass layoutClassFromWidth(double width) { if (width >= ChanoraBreakpoints.expanded) return LayoutClass.expanded; if (width >= ChanoraBreakpoints.medium) return LayoutClass.medium; return LayoutClass.compact; } ``` **`lib/design/viewport_info.dart`** — inherited widget that computes layout class once per frame: ```dart class ViewportInfo extends InheritedWidget { const ViewportInfo({ super.key, required this.layoutClass, required this.width, required this.height, required super.child, }); final LayoutClass layoutClass; final double width; final double height; static ViewportInfo of(BuildContext context) { final info = context.dependOnInheritedWidgetOfExactType(); assert(info != null, 'No ViewportInfo found in widget tree'); return info!; } bool get isCompact => layoutClass == LayoutClass.compact; bool get isMedium => layoutClass == LayoutClass.medium; bool get isExpanded => layoutClass == LayoutClass.expanded; @override bool updateShouldNotify(ViewportInfo old) => layoutClass != old.layoutClass || width != old.width || height != old.height; } ``` ### 3.3 What This Replaces The audit identified these duplicatable clusters that get consolidated: | Cluster | Current Locations | Replacement | |---|---|---| | 600dp breakpoint (×3) | `main.dart:304,2280,2468` | `ChanoraBreakpoints.medium` | | 400px cap (×2) | `connect_widgets.dart`, `voice_settings.dart` | Named token in `ChanoraBreakpoints` | | 72% modal height (×2) | `audio_output_tile.dart`, `ptt_capability_badge.dart` | Named token | | 320px voice bar width | `main.dart:2467` | `ChanoraBreakpoints.voicePanelWidth` | | Platform capability branching | `voice_settings.dart`, `voice_compact.dart`, `audio_processing_config_state.dart` | Centralized capability helper | ## 4. Adaptive Shell ### 4.1 Widget Tree The existing `_BetaHome` widget tree is restructured to use `ViewportInfo`: ``` _BetaHome ├─ macOS: Scaffold with traffic-light padding (unchanged) ├─ Mobile: ChanoraMobileScaffold (unchanged) └─ bodyContent: └─ LayoutBuilder └─ ViewportInfo (computes layoutClass from constraints) ├─ compact: Column [SnapshotView, VoiceStatusChip, PTT] ├─ medium: Row [VoicePanel, SnapshotView] └─ expanded: Row [VoicePanel, SnapshotView, ChatPanel] ``` `AdaptiveShell` is a pure layout widget — it reads `ViewportInfo` and composes the appropriate children. All state remains in `_BetaHome`. ### 4.2 Platform Handling Platform-specific scaffolding stays at the top level, unchanged: - **macOS**: `Scaffold` with `_macOSTrafficLightPad` top padding (28dp) - **Mobile**: `ChanoraMobileScaffold` with compact idle chrome - **Windows/Linux**: Default `Scaffold` The `ViewportInfo` + layout switch only affects the body content inside the scaffold. ## 5. Chat Panel Behavior ### 5.1 Compact (< 600dp) No change. Chat opens as a pushed `MaterialPageRoute`: ``` main.dart:_onOpenChat → Navigator.push(ChatPage) ``` Channel tree is fully replaced. Back button returns to main view. ### 5.2 Medium (600–1023dp) Same as compact. Chat is a pushed route. The 2-panel layout (VoicePanel + SnapshotView) stays as the home screen. ### 5.3 Expanded (≥ 1024dp) Chat renders inline in a 380dp right panel. The flow: 1. User taps "Open Text Chat" on a client, or taps the chat badge 2. `_onOpenChat` reads `ViewportInfo.of(context).isExpanded` 3. If expanded: sets `_inlineChatTarget` state → `ChatPanel` renders in the third column 4. If not expanded: pushes `ChatPage` route (unchanged behavior) ### 5.4 ChatPanel Widget New widget for ≥1024dp only: ``` ChatPanel (380dp fixed width) ├─ Header: target name + close button ├─ Message list (scrollable, max-width ~500dp for readability) └─ Input field ``` **State sharing:** The `_chatMessages` list and `_chatFeedRevision` listenable in `_BetaHome` already track all messages. `ChatPanel` reads from the same source — no duplication. **Close behavior:** User taps close button → `_inlineChatTarget` set to null → `ChatPanel` removed from tree. ### 5.5 Width Transition When the user resizes from ≥1024dp to <1024dp while chat is open inline: 1. `ChatPanel` disappears (it's only in the expanded layout branch) 2. A brief snackbar appears: "Tap the chat button to continue your conversation" 3. The `_inlineChatTarget` state is preserved — tapping the chat button reopens the pushed `ChatPage` route with the same target This matches Discord's behavior when the member list collapses on resize. ## 6. Panel Sizing | Element | Width | Behavior | |---|---|---| | VoicePanel (left) | 320dp fixed | VoiceBar, connection status, PTT controls. Unchanged. | | Panel gaps | 12dp | Between each panel. Unchanged. | | SnapshotView (center) | flex (1fr) | Grows to fill remaining space. | | ChatPanel (right) | 380dp fixed | Only rendered at ≥1024dp. | | Chat messages | max-width ~500dp | Centered within ChatPanel for readability. | | macOS traffic light pad | 28dp top | Unchanged. Only affects height. | **Center pane widths at common viewports:** | Viewport | Center Width | Feel | |---|---|---| | 1024dp | 300dp | Minimum viable (matches Discord at same width) | | 1200dp | 476dp | Comfortable | | 1280dp | 556dp | Spacious (Chanora's default window size) | | 1440dp | 716dp | Very spacious | | 1920dp | 1184dp | Ultra-wide — consider capping center max-width post-MVP | ## 7. Migration Map | File | Change | Scope | |---|---|---| | `lib/design/breakpoints.dart` | **New** — breakpoint tokens + `LayoutClass` enum | New file | | `lib/design/viewport_info.dart` | **New** — `ViewportInfo` inherited widget | New file | | `lib/main.dart` | Replace `_wideBreakpoint = 600.0` with `ChanoraBreakpoints.medium`. Wrap body in `ViewportInfo`. Add `_inlineChatTarget` state. Branch `_onOpenChat` for expanded vs compact/medium. Add `ChatPanel` to expanded Row. | Significant | | `lib/widgets/chat_views.dart` | Replace `_chatMobileBreakpoint` with `ChanoraBreakpoints.medium`. No structural changes. | Token swap | | `lib/widgets/connect_widgets.dart` | Replace hardcoded 400px with token. | Token swap | | `lib/widgets/app_snack_bar.dart` | Replace hardcoded 600/560px with tokens. | Token swap | | `lib/widgets/snapshot_view.dart` | No changes. Local spacer math stays local. | None | | `lib/widgets/voice_compact.dart` | Replace platform branching with centralized helper (optional, post-MVP). | Optional | **Unchanged:** macOS scaffold, ChanoraMobileScaffold, all voice controls, channel tree, chat route for compact/medium, all Rust bridge code. ## 8. Hardware Coverage The 1024dp threshold coverage based on 2026 resolution data: | Setup | Logical Width | Sees 3-Panel? | |---|---|---| | 1920×1080 @100% fullscreen | 1920dp | Yes | | 1920×1080 @125% fullscreen | 1536dp | Yes | | 1920×1080 @150% fullscreen | 1280dp | Yes | | 1366×768 @100% fullscreen | 1366dp | Yes | | 1366×768 @125% fullscreen | 1093dp | Yes | | 1366×768 @125% windowed (~85%) | ~930dp | No (2-panel) | | 2560×1440 @100% half-screen | ~1280dp | Yes | | 2560×1440 @125% half-screen | ~1024dp | Yes (edge) | | MacBook 13" Split View | ~708dp | No (2-panel) | | MacBook 14" Split View | ~744dp | No (2-panel) | | MacBook 16" Split View | ~852dp | No (2-panel) | Chanora's default window (1280×720 on Windows/Linux) starts in 3-panel mode immediately. ## 9. Out of Scope (Post-MVP) - Resizable panels (drag-to-resize VoicePanel/ChatPanel width) - NavigationRail for ultra-wide monitors - ChatPanel showing user profile or channel info - Center pane max-width cap for ultra-wide monitors - Centralized platform capability helper (consolidating voice_settings/voice_compact/audio_processing branching) - Animated transitions between layout classes - ChatPanel as a sheet/drawer on medium widths ## 10. References - Discord member list collapse at 1024px: [compact-discord](https://github.com/asportnoy/compact-discord) - Mattermost RHS persistent at ≥1024px: [structure.scss](https://github.com/mattermost/mattermost/blob/3440453d82613b1d8d67c93011c11d56a1380869/webapp/channels/src/sass/base/_structure.scss) - Rocket.Chat contextual bar persistent at ≥1024px (lg breakpoint): [fuselage-tokens](https://github.com/RocketChat/fuselage/blob/ed91cb04db9fd6c35b43390190cbf7327c3eab9e/packages/fuselage-tokens/src/breakpoints.jsonc) - Flutter AdaptiveScaffold discontinued: [flutter/flutter#162965](https://github.com/flutter/flutter/issues/162965) - Material 3 canonical breakpoints: [m3.material.io/foundations/layout](https://m3.material.io/foundations/layout/breakpoints/overview) - Chanora adaptive layout policy: [docs/ui-ux/adaptive-layout-platform-guide.md](../ui-ux/adaptive-layout-platform-guide.md)