Files
investor-flow/app/src/components/LayoutShell.tsx
T

131 lines
4.5 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect, useState } from "react";
import { SidebarNav } from "./SidebarNav";
import { WatchlistSidebar } from "./WatchlistSidebar";
import { SymbolHeader } from "./SymbolHeader";
import { MobileTabNav } from "./MobileTabNav";
import { CollapsibleProvider, useCollapsibleContext } from "@/lib/collapsible-context";
import { useKeyboardShortcuts } from "@/lib/useKeyboardShortcuts";
const MOBILE_BREAKPOINT = 768;
const TABLET_BREAKPOINT = 1024;
function useViewport() {
const [width, setWidth] = useState(typeof window !== "undefined" ? window.innerWidth : 1200);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
return width;
}
/**
* Inner component that attaches the global Escape listener.
* Wrapped in CollapsibleProvider so all CollapsibleSections can register.
*/
function LayoutShellInner({ children }: { children: React.ReactNode }) {
const { openSectionId, setOpenSectionId } = useCollapsibleContext();
const viewportWidth = useViewport();
const isMobile = viewportWidth < MOBILE_BREAKPOINT;
const isTablet = viewportWidth >= MOBILE_BREAKPOINT && viewportWidth < TABLET_BREAKPOINT;
// Keyboard shortcuts: Escape closes open sections, modals
useKeyboardShortcuts([
{
key: "Escape",
handler: () => {
if (openSectionId) {
setOpenSectionId(null);
}
},
},
]);
// Global Escape listener: collapse the currently open section.
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape" && openSectionId) {
e.preventDefault();
// Collapse the currently open section by clearing the shared state.
// Individual CollapsibleSection Escape handlers also respond to this.
setOpenSectionId(null);
}
};
window.addEventListener("keydown", handler, true); // capture phase
return () => window.removeEventListener("keydown", handler, true);
}, [openSectionId, setOpenSectionId]);
return (
<div className="min-h-screen bg-[#0a0b0f] text-[#e6e7ec] flex flex-col">
{/* Symbol Header — always visible */}
<SymbolHeader />
{/* Main Content Area — responsive layout */}
{isMobile ? (
/* Mobile: single column, no sidebar */
<>
<main className="flex-1 overflow-y-auto p-4 pb-20">
<div className="max-w-7xl mx-auto space-y-6">
{children}
</div>
</main>
<MobileTabNav />
</>
) : isTablet ? (
/* Tablet: 2-column layout, watchlist accessible via compact bar */
<div className="flex flex-1 overflow-hidden flex-col">
{/* Compact horizontal watchlist bar */}
<div className="hidden md:block border-b border-[#2a2b3a] bg-[#121318] px-4 py-2 overflow-x-auto">
<WatchlistSidebar compact />
</div>
<div className="flex flex-1 overflow-hidden">
{/* Center content */}
<main className="flex-1 overflow-y-auto p-4 sm:p-6">
<div className="max-w-7xl mx-auto space-y-6">
{children}
</div>
</main>
{/* Right analytics — drops below on tablet */}
<aside className="hidden lg:block w-80 border-l border-[#2a2b3a] bg-[#121318] overflow-y-auto">
<div className="p-4 space-y-4">
{/* Right column content placeholder */}
</div>
</aside>
</div>
</div>
) : (
/* Desktop: 3-column layout */
<div className="flex flex-1 overflow-hidden">
{/* Left Sidebar Navigation */}
<SidebarNav />
{/* Watchlist Sidebar */}
<WatchlistSidebar />
{/* Content */}
<main className="flex-1 overflow-y-auto p-4 sm:p-6">
<div className="max-w-7xl mx-auto space-y-6">
{children}
</div>
</main>
</div>
)}
</div>
);
}
/**
* Main layout shell for the redesigned terminal.
* - Desktop (>1024px): Sidebar + Watchlist + Header + Content (3-column)
* - Tablet (768-1024px): Watchlist + Header + Content (2-column, sidebar hidden)
* - Mobile (<768px): Header + Content (single column) + MobileTabNav
*/
export function LayoutShell({ children }: { children: React.ReactNode }) {
return (
<CollapsibleProvider>
<LayoutShellInner>{children}</LayoutShellInner>
</CollapsibleProvider>
);
}