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 (
executor
Beta
);
}
// ── NavItem ──────────────────────────────────────────────────────────────
function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) {
return (
{props.label}
);
}
// ── SourceList ───────────────────────────────────────────────────────────
function SourceList(props: { pathname: string; onNavigate?: () => void }) {
const scopeId = useScope();
const sources = useAtomValue(sourcesOptimisticAtom(scopeId));
return AsyncResult.match(sources, {
onInitial: () => (
{[80, 65, 72, 58, 68].map((w, i) => (
))}
),
onFailure: () => (
No sources yet
),
onSuccess: ({ value }) =>
value.length === 0 ? (
No sources yet
) : (
{value.map((s) => {
const detailPath = `/sources/${s.id}`;
const active =
props.pathname === detailPath || props.pathname.startsWith(`${detailPath}/`);
return (
{s.name}
{s.kind}
);
})}
),
});
}
// ── 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
;
}
return (
{initialsFor(props.name, props.email)}
);
}
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: () => Loading…,
onFailure: () => Failed to load organizations,
onSuccess: ({ value }) =>
value.organizations.length === 0 ? (
No organizations
) : (
<>
{value.organizations.map((organization: { id: string; name: string }) => {
const isActive = organization.id === props.activeOrganizationId;
return (
handleSwitch(organization.id)}
className="text-xs"
>
{organization.name}
{isActive && }
);
})}
>
),
});
}
function CheckIcon() {
return (
);
}
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 (
);
}
// ── SupportButton ────────────────────────────────────────────────────────
function HelpIcon({ className }: { className?: string }) {
return (
);
}
function SupportButton() {
const [open, setOpen] = useState(false);
return (
);
}
// ── 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 && (
)}
>
);
}
// ── 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 (
{/* Desktop sidebar */}
{/* Mobile sidebar overlay */}
{mobileSidebarOpen && (
{/* oxlint-disable-next-line react/forbid-elements */}
)}
{/* Main content */}
{/* Mobile top bar */}
setMobileSidebarOpen(true)}
className="bg-card hover:bg-accent/50"
>
);
}