Files
chanora/docs/superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md
T
Edison Jwa 5e8b7915db feat(ui): adaptive 3-panel layout, chat panel switching, audio metering fix
- Add responsive breakpoints (compact <600, medium 600-1023, expanded >=1024)
- Add ViewportInfo InheritedWidget for layout-aware descendants
- Add inline ChatPanel (380dp right column) for expanded desktop layout
- Add channel right-click context menu with Chat option for in-place switching
- Add per-target draft persistence via restoredDraft/onDraftChanged callbacks
- Fix header chat button to switch to current voice channel when panel open
- Fix close = dismiss (preserves last target and draft for reopen)
- Add unread dot indicator on channel tiles when chat is closed
- Fix audio regression: decimate dBFS computation to every 3rd callback (~31 Hz)
  to avoid buffer underruns on macOS CoreAudio real-time thread
- Add tools/build-macos.sh release build script (7-step process)
- Add chat panel switching implementation plan and 3-panel design spec

Tests: 183 passed, 2 skipped. Flutter analyze clean.
2026-06-07 23:12:07 +09:00

12 KiB
Raw Blame History

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:

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:

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