import { Link, Outlet, useLocation } from "@tanstack/react-router";import { useEffect, useRef, useState } from "react";import { useAtomValue, useAtomSet } from "@effect/atom-react";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import * as Exit from "effect/Exit";import { sourcesOptimisticAtom } from "@executor-js/react/api/atoms";import { useScope } from "@executor-js/react/api/scope-context";import { Button } from "@executor-js/react/components/button";import { Skeleton } from "@executor-js/react/components/skeleton";import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,} from "@executor-js/react/components/dialog";import { SupportOptions } from "./components/support-options";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,} from "@executor-js/react/components/dropdown-menu";import { SourceFavicon } from "@executor-js/react/components/source-favicon";import { CommandPalette } from "@executor-js/react/components/command-palette";import { authWriteKeys } from "@executor-js/react/api/reactivity-keys";import { AUTH_PATHS } from "../auth/api";import { organizationsAtom, switchOrganization, useAuth } from "./auth";import { CreateOrganizationFields, useCreateOrganizationForm,} from "./components/create-organization-form"; // ── Brand ──────────────────────────────────────────────────────────────── function Brand(props: { onNavigate?: () => void }) { return ( <Link to="/" onClick={props.onNavigate} className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground">executor</span> <span className="rounded-sm bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-primary"> Beta </span> </Link> );} // ── NavItem ────────────────────────────────────────────────────────────── function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) { return ( <Link to={props.to} onClick={props.onNavigate} className={[ "flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors", props.active ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > {props.label} </Link> );} // ── SourceList ─────────────────────────────────────────────────────────── function SourceList(props: { pathname: string; onNavigate?: () => void }) { const scopeId = useScope(); const sources = useAtomValue(sourcesOptimisticAtom(scopeId)); return AsyncResult.match(sources, { onInitial: () => ( <div className="flex flex-col gap-1 px-2.5 py-1"> {[80, 65, 72, 58, 68].map((w, i) => ( <div key={i} className="flex items-center gap-2 rounded-md py-1.5"> <Skeleton className="size-3.5 shrink-0 rounded" /> <Skeleton className="h-3" style={{ width: `${w}%` }} /> </div> ))} </div> ), onFailure: () => ( <div className="px-2.5 py-2 text-xs text-muted-foreground">No sources yet</div> ), onSuccess: ({ value }) => value.length === 0 ? ( <div className="px-2.5 py-2 text-sm leading-relaxed text-muted-foreground"> No sources yet </div> ) : ( <div className="flex flex-col gap-px"> {value.map((s) => { const detailPath = `/sources/${s.id}`; const active = props.pathname === detailPath || props.pathname.startsWith(`${detailPath}/`); return ( <Link key={s.id} to="/sources/$namespace" params={{ namespace: s.id }} onClick={props.onNavigate} className={[ "group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-xs transition-colors", active ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > <SourceFavicon url={s.url} /> <span className="flex-1 truncate">{s.name}</span> <span className="rounded bg-secondary/50 px-1 py-px text-xs font-medium text-muted-foreground"> {s.kind} </span> </Link> ); })} </div> ), });} // ── UserFooter ────────────────────────────────────────────────────────── function initialsFor(name: string | null, email: string) { if (name) { return name .split(" ") .map((n) => n[0]) .join("") .slice(0, 2) .toUpperCase(); } return email[0]!.toUpperCase();} function Avatar(props: { url: string | null; name: string | null; email: string; size?: "sm" | "md";}) { const size = props.size === "md" ? "size-8" : "size-7"; const text = props.size === "md" ? "text-sm" : "text-xs"; if (props.url) { return <img src={props.url} alt="" className={`${size} shrink-0 rounded-full`} />; } return ( <div className={`flex ${size} shrink-0 items-center justify-center rounded-full bg-primary/10 ${text} font-semibold text-primary`} > {initialsFor(props.name, props.email)} </div> );} function OrganizationSwitcherItems(props: { activeOrganizationId: string | null }) { const organizations = useAtomValue(organizationsAtom); const doSwitchOrganization = useAtomSet(switchOrganization, { mode: "promiseExit" }); const handleSwitch = async (organizationId: string) => { if (organizationId === props.activeOrganizationId) return; const exit = await doSwitchOrganization({ payload: { organizationId }, reactivityKeys: authWriteKeys, }); if (Exit.isSuccess(exit)) window.location.reload(); }; return AsyncResult.match(organizations, { onInitial: () => <DropdownMenuItem disabled>Loading…</DropdownMenuItem>, onFailure: () => <DropdownMenuItem disabled>Failed to load organizations</DropdownMenuItem>, onSuccess: ({ value }) => value.organizations.length === 0 ? ( <DropdownMenuItem disabled>No organizations</DropdownMenuItem> ) : ( <> {value.organizations.map((organization: { id: string; name: string }) => { const isActive = organization.id === props.activeOrganizationId; return ( <DropdownMenuItem key={organization.id} disabled={isActive} onClick={() => handleSwitch(organization.id)} className="text-xs" > <span className="min-w-0 flex-1 truncate">{organization.name}</span> {isActive && <CheckIcon />} </DropdownMenuItem> ); })} </> ), });} function CheckIcon() { return ( <svg viewBox="0 0 16 16" fill="none" className="ml-auto size-3 text-muted-foreground"> <path d="M3.5 8.5L6.5 11.5L12.5 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );} function UserFooter() { const auth = useAuth(); const [createOrganizationOpen, setCreateOrganizationOpen] = useState(false); const suggestedOrganizationName = auth.status === "authenticated" && auth.user.name?.trim() !== "" && auth.user.name != null ? `${auth.user.name}'s Organization` : "New Organization"; const form = useCreateOrganizationForm({ defaultName: suggestedOrganizationName, onSuccess: () => window.location.reload(), }); if (auth.status !== "authenticated") return null; const openCreateOrganization = () => { form.reset(suggestedOrganizationName); setCreateOrganizationOpen(true); }; return ( <div className="shrink-0 border-t border-sidebar-border px-3 py-2.5"> <Dialog open={createOrganizationOpen} onOpenChange={(open) => { setCreateOrganizationOpen(open); if (!open) form.reset(suggestedOrganizationName); }} > <DropdownMenu> <DropdownMenuTrigger asChild> <Button type="button" variant="ghost" className="flex h-auto w-full items-center justify-start gap-2.5 rounded-md px-1 py-1 text-left hover:bg-sidebar-active/60" > <Avatar url={auth.user.avatarUrl} name={auth.user.name} email={auth.user.email} /> <div className="min-w-0 flex-1"> <p className="truncate text-xs font-medium text-foreground"> {auth.user.name ?? auth.user.email} </p> {auth.organization && ( <p className="truncate text-xs text-muted-foreground">{auth.organization.name}</p> )} </div> <svg viewBox="0 0 16 16" fill="none" className="size-3.5 shrink-0 text-muted-foreground" > <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" /> </svg> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="start" side="top" className="w-64"> <DropdownMenuLabel className="text-xs font-normal text-muted-foreground"> Organization </DropdownMenuLabel> <DropdownMenuSub> <DropdownMenuSubTrigger className="text-xs"> <span className="min-w-0 flex-1 truncate"> {auth.organization?.name ?? "No organization"} </span> </DropdownMenuSubTrigger> <DropdownMenuSubContent className="w-56"> <OrganizationSwitcherItems activeOrganizationId={auth.organization?.id ?? null} /> <DropdownMenuSeparator /> <DropdownMenuItem className="text-xs" onSelect={(event) => { event.preventDefault(); openCreateOrganization(); }} > Create organization </DropdownMenuItem> </DropdownMenuSubContent> </DropdownMenuSub> <DropdownMenuSeparator /> <DropdownMenuItem asChild className="text-xs"> <Link to="/api-keys">API keys</Link> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuLabel className="text-xs font-normal text-muted-foreground"> Signed in as </DropdownMenuLabel> <DropdownMenuItem disabled className="gap-2 text-xs opacity-100"> <Avatar url={auth.user.avatarUrl} name={auth.user.name} email={auth.user.email} /> <div className="min-w-0 flex-1"> <p className="truncate font-medium text-foreground"> {auth.user.name ?? auth.user.email} </p> {auth.user.name && ( <p className="truncate text-muted-foreground">{auth.user.email}</p> )} </div> </DropdownMenuItem> <DropdownMenuItem className="text-xs text-destructive focus:text-destructive" onClick={async () => { await fetch(AUTH_PATHS.logout, { method: "POST" }); window.location.href = "/"; }} > Sign out </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> <DialogContent className="sm:max-w-[420px]"> <DialogHeader> <DialogTitle className="font-display text-xl">Create organization</DialogTitle> <DialogDescription className="text-sm leading-relaxed"> Add another organization under your current account and switch into it immediately. </DialogDescription> </DialogHeader> <CreateOrganizationFields name={form.name} onNameChange={(name) => { form.setName(name); if (form.error) form.setError(null); }} error={form.error} onSubmit={() => void form.submit()} /> <DialogFooter> <DialogClose asChild> <Button variant="ghost" size="sm" disabled={form.creating}> Cancel </Button> </DialogClose> <Button size="sm" onClick={() => void form.submit()} disabled={!form.canSubmit || form.creating} > {form.creating ? "Creating…" : "Create organization"} </Button> </DialogFooter> </DialogContent> </Dialog> </div> );} // ── SupportButton ──────────────────────────────────────────────────────── function HelpIcon({ className }: { className?: string }) { return ( <svg viewBox="0 0 16 16" fill="none" className={className} aria-hidden> <circle cx="8" cy="8" r="6.5" stroke="currentColor" strokeWidth="1.3" /> <path d="M6.25 6.25c.25-1 1-1.5 1.85-1.5 1 0 1.9.7 1.9 1.7 0 .8-.5 1.2-1.1 1.6-.55.4-.9.7-.9 1.45M8 11.25v.05" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" /> </svg> );} function SupportButton() { const [open, setOpen] = useState(false); return ( <Dialog open={open} onOpenChange={setOpen}> <Button type="button" variant="ghost" onClick={() => setOpen(true)} className="flex h-auto w-full items-center justify-start gap-2.5 rounded-md px-2.5 py-1.5 text-sm font-normal text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground" > <HelpIcon className="size-3.5 text-muted-foreground" /> Get support </Button> <DialogContent className="sm:max-w-[440px]"> <DialogHeader> <DialogTitle className="font-display text-xl">Get support</DialogTitle> <DialogDescription className="text-sm leading-relaxed"> Reach out through any of the channels below. </DialogDescription> </DialogHeader> <div className="py-2"> <SupportOptions /> </div> </DialogContent> </Dialog> );} // ── SidebarContent ─────────────────────────────────────────────────────── function SidebarContent(props: { pathname: string; onNavigate?: () => void; showBrand?: boolean }) { const isHome = props.pathname === "/"; const isSecrets = props.pathname === "/secrets"; const isConnections = props.pathname === "/connections"; const isPolicies = props.pathname === "/policies"; const isBilling = props.pathname === "/billing" || props.pathname.startsWith("/billing/"); const isOrg = props.pathname === "/org"; return ( <> {props.showBrand !== false && ( <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4"> <Brand onNavigate={props.onNavigate} /> </div> )} <nav className="flex flex-1 flex-col overflow-y-auto p-2"> <NavItem to="/" label="Sources" active={isHome} onNavigate={props.onNavigate} /> <NavItem to="/connections" label="Connections" active={isConnections} onNavigate={props.onNavigate} /> <NavItem to="/secrets" label="Secrets" active={isSecrets} onNavigate={props.onNavigate} /> <NavItem to="/policies" label="Policies" active={isPolicies} onNavigate={props.onNavigate} /> <NavItem to="/org" label="Organization" active={isOrg} onNavigate={props.onNavigate} /> <NavItem to="/billing" label="Billing" active={isBilling} onNavigate={props.onNavigate} /> <div className="mt-5 mb-1 px-2.5 text-xs font-medium uppercase tracking-widest text-muted-foreground"> <span>Sources</span> </div> <SourceList pathname={props.pathname} onNavigate={props.onNavigate} /> </nav> <div className="shrink-0 px-2 pb-2"> <SupportButton /> </div> <UserFooter /> </> );} // ── Shell ───────────────────────────────────────────────────────────────── export function Shell() { const location = useLocation(); const pathname = location.pathname; const lastPathname = useRef(pathname); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); if (lastPathname.current !== pathname) { lastPathname.current = pathname; if (mobileSidebarOpen) setMobileSidebarOpen(false); } // Lock scroll when mobile sidebar open useEffect(() => { if (!mobileSidebarOpen) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [mobileSidebarOpen]); return ( <div className="flex h-screen overflow-hidden"> <CommandPalette /> {/* Desktop sidebar */} <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56"> <SidebarContent pathname={pathname} /> </aside> {/* Mobile sidebar overlay */} {mobileSidebarOpen && ( <div className="fixed inset-0 z-50 flex md:hidden">Lines 1–500 of 567. Load more
import { Link, Outlet, useLocation } from "@tanstack/react-router"; import { useEffect, useRef, useState } from "react"; import { useAtomValue, useAtomSet } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import * as Exit from "effect/Exit"; import { sourcesOptimisticAtom } from "@executor-js/react/api/atoms"; import { useScope } from "@executor-js/react/api/scope-context"; import { Button } from "@executor-js/react/components/button"; import { Skeleton } from "@executor-js/react/components/skeleton"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@executor-js/react/components/dialog"; import { SupportOptions } from "./components/support-options"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@executor-js/react/components/dropdown-menu"; import { SourceFavicon } from "@executor-js/react/components/source-favicon"; import { CommandPalette } from "@executor-js/react/components/command-palette"; import { authWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { AUTH_PATHS } from "../auth/api"; import { organizationsAtom, switchOrganization, useAuth } from "./auth"; import { CreateOrganizationFields, useCreateOrganizationForm, } from "./components/create-organization-form"; // ── Brand ──────────────────────────────────────────────────────────────── function Brand(props: { onNavigate?: () => void }) { return ( <Link to="/" onClick={props.onNavigate} className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground">executor</span> <span className="rounded-sm bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-primary"> Beta </span> </Link> ); } // ── NavItem ────────────────────────────────────────────────────────────── function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) { return ( <Link to={props.to} onClick={props.onNavigate} className={[ "flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors", props.active ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > {props.label} </Link> ); } // ── SourceList ─────────────────────────────────────────────────────────── function SourceList(props: { pathname: string; onNavigate?: () => void }) { const scopeId = useScope(); const sources = useAtomValue(sourcesOptimisticAtom(scopeId)); return AsyncResult.match(sources, { onInitial: () => ( <div className="flex flex-col gap-1 px-2.5 py-1"> {[80, 65, 72, 58, 68].map((w, i) => ( <div key={i} className="flex items-center gap-2 rounded-md py-1.5"> <Skeleton className="size-3.5 shrink-0 rounded" /> <Skeleton className="h-3" style={{ width: `${w}%` }} /> </div> ))} </div> ), onFailure: () => ( <div className="px-2.5 py-2 text-xs text-muted-foreground">No sources yet</div> ), onSuccess: ({ value }) => value.length === 0 ? ( <div className="px-2.5 py-2 text-sm leading-relaxed text-muted-foreground"> No sources yet </div> ) : ( <div className="flex flex-col gap-px"> {value.map((s) => { const detailPath = `/sources/${s.id}`; const active = props.pathname === detailPath || props.pathname.startsWith(`${detailPath}/`); return ( <Link key={s.id} to="/sources/$namespace" params={{ namespace: s.id }} onClick={props.onNavigate} className={[ "group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-xs transition-colors", active ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > <SourceFavicon url={s.url} /> <span className="flex-1 truncate">{s.name}</span> <span className="rounded bg-secondary/50 px-1 py-px text-xs font-medium text-muted-foreground"> {s.kind} </span> </Link> ); })} </div> ), }); } // ── UserFooter ────────────────────────────────────────────────────────── function initialsFor(name: string | null, email: string) { if (name) { return name .split(" ") .map((n) => n[0]) .join("") .slice(0, 2) .toUpperCase(); } return email[0]!.toUpperCase(); } function Avatar(props: { url: string | null; name: string | null; email: string; size?: "sm" | "md"; }) { const size = props.size === "md" ? "size-8" : "size-7"; const text = props.size === "md" ? "text-sm" : "text-xs"; if (props.url) { return <img src={props.url} alt="" className={`${size} shrink-0 rounded-full`} />; } return ( <div className={`flex ${size} shrink-0 items-center justify-center rounded-full bg-primary/10 ${text} font-semibold text-primary`} > {initialsFor(props.name, props.email)} </div> ); } function OrganizationSwitcherItems(props: { activeOrganizationId: string | null }) { const organizations = useAtomValue(organizationsAtom); const doSwitchOrganization = useAtomSet(switchOrganization, { mode: "promiseExit" }); const handleSwitch = async (organizationId: string) => { if (organizationId === props.activeOrganizationId) return; const exit = await doSwitchOrganization({ payload: { organizationId }, reactivityKeys: authWriteKeys, }); if (Exit.isSuccess(exit)) window.location.reload(); }; return AsyncResult.match(organizations, { onInitial: () => <DropdownMenuItem disabled>Loading…</DropdownMenuItem>, onFailure: () => <DropdownMenuItem disabled>Failed to load organizations</DropdownMenuItem>, onSuccess: ({ value }) => value.organizations.length === 0 ? ( <DropdownMenuItem disabled>No organizations</DropdownMenuItem> ) : ( <> {value.organizations.map((organization: { id: string; name: string }) => { const isActive = organization.id === props.activeOrganizationId; return ( <DropdownMenuItem key={organization.id} disabled={isActive} onClick={() => handleSwitch(organization.id)} className="text-xs" > <span className="min-w-0 flex-1 truncate">{organization.name}</span> {isActive && <CheckIcon />} </DropdownMenuItem> ); })} </> ), }); } function CheckIcon() { return ( <svg viewBox="0 0 16 16" fill="none" className="ml-auto size-3 text-muted-foreground"> <path d="M3.5 8.5L6.5 11.5L12.5 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> ); } function UserFooter() { const auth = useAuth(); const [createOrganizationOpen, setCreateOrganizationOpen] = useState(false); const suggestedOrganizationName = auth.status === "authenticated" && auth.user.name?.trim() !== "" && auth.user.name != null ? `${auth.user.name}'s Organization` : "New Organization"; const form = useCreateOrganizationForm({ defaultName: suggestedOrganizationName, onSuccess: () => window.location.reload(), }); if (auth.status !== "authenticated") return null; const openCreateOrganization = () => { form.reset(suggestedOrganizationName); setCreateOrganizationOpen(true); }; return ( <div className="shrink-0 border-t border-sidebar-border px-3 py-2.5"> <Dialog open={createOrganizationOpen} onOpenChange={(open) => { setCreateOrganizationOpen(open); if (!open) form.reset(suggestedOrganizationName); }} > <DropdownMenu> <DropdownMenuTrigger asChild> <Button type="button" variant="ghost" className="flex h-auto w-full items-center justify-start gap-2.5 rounded-md px-1 py-1 text-left hover:bg-sidebar-active/60" > <Avatar url={auth.user.avatarUrl} name={auth.user.name} email={auth.user.email} /> <div className="min-w-0 flex-1"> <p className="truncate text-xs font-medium text-foreground"> {auth.user.name ?? auth.user.email} </p> {auth.organization && ( <p className="truncate text-xs text-muted-foreground">{auth.organization.name}</p> )} </div> <svg viewBox="0 0 16 16" fill="none" className="size-3.5 shrink-0 text-muted-foreground" > <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" /> </svg> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="start" side="top" className="w-64"> <DropdownMenuLabel className="text-xs font-normal text-muted-foreground"> Organization </DropdownMenuLabel> <DropdownMenuSub> <DropdownMenuSubTrigger className="text-xs"> <span className="min-w-0 flex-1 truncate"> {auth.organization?.name ?? "No organization"} </span> </DropdownMenuSubTrigger> <DropdownMenuSubContent className="w-56"> <OrganizationSwitcherItems activeOrganizationId={auth.organization?.id ?? null} /> <DropdownMenuSeparator /> <DropdownMenuItem className="text-xs" onSelect={(event) => { event.preventDefault(); openCreateOrganization(); }} > Create organization </DropdownMenuItem> </DropdownMenuSubContent> </DropdownMenuSub> <DropdownMenuSeparator /> <DropdownMenuItem asChild className="text-xs"> <Link to="/api-keys">API keys</Link> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuLabel className="text-xs font-normal text-muted-foreground"> Signed in as </DropdownMenuLabel> <DropdownMenuItem disabled className="gap-2 text-xs opacity-100"> <Avatar url={auth.user.avatarUrl} name={auth.user.name} email={auth.user.email} /> <div className="min-w-0 flex-1"> <p className="truncate font-medium text-foreground"> {auth.user.name ?? auth.user.email} </p> {auth.user.name && ( <p className="truncate text-muted-foreground">{auth.user.email}</p> )} </div> </DropdownMenuItem> <DropdownMenuItem className="text-xs text-destructive focus:text-destructive" onClick={async () => { await fetch(AUTH_PATHS.logout, { method: "POST" }); window.location.href = "/"; }} > Sign out </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> <DialogContent className="sm:max-w-[420px]"> <DialogHeader> <DialogTitle className="font-display text-xl">Create organization</DialogTitle> <DialogDescription className="text-sm leading-relaxed"> Add another organization under your current account and switch into it immediately. </DialogDescription> </DialogHeader> <CreateOrganizationFields name={form.name} onNameChange={(name) => { form.setName(name); if (form.error) form.setError(null); }} error={form.error} onSubmit={() => void form.submit()} /> <DialogFooter> <DialogClose asChild> <Button variant="ghost" size="sm" disabled={form.creating}> Cancel </Button> </DialogClose> <Button size="sm" onClick={() => void form.submit()} disabled={!form.canSubmit || form.creating} > {form.creating ? "Creating…" : "Create organization"} </Button> </DialogFooter> </DialogContent> </Dialog> </div> ); } // ── SupportButton ──────────────────────────────────────────────────────── function HelpIcon({ className }: { className?: string }) { return ( <svg viewBox="0 0 16 16" fill="none" className={className} aria-hidden> <circle cx="8" cy="8" r="6.5" stroke="currentColor" strokeWidth="1.3" /> <path d="M6.25 6.25c.25-1 1-1.5 1.85-1.5 1 0 1.9.7 1.9 1.7 0 .8-.5 1.2-1.1 1.6-.55.4-.9.7-.9 1.45M8 11.25v.05" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" /> </svg> ); } function SupportButton() { const [open, setOpen] = useState(false); return ( <Dialog open={open} onOpenChange={setOpen}> <Button type="button" variant="ghost" onClick={() => setOpen(true)} className="flex h-auto w-full items-center justify-start gap-2.5 rounded-md px-2.5 py-1.5 text-sm font-normal text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground" > <HelpIcon className="size-3.5 text-muted-foreground" /> Get support </Button> <DialogContent className="sm:max-w-[440px]"> <DialogHeader> <DialogTitle className="font-display text-xl">Get support</DialogTitle> <DialogDescription className="text-sm leading-relaxed"> Reach out through any of the channels below. </DialogDescription> </DialogHeader> <div className="py-2"> <SupportOptions /> </div> </DialogContent> </Dialog> ); } // ── SidebarContent ─────────────────────────────────────────────────────── function SidebarContent(props: { pathname: string; onNavigate?: () => void; showBrand?: boolean }) { const isHome = props.pathname === "/"; const isSecrets = props.pathname === "/secrets"; const isConnections = props.pathname === "/connections"; const isPolicies = props.pathname === "/policies"; const isBilling = props.pathname === "/billing" || props.pathname.startsWith("/billing/"); const isOrg = props.pathname === "/org"; return ( <> {props.showBrand !== false && ( <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4"> <Brand onNavigate={props.onNavigate} /> </div> )} <nav className="flex flex-1 flex-col overflow-y-auto p-2"> <NavItem to="/" label="Sources" active={isHome} onNavigate={props.onNavigate} /> <NavItem to="/connections" label="Connections" active={isConnections} onNavigate={props.onNavigate} /> <NavItem to="/secrets" label="Secrets" active={isSecrets} onNavigate={props.onNavigate} /> <NavItem to="/policies" label="Policies" active={isPolicies} onNavigate={props.onNavigate} /> <NavItem to="/org" label="Organization" active={isOrg} onNavigate={props.onNavigate} /> <NavItem to="/billing" label="Billing" active={isBilling} onNavigate={props.onNavigate} /> <div className="mt-5 mb-1 px-2.5 text-xs font-medium uppercase tracking-widest text-muted-foreground"> <span>Sources</span> </div> <SourceList pathname={props.pathname} onNavigate={props.onNavigate} /> </nav> <div className="shrink-0 px-2 pb-2"> <SupportButton /> </div> <UserFooter /> </> ); } // ── Shell ───────────────────────────────────────────────────────────────── export function Shell() { const location = useLocation(); const pathname = location.pathname; const lastPathname = useRef(pathname); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); if (lastPathname.current !== pathname) { lastPathname.current = pathname; if (mobileSidebarOpen) setMobileSidebarOpen(false); } // Lock scroll when mobile sidebar open useEffect(() => { if (!mobileSidebarOpen) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [mobileSidebarOpen]); return ( <div className="flex h-screen overflow-hidden"> <CommandPalette /> {/* Desktop sidebar */} <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56"> <SidebarContent pathname={pathname} /> </aside> {/* Mobile sidebar overlay */} {mobileSidebarOpen && ( <div className="fixed inset-0 z-50 flex md:hidden">