rocRevyAreGoals15's picture
Initial HF Space deployment
9e89154
Raw
History Blame Contribute Delete
13.1 kB
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { type ReactNode, useEffect, useState } from "react";
import { useLedgerSession } from "@/context/LedgerSessionContext";
import { useThemePreference, type ThemePreference } from "@/context/ThemeContext";
const SIDEBAR_COLLAPSED_KEY = "ql-sidebar-collapsed";
/** Sidebar order: executive mockup core (Strategy → Quantum → Simulations) + product routes + Settings stub */
const NAV_ITEMS = [
{ href: "/dashboard", label: "Executive Dashboard", icon: "dashboard" },
{ href: "/portfolio", label: "Portfolio Lab", icon: "science" },
{ href: "/strategy", label: "Strategy Builder", icon: "architecture" },
{ href: "/quantum", label: "Quantum Engine", icon: "memory" },
{ href: "/simulations", label: "Simulations", icon: "query_stats" },
{ href: "/reports", label: "Reports", icon: "description" },
{ href: "/settings", label: "Settings", icon: "settings" },
];
function NavItem({
href,
label,
icon,
active,
collapsed,
}: {
href: string;
label: string;
icon: string;
active: boolean;
collapsed: boolean;
}) {
const base =
"flex items-center rounded-lg text-sm font-medium transition-all no-underline shrink-0";
const expandedPad = "gap-3 px-4 py-3 border-l-4";
const iconOnlyPad = "justify-center px-2 py-3 min-w-0";
const activeExpanded =
"bg-ql-surface-container text-ql-primary border-ql-primary-container";
const activeCollapsed =
"bg-ql-surface-container text-ql-primary ring-2 ring-inset ring-ql-primary-container/80 border-transparent";
const idleExpanded =
"text-ql-on-surface-variant hover:bg-ql-surface-container hover:text-ql-on-surface border-transparent";
const idleCollapsed =
"text-ql-on-surface-variant hover:bg-ql-surface-container hover:text-ql-on-surface border-transparent";
return (
<Link
href={href}
title={collapsed ? label : undefined}
className={`${base} ${collapsed ? iconOnlyPad : expandedPad} ${
active
? collapsed
? activeCollapsed
: `${activeExpanded} border-l-4`
: collapsed
? idleCollapsed
: `${idleExpanded} border-l-4`
}`}
>
<span className="material-symbols-outlined text-xl shrink-0">{icon}</span>
{!collapsed ? <span className="font-body truncate">{label}</span> : null}
</Link>
);
}
const THEME_META: Record<ThemePreference, { icon: string; label: string }> = {
dark: { icon: "dark_mode", label: "Dark" },
light: { icon: "light_mode", label: "Light" },
system: { icon: "contrast", label: "System" },
};
function objectiveLabel(obj: string): string {
return obj.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
function sessionsDiffer(
last: { objective: string; tickers: string[] },
current: { objective: string; tickers: string[] }
): boolean {
if (last.objective !== current.objective) return true;
if (last.tickers.length !== current.tickers.length) return true;
const a = [...last.tickers].sort().join("\0");
const b = [...current.tickers].sort().join("\0");
return a !== b;
}
export default function AppLayout({ children }: { children: ReactNode }) {
const pathname = usePathname();
const { session } = useLedgerSession();
const { preference, cycle } = useThemePreference();
const themeMeta = THEME_META[preference];
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
useEffect(() => {
try {
if (typeof window !== "undefined" && localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "1") {
setSidebarCollapsed(true);
}
} catch {
/* ignore */
}
}, []);
const toggleSidebar = () => {
setSidebarCollapsed((c) => {
const next = !c;
try {
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, next ? "1" : "0");
} catch {
/* ignore */
}
return next;
});
};
const lo = session.lastOptimize;
const displayObjective = lo ? lo.objective : session.objective;
const displayTickerCount = lo ? lo.tickers.length : session.tickers.length;
const currentDiffersFromLast =
lo != null && sessionsDiffer(lo, session);
const sessionTooltip = [
`Session · ${objectiveLabel(displayObjective)}`,
`${displayTickerCount} ticker${displayTickerCount === 1 ? "" : "s"}`,
lo
? `Last run ${new Date(lo.at).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}`
: null,
currentDiffersFromLast
? `Current: ${objectiveLabel(session.objective)} · ${session.tickers.length} tickers`
: null,
]
.filter(Boolean)
.join(" · ");
return (
<>
{/* Material Symbols: loaded in root layout <head> (not here — body <link> breaks layout/CSS ordering in some browsers). */}
{/* Sidebar */}
<aside
className={`hidden md:flex flex-col h-screen fixed left-0 top-0 bg-ql-surface-low border-r border-ql-outline-variant/15 py-6 z-50 overflow-x-hidden overflow-y-auto transition-[width] duration-200 ease-out ${
sidebarCollapsed ? "w-16" : "w-64"
}`}
>
<div
className={`mb-6 shrink-0 flex gap-2 ${
sidebarCollapsed ? "flex-col items-center px-2" : "items-start justify-between px-6"
}`}
>
<Link
href="/dashboard"
className={`block no-underline text-inherit hover:opacity-95 transition-opacity min-w-0 ${
sidebarCollapsed ? "text-center" : ""
}`}
title={sidebarCollapsed ? "Quantum Ledger · Dashboard" : undefined}
>
{sidebarCollapsed ? (
<span className="font-headline text-ql-primary text-lg font-bold tracking-tighter block">
QL
</span>
) : (
<>
<h1 className="font-headline text-ql-primary text-lg font-bold tracking-tighter">
Quantum Ledger
</h1>
<p className="text-ql-on-surface-variant text-[10px] uppercase tracking-widest mt-1">
v1.0 Active
</p>
</>
)}
</Link>
<button
type="button"
onClick={toggleSidebar}
aria-expanded={!sidebarCollapsed}
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
className={`shrink-0 flex items-center justify-center rounded-lg p-1.5 text-ql-on-surface-variant hover:bg-ql-surface-container hover:text-ql-on-surface transition-colors ${
sidebarCollapsed ? "w-full" : ""
}`}
title={sidebarCollapsed ? "Expand sidebar" : "Collapse to icons"}
>
<span className="material-symbols-outlined text-xl">
{sidebarCollapsed ? "chevron_right" : "chevron_left"}
</span>
</button>
</div>
<nav className={`flex-1 space-y-1 min-h-0 ${sidebarCollapsed ? "px-1.5" : "px-3"}`}>
{NAV_ITEMS.map((item) => (
<NavItem
key={item.href}
href={item.href}
label={item.label}
icon={item.icon}
active={pathname.startsWith(item.href)}
collapsed={sidebarCollapsed}
/>
))}
</nav>
<div className={`mt-auto space-y-3 shrink-0 ${sidebarCollapsed ? "px-1.5" : "px-6"}`}>
<button
type="button"
onClick={cycle}
className={`flex items-center rounded-lg text-xs font-bold text-ql-on-surface-variant hover:bg-ql-surface-container transition-colors ${
sidebarCollapsed
? "justify-center w-full p-2"
: "gap-2 w-full px-3 py-2"
}`}
title={`Theme: ${themeMeta.label}. Click to cycle.`}
>
<span className="material-symbols-outlined text-base shrink-0">
{themeMeta.icon}
</span>
{!sidebarCollapsed ? themeMeta.label : null}
</button>
{sidebarCollapsed ? (
<div
className="flex justify-center rounded-lg bg-ql-surface-container/60 border border-ql-outline-variant/10 p-2"
title={sessionTooltip}
>
<span className="material-symbols-outlined text-ql-on-surface-variant text-xl">
insights
</span>
</div>
) : (
<div className="rounded-lg bg-ql-surface-container/60 border border-ql-outline-variant/10 px-3 py-2.5 space-y-1">
<div className="flex items-center justify-between">
<span className="text-[9px] text-slate-500 uppercase tracking-widest font-bold">
Session
</span>
{lo ? (
<span className="flex items-center gap-1 text-[9px] text-ql-tertiary font-bold">
<span className="w-1.5 h-1.5 rounded-full bg-ql-tertiary" />
Last run
</span>
) : null}
</div>
<p className="text-xs font-mono text-ql-on-surface truncate" title={lo ? lo.at : undefined}>
{objectiveLabel(displayObjective)}
</p>
<p className="text-[10px] text-slate-500">
{displayTickerCount} ticker{displayTickerCount === 1 ? "" : "s"}
{lo ? (
<span className="text-slate-600">
{" "}
·{" "}
{new Date(lo.at).toLocaleString([], {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</span>
) : null}
</p>
{currentDiffersFromLast ? (
<p className="text-[9px] text-slate-500 leading-snug pt-0.5 border-t border-ql-outline-variant/10 mt-1">
Current: {objectiveLabel(session.objective)} ·{" "}
{session.tickers.length} ticker
{session.tickers.length === 1 ? "" : "s"}
</p>
) : null}
</div>
)}
<Link
href="/portfolio"
title="Portfolio Lab"
className={`flex items-center justify-center w-full no-underline primary-gradient text-[#001D33] font-bold rounded-lg transition-all active:scale-[0.98] shadow-lg shadow-ql-primary/10 ${
sidebarCollapsed ? "py-2.5 px-0" : "gap-1.5 py-3 text-sm"
}`}
>
<span className="material-symbols-outlined text-lg shrink-0">science</span>
{!sidebarCollapsed ? <span>PL</span> : null}
</Link>
<a
href="/api/docs/openapi"
target="_blank"
rel="noopener"
title="API Docs (OpenAPI)"
className={`flex items-center text-ql-on-surface-variant hover:text-ql-on-surface transition-colors no-underline ${
sidebarCollapsed ? "justify-center mt-2 p-1" : "gap-3 text-xs mt-4"
}`}
>
<span className="material-symbols-outlined text-sm shrink-0">code</span>
{!sidebarCollapsed ? <span>API Docs</span> : null}
</a>
</div>
</aside>
{/* Mobile top bar */}
<header className="app-shell-mobile-header md:hidden bg-ql-surface flex justify-between items-center px-4 h-14 sticky top-0 z-40 border-b border-ql-outline-variant/10">
<Link
href="/dashboard"
className="font-headline text-ql-primary text-xl font-bold tracking-tighter no-underline"
>
QL
</Link>
</header>
{/* Main — light mode negative space: see globals.css html.light .app-shell-main */}
<main
data-sidebar={sidebarCollapsed ? "collapsed" : "expanded"}
className={`app-shell-main min-h-screen bg-ql-surface text-ql-on-surface pb-24 md:pb-8 transition-[margin] duration-200 ease-out ${
sidebarCollapsed ? "md:ml-16" : "md:ml-64"
}`}
>
{children}
</main>
{/* Mobile bottom nav */}
<nav className="app-shell-mobile-nav md:hidden fixed bottom-0 left-0 w-full bg-ql-surface-container flex justify-around items-center h-16 border-t border-ql-outline-variant/10 z-50">
{NAV_ITEMS.slice(0, 4).map((item) => (
<Link
key={item.href}
href={item.href}
className={`flex flex-col items-center gap-1 no-underline ${
pathname.startsWith(item.href)
? "text-ql-primary"
: "text-ql-on-surface-variant"
}`}
>
<span className="material-symbols-outlined">{item.icon}</span>
<span className="text-[10px] font-bold">{item.label.split(" ")[0]}</span>
</Link>
))}
</nav>
</>
);
}