"use client"; // FrameworkSelector - persistent docs selector. The sidebar variant // exposes frontend and agent backend as separate, simple dropdowns. import React, { useEffect, useRef, useState } from "react"; import { ChevronDown } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { usePostHog } from "posthog-js/react"; import { DEFAULT_FRAMEWORK, useFramework } from "./framework-provider"; import { FrontendLogo } from "./frontend-logo"; import { FrameworkLogo } from "./icons/framework-icons"; import { compareByDisplayOrder } from "@/lib/framework-order"; import { FRONTEND_OPTIONS, backendPathForCurrentPath, frontendFromPathname, frontendPathForCurrentPath, isFrontendEarlyAccess, } from "@/lib/frontend-options"; import type { FrontendId } from "@/lib/frontend-options"; export interface FrameworkOption { slug: string; name: string; category: string; logo?: string | null; deployed: boolean; } export interface FrameworkSelectorProps { options: FrameworkOption[]; /** * Ordered category ids (from the registry) used to group entries in the * dropdown panel. Unknown categories fall through to "Other". */ categoryOrder: { id: string; name: string }[]; /** Extra wrapper class (positioning). */ className?: string; /** * Presentation flavor. * - `topbar` (default, legacy): compact pill sized for a horizontal bar. * - `sidebar`: two full-width selector rows for frontend and backend. */ variant?: "topbar" | "sidebar"; } function SelectorAffordance({ active }: { active: boolean }) { return ( ); } function FrontendEarlyAccessBadge() { return ( Early access ); } export function FrameworkSelector({ options, categoryOrder: _categoryOrder, className, variant = "topbar", }: FrameworkSelectorProps) { const router = useRouter(); const pathname = usePathname() ?? ""; const posthog = usePostHog(); const { effectiveFramework, setStoredFramework } = useFramework(); const urlFrontend = frontendFromPathname(pathname); const [openMenu, setOpenMenu] = useState<"frontend" | "backend" | null>(null); const rootRef = useRef(null); // Close on outside-click / Escape. useEffect(() => { if (!openMenu) return; const handleClick = (e: MouseEvent) => { const target = e.target instanceof Node ? e.target : null; if (!target) return; if (rootRef.current?.contains(target)) return; setOpenMenu(null); }; const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpenMenu(null); }; document.addEventListener("mousedown", handleClick); document.addEventListener("keydown", handleKey); return () => { document.removeEventListener("mousedown", handleClick); document.removeEventListener("keydown", handleKey); }; }, [openMenu]); // Display whatever the page is currently rendering as: URL framework // when present, then stored choice, then the soft-default. const current = options.find((o) => o.slug === effectiveFramework); const isSidebar = variant === "sidebar"; const displayNameFor = (opt: FrameworkOption) => isSidebar && opt.slug === "built-in-agent" ? "CopilotKit" : opt.name; const label = current ? displayNameFor(current) : "Pick an agentic backend"; const effectiveFrontendId = urlFrontend ?? "react"; const selectedFrontend = FRONTEND_OPTIONS.find((option) => option.id === effectiveFrontendId) ?? FRONTEND_OPTIONS[0]; function selectFrontend(id: FrontendId) { if (id !== effectiveFrontendId) { router.replace( frontendPathForCurrentPath( id, pathname, options.map((option) => option.slug), ), ); } setOpenMenu(null); } function selectFramework(slug: string) { setStoredFramework(slug); try { const opt = options.find((o) => o.slug === slug); posthog?.capture("docs.framework_selected", { framework: slug, framework_name: opt?.name ?? slug, category: opt?.category, from_path: pathname, }); } catch { // Swallow - analytics is fire-and-forget. } router.replace( backendPathForCurrentPath( slug, pathname, options.map((option) => option.slug), DEFAULT_FRAMEWORK, ), ); setOpenMenu(null); } // Single flat list, ordered by the canonical display order. The // category buckets ("Most Popular / Agent Frameworks / Intelligence Platform / // Emerging") used to live here but partners read them as a tier // list — we now show every backend in one neutral list. const flatOptions = options .filter((opt) => !(isSidebar && opt.slug === "built-in-agent")) .slice() .sort((a, b) => compareByDisplayOrder(a.slug, b.slug)); const pinnedBIA = isSidebar ? (options.find((o) => o.slug === "built-in-agent") ?? null) : null; const topbarBtnClasses = "shell-docs-radius-control flex items-center gap-1.5 px-2.5 py-1.5 border border-[var(--border)] bg-[var(--bg-surface)] text-[12px] font-medium text-[var(--text)] hover:border-[var(--accent)] transition-colors cursor-pointer max-w-[220px]"; const backendOptions = ( includePinnedBIA: boolean, optionHoverClass: string, ) => ( <> {includePinnedBIA && pinnedBIA && ( )} {flatOptions.map((opt) => { const isActive = opt.slug === effectiveFramework; return ( ); })} ); return (
{isSidebar ? ( <>
{openMenu === "frontend" && (
{FRONTEND_OPTIONS.map((option) => { const isActive = option.id === selectedFrontend.id; return ( ); })}
)} {openMenu === "backend" && (
{backendOptions( true, "hover:bg-[var(--bg-elevated)] hover:text-[var(--text)]", )}
)} ) : ( <> {openMenu === "backend" && (
{backendOptions( false, "hover:bg-[var(--bg-elevated)] hover:text-[var(--text)]", )}
)} )}
); }