Files
stack/apps/web/src/app/(authenticated)/layout.tsx
Jason Woltje 28620b2d70
All checks were successful
ci/woodpecker/push/web Pipeline was successful
feat(web): add responsive layout with mobile sidebar overlay (MS15-FE-005)
Mobile (< 768px): sidebar hidden, hamburger button in header, sidebar
slides in as fixed overlay with backdrop.

Tablet (768-1023px): sidebar toggleable via hamburger, pushes content.

Desktop (>= 1024px): sidebar always visible, no hamburger button.

SidebarContext extended with mobileOpen state and isMobile detection
via matchMedia listener.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:54:06 -06:00

108 lines
3.4 KiB
TypeScript
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.
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/lib/auth/auth-context";
import { IS_MOCK_AUTH_MODE } from "@/lib/config";
import { AppHeader } from "@/components/layout/AppHeader";
import { AppSidebar } from "@/components/layout/AppSidebar";
import { SidebarProvider, useSidebar } from "@/components/layout/SidebarContext";
import { ChatOverlay } from "@/components/chat";
import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
import type { ReactNode } from "react";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const SIDEBAR_EXPANDED_WIDTH = "240px";
const SIDEBAR_COLLAPSED_WIDTH = "60px";
// ---------------------------------------------------------------------------
// Inner shell — must be a child of SidebarProvider to use useSidebar
// ---------------------------------------------------------------------------
interface AppShellProps {
children: ReactNode;
}
function AppShell({ children }: AppShellProps): React.JSX.Element {
const { collapsed, isMobile } = useSidebar();
// On tablet (mdlg), hide sidebar from the grid when the sidebar is collapsed.
// On mobile, the sidebar is fixed-position so the grid is always single-column.
const sidebarHidden = !isMobile && collapsed;
return (
<div
className="app-shell"
data-sidebar-hidden={sidebarHidden ? "true" : undefined}
style={
{
"--sidebar-w": collapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_EXPANDED_WIDTH,
transition: "grid-template-columns 0.2s var(--ease, ease)",
} as React.CSSProperties
}
>
{/* Full-width header — grid-column: 1 / -1 via .app-header CSS class */}
<AppHeader />
{/* Sidebar — left column, row 2, via .app-sidebar CSS class */}
<AppSidebar />
{/* Main content — right column, row 2, via .app-main CSS class */}
<main className="app-main" id="main-content">
{IS_MOCK_AUTH_MODE && (
<div
className="border-b px-4 py-2 text-xs font-medium flex-shrink-0"
style={{
borderColor: "var(--ms-amber-500)",
background: "rgba(245, 158, 11, 0.08)",
color: "var(--ms-amber-400)",
}}
data-testid="mock-auth-banner"
>
Mock Auth Mode (Local Only): Real authentication is bypassed for frontend development.
</div>
)}
<div className="flex-1 overflow-y-auto p-5">{children}</div>
</main>
{!IS_MOCK_AUTH_MODE && <ChatOverlay />}
</div>
);
}
// ---------------------------------------------------------------------------
// Authenticated layout — handles auth guard + provides sidebar context
// ---------------------------------------------------------------------------
export default function AuthenticatedLayout({
children,
}: {
children: ReactNode;
}): React.JSX.Element | null {
const router = useRouter();
const { isAuthenticated, isLoading } = useAuth();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push("/login");
}
}, [isAuthenticated, isLoading, router]);
if (isLoading) {
return <MosaicSpinner size={48} fullPage />;
}
if (!isAuthenticated) {
return null;
}
return (
<SidebarProvider>
<AppShell>{children}</AppShell>
</SidebarProvider>
);
}