# 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<ViewportInfo>();
    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)
