Files
chanora/dev-docs/superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md
T
Edison Jwa bba6273af7 refactor: restructure docs as submodule, add dev-docs/ and AGENTS.md
- 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)
2026-06-13 03:32:33 +09:00

267 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 6001023dp | **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` | 6001023dp | 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 (6001023dp)
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)