- Move ASPICE docs to chanoraapp/docs submodule at docs/ - Move development docs to dev-docs/ (superpowers, offline-knowledge, impl-mapping) - Add AGENTS.md with project conventions for AI agents - Add impl-mapping.md (SAD component → source file mapping) - Archive completed plans to dev-docs/superpowers/plans/_archived/ - Remove AGENTS.md from .gitignore (now tracked)
267 lines
12 KiB
Markdown
267 lines
12 KiB
Markdown
# 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)
|