dorfteich/apps/web/src/layout/AppLayout.tsx
Claude Fable 5 6c4f37ef91
Some checks failed
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Successful in 3m35s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m45s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Has been cancelled
Make the sidebar width drag-resizable and persistent (#99)
- SidebarResizer: pointer-drag handle on the sidebar's right edge,
  keyboard-adjustable (arrows, Home/End), double-click resets to the
  16rem default; width clamped to 12-32rem
- AppLayout persists the width via usePersistentState (ui.sidebar.width)
  and sets --sidebar-width inline on .app-body, so collapse/force-hide
  keep animating from/to the chosen width
- localized aria-label (de+en), handle hidden while collapsed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-12 04:16:56 +02:00

57 lines
2.1 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
import { usePersistentState } from '../lib/use-persistent-state';
import { Footer } from './Footer';
import { SidebarChromeContext } from './sidebar-chrome';
import { Sidebar } from './Sidebar';
import { clampSidebarWidth, SIDEBAR_DEFAULT_WIDTH_REM, SidebarResizer } from './SidebarResizer';
import { TopBar } from './TopBar';
export function AppLayout(): React.JSX.Element {
const [sidebarCollapsed, setSidebarCollapsed] = usePersistentState('ui.sidebar.collapsed', false);
const [sidebarWidth, setSidebarWidth] = usePersistentState(
'ui.sidebar.width',
SIDEBAR_DEFAULT_WIDTH_REM,
);
const [forcedHidden, setForcedHidden] = useState(false);
const collapsed = sidebarCollapsed || forcedHidden;
// Clamp on read too — the persisted value may predate a bounds change.
const widthRem = clampSidebarWidth(sidebarWidth);
// Ctrl/Cmd+\ toggles the sidebar (same shortcut as Notion), regardless of
// which element has focus.
useEffect(() => {
function handleKeyDown(event: KeyboardEvent): void {
if ((event.ctrlKey || event.metaKey) && event.key === '\\') {
event.preventDefault();
setSidebarCollapsed(!sidebarCollapsed);
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [sidebarCollapsed, setSidebarCollapsed]);
return (
<SidebarChromeContext.Provider value={setForcedHidden}>
<div className="app">
<TopBar
sidebarCollapsed={collapsed}
onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)}
/>
<div
className="app-body"
style={{ '--sidebar-width': `${widthRem}rem` } as React.CSSProperties}
>
<Sidebar collapsed={collapsed} />
{!collapsed && <SidebarResizer widthRem={widthRem} onResize={setSidebarWidth} />}
<main className="main">
<Outlet />
<Footer />
</main>
</div>
</div>
</SidebarChromeContext.Provider>
);
}