P14: dock the session sidebar (persistent column, not a temp drawer)

- The session panel now docks as a persistent right-hand column on desktop and
  only floats as an overlay drawer on phones (md breakpoint). Open state persists
  (uiStore.sessionDockOpen). The header button opens it (idempotent); the ✕ closes.
- Players get it auto-opened on join — they live in the panel during play.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 17:08:13 +02:00
parent b2cf62f642
commit 4cb834ad6c
3 changed files with 33 additions and 14 deletions
+23 -9
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Link, Outlet, useRouterState } from '@tanstack/react-router';
import { useUiStore } from '@/stores/uiStore';
import { CommandPalette } from '@/components/CommandPalette';
@@ -63,8 +63,11 @@ function ThemeToggle() {
export function RootLayout() {
const pathname = useRouterState({ select: (s) => s.location.pathname });
const [paletteOpen, setPaletteOpen] = useState(false);
const [sessionOpen, setSessionOpen] = useState(false);
const sessionDockOpen = useUiStore((s) => s.sessionDockOpen);
const setSessionDock = useUiStore((s) => s.setSessionDock);
const sessionConnected = useSessionStore((s) => s.status === 'connected');
const sessionRole = useSessionStore((s) => s.role);
const joinIntent = useSessionStore((s) => s.joinIntent);
const activeCampaign = useActiveCampaign();
// While the GM is hosting, mirror state to players (inert unless hosting).
useSessionBroadcaster(activeCampaign ?? null);
@@ -82,6 +85,15 @@ export function RootLayout() {
return () => window.removeEventListener('keydown', onKey);
}, []);
// Players get the session panel docked open by default (once) — they live in it.
const autoOpenedRef = useRef(false);
useEffect(() => {
if (!autoOpenedRef.current && joinIntent && sessionRole === 'player') {
autoOpenedRef.current = true;
setSessionDock(true);
}
}, [joinIntent, sessionRole, setSessionDock]);
return (
<div className="flex h-full flex-col">
<header className="flex items-center gap-4 border-b border-line bg-panel px-4 py-2">
@@ -107,7 +119,7 @@ export function RootLayout() {
</nav>
<div className="ml-auto flex items-center gap-2">
<PlayerSessionBadge />
<Button size="sm" variant="ghost" onClick={() => setSessionOpen(true)} title="Session panel — who's here, rolls, chat" aria-label="Open session panel">
<Button size="sm" variant="ghost" onClick={() => setSessionDock(true)} title="Session panel — who's here, rolls, chat" aria-label="Open session panel">
Session{sessionConnected && <span className="ml-1 inline-block h-1.5 w-1.5 rounded-full bg-success align-middle" />}
</Button>
{activeCampaign && <HandoutControl />}
@@ -121,14 +133,16 @@ export function RootLayout() {
</div>
</header>
<main className="flex-1 overflow-auto">
<ErrorBoundary resetKey={pathname}>
<Outlet />
</ErrorBoundary>
</main>
<div className="flex min-h-0 flex-1">
<main className="flex-1 overflow-auto">
<ErrorBoundary resetKey={pathname}>
<Outlet />
</ErrorBoundary>
</main>
<SessionSidebar open={sessionDockOpen} onClose={() => setSessionDock(false)} />
</div>
<RollTray />
<SessionSidebar open={sessionOpen} onClose={() => setSessionOpen(false)} />
{paletteOpen && <CommandPalette onClose={() => setPaletteOpen(false)} />}
</div>
);