import { Link, Outlet, useLocation, useMatchRoute } from "@tanstack/react-router";import { useCallback, useEffect, useState } from "react";import { useSources, type Source } from "@executor/react";import { Button, sourcePluginsIndexPath } from "@executor/react/plugins";import { cn } from "../lib/utils";import { IconPlus, IconCopy, IconCheck, IconClose, IconMenu } from "./icons";import { LoadableBlock } from "./loadable";import { SourceFavicon } from "./source-favicon";import { getSourceFrontendPaths, registeredFrontendPluginNavRoutes,} from "../plugins"; // ── Status dot color ───────────────────────────────────────────────────── const statusColor: Record<string, string> = { connected: "bg-primary", probing: "bg-amber-400", draft: "bg-muted-foreground/30", auth_required: "bg-amber-500", error: "bg-destructive",}; type AppMetaEnv = { readonly VITE_APP_VERSION: string; readonly VITE_GITHUB_URL: string;}; const { VITE_APP_VERSION, VITE_GITHUB_URL } = (import.meta as ImportMeta & { readonly env: AppMetaEnv;}).env; type UpdateChannel = "latest" | "beta"; const EXECUTOR_DIST_TAGS_PATH = "/v1/app/npm/dist-tags"; type ParsedVersion = { readonly major: number; readonly minor: number; readonly patch: number; readonly prerelease: ReadonlyArray<string | number> | null;}; const semverPattern = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/; const resolveUpdateChannel = (version: string): UpdateChannel => version.includes("-beta.") ? "beta" : "latest"; const parseVersion = (version: string): ParsedVersion | null => { const match = version.trim().match(semverPattern); if (!match?.groups) { return null; } return { major: Number(match.groups.major), minor: Number(match.groups.minor), patch: Number(match.groups.patch), prerelease: match.groups.prerelease ? match.groups.prerelease.split(".").map((identifier) => /^\d+$/.test(identifier) ? Number(identifier) : identifier, ) : null, };}; const comparePrereleaseIdentifiers = ( left: ReadonlyArray<string | number> | null, right: ReadonlyArray<string | number> | null,): number => { if (left === null && right === null) { return 0; } if (left === null) { return 1; } if (right === null) { return -1; } const length = Math.max(left.length, right.length); for (let index = 0; index < length; index += 1) { const leftIdentifier = left[index]; const rightIdentifier = right[index]; if (leftIdentifier === rightIdentifier) { continue; } if (leftIdentifier === undefined) { return -1; } if (rightIdentifier === undefined) { return 1; } if (typeof leftIdentifier === "number" && typeof rightIdentifier === "number") { return leftIdentifier < rightIdentifier ? -1 : 1; } if (typeof leftIdentifier === "number") { return -1; } if (typeof rightIdentifier === "number") { return 1; } return leftIdentifier < rightIdentifier ? -1 : 1; } return 0;}; const compareVersions = (left: string, right: string): number | null => { const leftVersion = parseVersion(left); const rightVersion = parseVersion(right); if (!leftVersion || !rightVersion) { return null; } if (leftVersion.major !== rightVersion.major) { return leftVersion.major < rightVersion.major ? -1 : 1; } if (leftVersion.minor !== rightVersion.minor) { return leftVersion.minor < rightVersion.minor ? -1 : 1; } if (leftVersion.patch !== rightVersion.patch) { return leftVersion.patch < rightVersion.patch ? -1 : 1; } return comparePrereleaseIdentifiers(leftVersion.prerelease, rightVersion.prerelease);}; // ── useLatestVersion ───────────────────────────────────────────────────── function useLatestVersion(currentVersion: string) { const channel = resolveUpdateChannel(currentVersion); const [latestVersion, setLatestVersion] = useState<string | null>(null); useEffect(() => { let cancelled = false; fetch(EXECUTOR_DIST_TAGS_PATH) .then(async (res) => { if (!res.ok) { throw new Error(`Failed to load dist tags: ${res.status}`); } return res.json() as Promise<Partial<Record<UpdateChannel, string>>>; }) .then((data: Partial<Record<UpdateChannel, string>>) => { if (!cancelled) { setLatestVersion(data[channel] ?? null); } }) .catch(() => {}); return () => { cancelled = true; }; }, [channel]); const updateAvailable = latestVersion !== null && compareVersions(currentVersion, latestVersion) === -1; return { latestVersion, updateAvailable, channel };} // ── UpdateCard ─────────────────────────────────────────────────────────── function UpdateCard(props: { latestVersion: string; channel: UpdateChannel }) { const command = `npm i -g executor@${props.channel}`; const [copied, setCopied] = useState(false); const handleCopy = useCallback(() => { void navigator.clipboard.writeText(command).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }, [command]); return ( <div className="mx-2 mb-2 rounded-xl border border-primary/25 bg-primary/[0.06] p-3"> <div className="flex items-center gap-2"> <div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/15"> <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary"> <path d="M8 3v7M5 7l3 3 3-3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /> <path d="M3 12h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /> </svg> </div> <div className="min-w-0"> <p className="text-[11px] font-semibold text-foreground">Update available</p> <p className="text-[10px] text-muted-foreground"> v{props.latestVersion} </p> </div> </div> <Button type="button" variant="outline" onClick={handleCopy} className="mt-2.5 flex w-full items-center justify-between gap-2 rounded-lg border-border/60 bg-background/50 px-2.5 py-1.5 text-left hover:bg-background/80" > <code className="truncate font-mono text-[10px] text-sidebar-foreground"> {command} </code> <span className="shrink-0 text-muted-foreground transition-colors group-hover:text-foreground"> {copied ? ( <IconCheck className="size-3 text-primary" /> ) : ( <IconCopy className="size-3" /> )} </span> </Button> </div> );} // ── AppShell ─────────────────────────────────────────────────────────────export function AppShell() { const sources = useSources(); const location = useLocation(); const matchRoute = useMatchRoute(); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const isHome = matchRoute({ to: "/" }); const isSecrets = matchRoute({ to: "/secrets" }); const isPolicies = matchRoute({ to: "/policies" }); const { latestVersion, updateAvailable, channel } = useLatestVersion(VITE_APP_VERSION); const mainPluginNavItems = registeredFrontendPluginNavRoutes.filter( ({ route }) => (route.nav?.section ?? "main") === "main", ); const sourcePluginNavItems = registeredFrontendPluginNavRoutes.filter( ({ route }) => route.nav?.section === "sources", ); useEffect(() => { setMobileSidebarOpen(false); }, [location.pathname]); useEffect(() => { if (!mobileSidebarOpen) { return; } const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = previousOverflow; }; }, [mobileSidebarOpen]); return ( <div className="flex h-screen overflow-hidden"> <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56"> <SidebarContent isHome={!!isHome} isSecrets={!!isSecrets} isPolicies={!!isPolicies} latestVersion={latestVersion} updateAvailable={updateAvailable} channel={channel} locationPathname={location.pathname} mainPluginNavItems={mainPluginNavItems} sourcePluginNavItems={sourcePluginNavItems} sources={sources} /> </aside> {mobileSidebarOpen && ( <div className="fixed inset-0 z-50 flex md:hidden"> <button type="button" aria-label="Close navigation" className="absolute inset-0 bg-black/45 backdrop-blur-[1px]" onClick={() => setMobileSidebarOpen(false)} /> <div className="relative flex h-full w-[84vw] max-w-xs flex-col border-r border-sidebar-border bg-sidebar shadow-2xl"> <div className="flex h-12 shrink-0 items-center justify-between border-b border-sidebar-border px-4"> <Link to="/" className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground"> executor </span> <span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary"> v3 </span> </Link> <Button type="button" variant="ghost" size="icon" aria-label="Close navigation" onClick={() => setMobileSidebarOpen(false)} className="size-8 text-sidebar-foreground hover:bg-sidebar-active hover:text-foreground" > <IconClose className="size-3.5" /> </Button> </div> <SidebarContent isHome={!!isHome} isSecrets={!!isSecrets} isPolicies={!!isPolicies} latestVersion={latestVersion} updateAvailable={updateAvailable} channel={channel} locationPathname={location.pathname} mainPluginNavItems={mainPluginNavItems} sourcePluginNavItems={sourcePluginNavItems} sources={sources} onNavigate={() => setMobileSidebarOpen(false)} showBrand={false} /> </div> </div> )} {/* Main content */} <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden"> <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden"> <Button type="button" variant="outline" size="icon" aria-label="Open navigation" onClick={() => setMobileSidebarOpen(true)} className="size-8 bg-card hover:bg-accent/50" > <IconMenu className="size-4" /> </Button> <Link to="/" className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground"> executor </span> <span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary"> v3 </span> </Link> <div className="w-8 shrink-0" /> </div> <Outlet /> </main> </div> );} function SidebarContent(props: { isHome: boolean; isSecrets: boolean; isPolicies: boolean; latestVersion: string | null; updateAvailable: boolean; channel: UpdateChannel; locationPathname: string; mainPluginNavItems: typeof registeredFrontendPluginNavRoutes; sourcePluginNavItems: typeof registeredFrontendPluginNavRoutes; sources: ReturnType<typeof useSources>; onNavigate?: () => void; showBrand?: boolean;}) { return ( <> {props.showBrand !== false && ( <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4"> <Link to="/" className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground"> executor </span> <span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary"> v3 </span> </Link> </div> )} <nav className="flex flex-1 flex-col overflow-y-auto p-2"> <NavItem to="/" label="Dashboard" active={props.isHome} onNavigate={props.onNavigate} /> <NavItem to="/secrets" label="Secrets" active={props.isSecrets} onNavigate={props.onNavigate} /> <NavItem to="/policies" label="Policies" active={props.isPolicies} onNavigate={props.onNavigate} /> {props.mainPluginNavItems.map(({ plugin, route, to }) => ( <NavItem key={`${plugin.key}:${route.key}`} to={to} label={route.nav?.label ?? route.key} active={ props.locationPathname === to || props.locationPathname.startsWith(`${to}/`) } onNavigate={props.onNavigate} /> ))} <div className="mt-5 mb-1 px-2.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50"> <div className="flex items-center justify-between gap-2"> <span>Sources</span> <Link to={sourcePluginsIndexPath} onClick={props.onNavigate} className="inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-[10px] font-medium normal-case tracking-normal text-primary transition-colors hover:bg-sidebar-active hover:text-foreground" > <IconPlus className="size-3" /> Add </Link> </div> </div> {props.sourcePluginNavItems.length > 0 && ( <div className="mb-2 flex flex-col gap-px"> {props.sourcePluginNavItems.map(({ plugin, route, to }) => ( <NavItem key={`${plugin.key}:${route.key}`} to={to} label={route.nav?.label ?? route.key} active={ props.locationPathname === to || props.locationPathname.startsWith(`${to}/`) } onNavigate={props.onNavigate} /> ))} </div> )} <LoadableBlock loadable={props.sources} loading="Loading..."> {(items) => !Array.isArray(items) ? ( <div className="px-2.5 py-2 text-[11px] leading-relaxed text-destructive"> Sources returned an unexpected payload. </div> ) : items.length === 0 ? ( <div className="px-2.5 py-2 text-[11px] leading-relaxed text-muted-foreground/40"> No sources yet </div> ) : ( <div className="flex flex-col gap-px"> {items.map((source) => ( <SourceItem key={source.id} pathname={props.locationPathname} source={source} onNavigate={props.onNavigate} /> ))} </div> ) } </LoadableBlock> </nav> {props.updateAvailable && props.latestVersion && ( <UpdateCard latestVersion={props.latestVersion} channel={props.channel} /> )} <div className="shrink-0 border-t border-sidebar-border px-4 py-2.5"> <div className="flex items-center justify-between text-[10px] leading-none"> <span className="text-muted-foreground/70 tabular-nums">v{VITE_APP_VERSION}</span> <a href={VITE_GITHUB_URL} target="_blank" rel="noreferrer" className="text-muted-foreground/70 transition-colors hover:text-foreground" > GitHub </a> </div> </div> </> );} // ── SourceItem ─────────────────────────────────────────────────────────── function SourceItem(props: { pathname: string; source: Source; onNavigate?: () => void;}) { const paths = getSourceFrontendPaths(props.source.kind); if (!paths) { return ( <div className="group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-[13px] text-sidebar-foreground/60"> <div className="flex size-3 shrink-0 items-center justify-center text-muted-foreground/50"> <SourceFavicon source={props.source} className="size-3" /> </div>Lines 1–500 of 561. Load more
import { Link, Outlet, useLocation, useMatchRoute } from "@tanstack/react-router"; import { useCallback, useEffect, useState } from "react"; import { useSources, type Source } from "@executor/react"; import { Button, sourcePluginsIndexPath } from "@executor/react/plugins"; import { cn } from "../lib/utils"; import { IconPlus, IconCopy, IconCheck, IconClose, IconMenu } from "./icons"; import { LoadableBlock } from "./loadable"; import { SourceFavicon } from "./source-favicon"; import { getSourceFrontendPaths, registeredFrontendPluginNavRoutes, } from "../plugins"; // ── Status dot color ───────────────────────────────────────────────────── const statusColor: Record<string, string> = { connected: "bg-primary", probing: "bg-amber-400", draft: "bg-muted-foreground/30", auth_required: "bg-amber-500", error: "bg-destructive", }; type AppMetaEnv = { readonly VITE_APP_VERSION: string; readonly VITE_GITHUB_URL: string; }; const { VITE_APP_VERSION, VITE_GITHUB_URL } = (import.meta as ImportMeta & { readonly env: AppMetaEnv; }).env; type UpdateChannel = "latest" | "beta"; const EXECUTOR_DIST_TAGS_PATH = "/v1/app/npm/dist-tags"; type ParsedVersion = { readonly major: number; readonly minor: number; readonly patch: number; readonly prerelease: ReadonlyArray<string | number> | null; }; const semverPattern = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/; const resolveUpdateChannel = (version: string): UpdateChannel => version.includes("-beta.") ? "beta" : "latest"; const parseVersion = (version: string): ParsedVersion | null => { const match = version.trim().match(semverPattern); if (!match?.groups) { return null; } return { major: Number(match.groups.major), minor: Number(match.groups.minor), patch: Number(match.groups.patch), prerelease: match.groups.prerelease ? match.groups.prerelease.split(".").map((identifier) => /^\d+$/.test(identifier) ? Number(identifier) : identifier, ) : null, }; }; const comparePrereleaseIdentifiers = ( left: ReadonlyArray<string | number> | null, right: ReadonlyArray<string | number> | null, ): number => { if (left === null && right === null) { return 0; } if (left === null) { return 1; } if (right === null) { return -1; } const length = Math.max(left.length, right.length); for (let index = 0; index < length; index += 1) { const leftIdentifier = left[index]; const rightIdentifier = right[index]; if (leftIdentifier === rightIdentifier) { continue; } if (leftIdentifier === undefined) { return -1; } if (rightIdentifier === undefined) { return 1; } if (typeof leftIdentifier === "number" && typeof rightIdentifier === "number") { return leftIdentifier < rightIdentifier ? -1 : 1; } if (typeof leftIdentifier === "number") { return -1; } if (typeof rightIdentifier === "number") { return 1; } return leftIdentifier < rightIdentifier ? -1 : 1; } return 0; }; const compareVersions = (left: string, right: string): number | null => { const leftVersion = parseVersion(left); const rightVersion = parseVersion(right); if (!leftVersion || !rightVersion) { return null; } if (leftVersion.major !== rightVersion.major) { return leftVersion.major < rightVersion.major ? -1 : 1; } if (leftVersion.minor !== rightVersion.minor) { return leftVersion.minor < rightVersion.minor ? -1 : 1; } if (leftVersion.patch !== rightVersion.patch) { return leftVersion.patch < rightVersion.patch ? -1 : 1; } return comparePrereleaseIdentifiers(leftVersion.prerelease, rightVersion.prerelease); }; // ── useLatestVersion ───────────────────────────────────────────────────── function useLatestVersion(currentVersion: string) { const channel = resolveUpdateChannel(currentVersion); const [latestVersion, setLatestVersion] = useState<string | null>(null); useEffect(() => { let cancelled = false; fetch(EXECUTOR_DIST_TAGS_PATH) .then(async (res) => { if (!res.ok) { throw new Error(`Failed to load dist tags: ${res.status}`); } return res.json() as Promise<Partial<Record<UpdateChannel, string>>>; }) .then((data: Partial<Record<UpdateChannel, string>>) => { if (!cancelled) { setLatestVersion(data[channel] ?? null); } }) .catch(() => {}); return () => { cancelled = true; }; }, [channel]); const updateAvailable = latestVersion !== null && compareVersions(currentVersion, latestVersion) === -1; return { latestVersion, updateAvailable, channel }; } // ── UpdateCard ─────────────────────────────────────────────────────────── function UpdateCard(props: { latestVersion: string; channel: UpdateChannel }) { const command = `npm i -g executor@${props.channel}`; const [copied, setCopied] = useState(false); const handleCopy = useCallback(() => { void navigator.clipboard.writeText(command).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }, [command]); return ( <div className="mx-2 mb-2 rounded-xl border border-primary/25 bg-primary/[0.06] p-3"> <div className="flex items-center gap-2"> <div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/15"> <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary"> <path d="M8 3v7M5 7l3 3 3-3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /> <path d="M3 12h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /> </svg> </div> <div className="min-w-0"> <p className="text-[11px] font-semibold text-foreground">Update available</p> <p className="text-[10px] text-muted-foreground"> v{props.latestVersion} </p> </div> </div> <Button type="button" variant="outline" onClick={handleCopy} className="mt-2.5 flex w-full items-center justify-between gap-2 rounded-lg border-border/60 bg-background/50 px-2.5 py-1.5 text-left hover:bg-background/80" > <code className="truncate font-mono text-[10px] text-sidebar-foreground"> {command} </code> <span className="shrink-0 text-muted-foreground transition-colors group-hover:text-foreground"> {copied ? ( <IconCheck className="size-3 text-primary" /> ) : ( <IconCopy className="size-3" /> )} </span> </Button> </div> ); } // ── AppShell ───────────────────────────────────────────────────────────── export function AppShell() { const sources = useSources(); const location = useLocation(); const matchRoute = useMatchRoute(); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const isHome = matchRoute({ to: "/" }); const isSecrets = matchRoute({ to: "/secrets" }); const isPolicies = matchRoute({ to: "/policies" }); const { latestVersion, updateAvailable, channel } = useLatestVersion(VITE_APP_VERSION); const mainPluginNavItems = registeredFrontendPluginNavRoutes.filter( ({ route }) => (route.nav?.section ?? "main") === "main", ); const sourcePluginNavItems = registeredFrontendPluginNavRoutes.filter( ({ route }) => route.nav?.section === "sources", ); useEffect(() => { setMobileSidebarOpen(false); }, [location.pathname]); useEffect(() => { if (!mobileSidebarOpen) { return; } const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = previousOverflow; }; }, [mobileSidebarOpen]); return ( <div className="flex h-screen overflow-hidden"> <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56"> <SidebarContent isHome={!!isHome} isSecrets={!!isSecrets} isPolicies={!!isPolicies} latestVersion={latestVersion} updateAvailable={updateAvailable} channel={channel} locationPathname={location.pathname} mainPluginNavItems={mainPluginNavItems} sourcePluginNavItems={sourcePluginNavItems} sources={sources} /> </aside> {mobileSidebarOpen && ( <div className="fixed inset-0 z-50 flex md:hidden"> <button type="button" aria-label="Close navigation" className="absolute inset-0 bg-black/45 backdrop-blur-[1px]" onClick={() => setMobileSidebarOpen(false)} /> <div className="relative flex h-full w-[84vw] max-w-xs flex-col border-r border-sidebar-border bg-sidebar shadow-2xl"> <div className="flex h-12 shrink-0 items-center justify-between border-b border-sidebar-border px-4"> <Link to="/" className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground"> executor </span> <span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary"> v3 </span> </Link> <Button type="button" variant="ghost" size="icon" aria-label="Close navigation" onClick={() => setMobileSidebarOpen(false)} className="size-8 text-sidebar-foreground hover:bg-sidebar-active hover:text-foreground" > <IconClose className="size-3.5" /> </Button> </div> <SidebarContent isHome={!!isHome} isSecrets={!!isSecrets} isPolicies={!!isPolicies} latestVersion={latestVersion} updateAvailable={updateAvailable} channel={channel} locationPathname={location.pathname} mainPluginNavItems={mainPluginNavItems} sourcePluginNavItems={sourcePluginNavItems} sources={sources} onNavigate={() => setMobileSidebarOpen(false)} showBrand={false} /> </div> </div> )} {/* Main content */} <main className="flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden"> <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden"> <Button type="button" variant="outline" size="icon" aria-label="Open navigation" onClick={() => setMobileSidebarOpen(true)} className="size-8 bg-card hover:bg-accent/50" > <IconMenu className="size-4" /> </Button> <Link to="/" className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground"> executor </span> <span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary"> v3 </span> </Link> <div className="w-8 shrink-0" /> </div> <Outlet /> </main> </div> ); } function SidebarContent(props: { isHome: boolean; isSecrets: boolean; isPolicies: boolean; latestVersion: string | null; updateAvailable: boolean; channel: UpdateChannel; locationPathname: string; mainPluginNavItems: typeof registeredFrontendPluginNavRoutes; sourcePluginNavItems: typeof registeredFrontendPluginNavRoutes; sources: ReturnType<typeof useSources>; onNavigate?: () => void; showBrand?: boolean; }) { return ( <> {props.showBrand !== false && ( <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4"> <Link to="/" className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground"> executor </span> <span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary"> v3 </span> </Link> </div> )} <nav className="flex flex-1 flex-col overflow-y-auto p-2"> <NavItem to="/" label="Dashboard" active={props.isHome} onNavigate={props.onNavigate} /> <NavItem to="/secrets" label="Secrets" active={props.isSecrets} onNavigate={props.onNavigate} /> <NavItem to="/policies" label="Policies" active={props.isPolicies} onNavigate={props.onNavigate} /> {props.mainPluginNavItems.map(({ plugin, route, to }) => ( <NavItem key={`${plugin.key}:${route.key}`} to={to} label={route.nav?.label ?? route.key} active={ props.locationPathname === to || props.locationPathname.startsWith(`${to}/`) } onNavigate={props.onNavigate} /> ))} <div className="mt-5 mb-1 px-2.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50"> <div className="flex items-center justify-between gap-2"> <span>Sources</span> <Link to={sourcePluginsIndexPath} onClick={props.onNavigate} className="inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-[10px] font-medium normal-case tracking-normal text-primary transition-colors hover:bg-sidebar-active hover:text-foreground" > <IconPlus className="size-3" /> Add </Link> </div> </div> {props.sourcePluginNavItems.length > 0 && ( <div className="mb-2 flex flex-col gap-px"> {props.sourcePluginNavItems.map(({ plugin, route, to }) => ( <NavItem key={`${plugin.key}:${route.key}`} to={to} label={route.nav?.label ?? route.key} active={ props.locationPathname === to || props.locationPathname.startsWith(`${to}/`) } onNavigate={props.onNavigate} /> ))} </div> )} <LoadableBlock loadable={props.sources} loading="Loading..."> {(items) => !Array.isArray(items) ? ( <div className="px-2.5 py-2 text-[11px] leading-relaxed text-destructive"> Sources returned an unexpected payload. </div> ) : items.length === 0 ? ( <div className="px-2.5 py-2 text-[11px] leading-relaxed text-muted-foreground/40"> No sources yet </div> ) : ( <div className="flex flex-col gap-px"> {items.map((source) => ( <SourceItem key={source.id} pathname={props.locationPathname} source={source} onNavigate={props.onNavigate} /> ))} </div> ) } </LoadableBlock> </nav> {props.updateAvailable && props.latestVersion && ( <UpdateCard latestVersion={props.latestVersion} channel={props.channel} /> )} <div className="shrink-0 border-t border-sidebar-border px-4 py-2.5"> <div className="flex items-center justify-between text-[10px] leading-none"> <span className="text-muted-foreground/70 tabular-nums">v{VITE_APP_VERSION}</span> <a href={VITE_GITHUB_URL} target="_blank" rel="noreferrer" className="text-muted-foreground/70 transition-colors hover:text-foreground" > GitHub </a> </div> </div> </> ); } // ── SourceItem ─────────────────────────────────────────────────────────── function SourceItem(props: { pathname: string; source: Source; onNavigate?: () => void; }) { const paths = getSourceFrontendPaths(props.source.kind); if (!paths) { return ( <div className="group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-[13px] text-sidebar-foreground/60"> <div className="flex size-3 shrink-0 items-center justify-center text-muted-foreground/50"> <SourceFavicon source={props.source} className="size-3" /> </div>