Spaces:
Running
Running
File size: 5,621 Bytes
a32aee9 a5b5bcd a32aee9 a5b5bcd a32aee9 a5b5bcd a32aee9 a5b5bcd a32aee9 f1fa34c a32aee9 a5b5bcd a32aee9 a5b5bcd f1fa34c a5b5bcd a32aee9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | import { useState, type ReactNode } from "react";
import { Database, Flask, List, SidebarSimple, SignOut, X } from "@phosphor-icons/react";
import { SidebarContent } from "./Sidebar";
import { useAuth } from "@/context/AuthContext";
import { useSettings } from "@/context/SettingsContext";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { cn } from "@/lib/utils";
/**
* Workspace shell: a floating glass island nav on top, the thread in the
* centre, and the source/ingest rail on the right. The rail is collapsible on
* desktop (state persists across reloads) and becomes the off-canvas drawer
* below `lg`.
*/
export function AppShell({ nav, children }: { nav?: ReactNode; children: ReactNode }) {
const [open, setOpen] = useState(false);
const [railOpen, setRailOpen] = useLocalStorage<boolean>("rr_rail_open", true);
const { session, logout } = useAuth();
const { kbOnly, setKbOnly } = useSettings();
return (
<div className="relative flex h-[100dvh] flex-col overflow-hidden bg-zinc-950">
{/* First tab stop: lets keyboard users bypass the nav and the rail. */}
<a href="#main-content" className="skip-link">
Skip to main content
</a>
{/* Ambient wash — single accent hue, no purple. */}
<div className="pointer-events-none fixed inset-0 z-0" aria-hidden>
<div className="absolute left-[8%] top-[-14%] h-[520px] w-[720px] rounded-full bg-accent/[0.07] blur-[140px]" />
<div className="absolute bottom-[-18%] right-[2%] h-[420px] w-[560px] rounded-full bg-accent/[0.03] blur-[150px]" />
</div>
<div className="grain" aria-hidden />
{/* Floating island nav */}
<header className="relative z-30 flex shrink-0 justify-center px-4 pt-5">
<div className="island w-full max-w-3xl lg:w-auto lg:max-w-none">
<button
onClick={() => setOpen(true)}
className="icon-btn lg:hidden"
aria-label="Open menu — views, scope and sources"
>
<List className="h-4 w-4" />
</button>
<div className="flex items-center gap-2 pr-3 lg:border-r lg:border-white/[0.07]">
<Flask className="h-4 w-4 text-accent" />
<span className="text-[13.5px] font-medium tracking-tight text-white">ResearchRAG</span>
</div>
{nav && <nav className="hidden gap-1 sm:flex">{nav}</nav>}
<div className="ml-auto flex items-center gap-2 lg:ml-0 lg:border-l lg:border-white/[0.07] lg:pl-3">
<button
onClick={() => setKbOnly(!kbOnly)}
className={cn("pill hidden sm:flex", kbOnly && "pill-active")}
title={
kbOnly
? "Answers use your ingested papers only"
: "Falls back to live OpenAlex / web when the KB seems irrelevant"
}
>
<Database className="h-3.5 w-3.5" />
{kbOnly ? "Library only" : "Library + web"}
</button>
{/* Desktop-only: below `lg` the rail is the drawer, which has its
own trigger on the left of the island. */}
<button
onClick={() => setRailOpen(!railOpen)}
className={cn("icon-btn hidden lg:grid", railOpen && "text-accent")}
title={railOpen ? "Hide sources rail" : "Show sources rail"}
aria-label={railOpen ? "Hide sources rail" : "Show sources rail"}
aria-expanded={railOpen}
>
<SidebarSimple className="h-4 w-4" />
</button>
<button
onClick={logout}
className="icon-btn"
title={session ? `Sign out ${session.displayName}` : "Sign out"}
aria-label="Sign out"
>
<SignOut className="h-4 w-4" />
</button>
</div>
</div>
</header>
{/* Thread + rail — full bleed, so the rail stays flush with the right
edge. Collapsing it hands the whole width back to the thread. */}
<div
className={cn(
"relative z-10 grid min-h-0 flex-1 gap-6 px-4 pb-4 pt-1 lg:px-6",
railOpen && "lg:grid-cols-[minmax(0,1fr)_clamp(288px,26vw,352px)]",
)}
>
<main id="main-content" tabIndex={-1} className="min-h-0 min-w-0 focus:outline-none">
{children}
</main>
<aside
className={cn(
"hidden min-h-0 overflow-y-auto scrollbar-thin pb-2",
railOpen && "rail-in lg:block",
)}
>
<SidebarContent />
</aside>
</div>
{/* Mobile drawer */}
{open && (
<div className="fixed inset-0 z-40 lg:hidden">
<div
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
onClick={() => setOpen(false)}
/>
<aside className="absolute right-0 top-0 h-full w-[22rem] max-w-[88vw] overflow-y-auto scrollbar-thin border-l border-white/[0.07] bg-zinc-950 p-3 shadow-2xl animate-fade-up">
<button
onClick={() => setOpen(false)}
className="icon-btn absolute right-4 top-4 z-10"
aria-label="Close panel"
>
<X className="h-4 w-4" />
</button>
{/* The island nav hides below `sm`, so the drawer carries the view
switcher and the library-scope toggle on phones. */}
<SidebarContent showIdentity nav={nav} onNavigate={() => setOpen(false)} />
</aside>
</div>
)}
</div>
);
}
|