chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
"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 (
|
||||
<span className="ml-1 flex shrink-0 items-center" aria-hidden="true">
|
||||
<ChevronDown
|
||||
className={`h-3.5 w-3.5 transition-colors ${
|
||||
active
|
||||
? "text-[var(--accent)]"
|
||||
: "text-[var(--text-muted)] group-hover:text-[var(--accent)]"
|
||||
}`}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function FrontendEarlyAccessBadge() {
|
||||
return (
|
||||
<span className="shell-docs-radius-control inline-flex shrink-0 self-center border border-[var(--border)] bg-[var(--bg-elevated)] px-1 py-0 text-[8px] font-semibold leading-[10px] text-[var(--text-muted)]">
|
||||
Early access
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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<HTMLDivElement>(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 && (
|
||||
<button
|
||||
key={pinnedBIA.slug}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={pinnedBIA.slug === effectiveFramework}
|
||||
onClick={() => selectFramework(pinnedBIA.slug)}
|
||||
className={`shell-docs-radius-control flex w-full cursor-pointer items-center gap-2 px-2 py-1.5 text-[13px] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] ${
|
||||
pinnedBIA.slug === effectiveFramework
|
||||
? "bg-[var(--accent-dim)] text-[var(--accent)]"
|
||||
: `text-[var(--text-secondary)] ${optionHoverClass}`
|
||||
}`}
|
||||
>
|
||||
<FrameworkLogo
|
||||
slug={pinnedBIA.slug}
|
||||
fallbackSrc={pinnedBIA.logo}
|
||||
size={16}
|
||||
className="shrink-0 text-[var(--accent)]"
|
||||
/>
|
||||
<span className="flex-1 truncate text-left">
|
||||
{displayNameFor(pinnedBIA)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{flatOptions.map((opt) => {
|
||||
const isActive = opt.slug === effectiveFramework;
|
||||
return (
|
||||
<button
|
||||
key={opt.slug}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isActive}
|
||||
onClick={() => selectFramework(opt.slug)}
|
||||
className={`shell-docs-radius-control flex w-full cursor-pointer items-center gap-2 px-2 py-1.5 text-[13px] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] ${
|
||||
isActive
|
||||
? "bg-[var(--accent-dim)] text-[var(--accent)]"
|
||||
: `text-[var(--text-secondary)] ${optionHoverClass}`
|
||||
}`}
|
||||
>
|
||||
<FrameworkLogo
|
||||
slug={opt.slug}
|
||||
fallbackSrc={opt.logo}
|
||||
size={16}
|
||||
className="shrink-0 text-[var(--accent)]"
|
||||
/>
|
||||
<span className="flex-1 truncate text-left">
|
||||
{displayNameFor(opt)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={`relative ${openMenu ? "z-50" : ""} ${className ?? ""}`}
|
||||
>
|
||||
{isSidebar ? (
|
||||
<>
|
||||
<div className="shell-docs-picker-group shell-docs-picker-group-selected shell-docs-picker-group-bordered space-y-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOpenMenu((menu) => (menu === "frontend" ? null : "frontend"))
|
||||
}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={openMenu === "frontend"}
|
||||
aria-label="Choose frontend"
|
||||
className="shell-docs-picker-row group flex min-h-[52px] w-full cursor-pointer items-center gap-2.5 px-2 py-1.5 text-left transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)]"
|
||||
>
|
||||
<span
|
||||
className="shell-docs-picker-icon-chip flex h-8 w-8 shrink-0 items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<FrontendLogo icon={selectedFrontend.icon} size={19} />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-[11px] font-medium leading-tight text-[var(--text-muted)]">
|
||||
Frontend
|
||||
</span>
|
||||
<span className="mt-0.5 flex min-w-0 items-center gap-2 text-[13px] font-semibold leading-tight text-[var(--text)]">
|
||||
<span className="truncate">{selectedFrontend.name}</span>
|
||||
{isFrontendEarlyAccess(selectedFrontend.id) && (
|
||||
<FrontendEarlyAccessBadge />
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<SelectorAffordance active={openMenu === "frontend"} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOpenMenu((menu) => (menu === "backend" ? null : "backend"))
|
||||
}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={openMenu === "backend"}
|
||||
aria-label="Choose agent backend"
|
||||
className="shell-docs-picker-row shell-docs-picker-row-divided group flex min-h-[52px] w-full cursor-pointer items-center gap-2.5 px-2 py-1.5 text-left transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)]"
|
||||
>
|
||||
<span
|
||||
className="shell-docs-picker-icon-chip flex h-8 w-8 shrink-0 items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{current ? (
|
||||
<FrameworkLogo
|
||||
slug={current.slug}
|
||||
fallbackSrc={current.logo}
|
||||
size={17}
|
||||
className="text-[var(--accent)]"
|
||||
/>
|
||||
) : (
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-[var(--accent)] opacity-70" />
|
||||
)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-[11px] font-medium leading-tight text-[var(--text-muted)]">
|
||||
Agent backend
|
||||
</span>
|
||||
{current ? (
|
||||
<span className="mt-0.5 block truncate text-[13px] font-semibold leading-tight text-[var(--text)]">
|
||||
{label}
|
||||
</span>
|
||||
) : (
|
||||
<span className="mt-0.5 block truncate text-[13px] font-medium leading-tight text-[var(--text-muted)]">
|
||||
No backend selected
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<SelectorAffordance active={openMenu === "backend"} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{openMenu === "frontend" && (
|
||||
<div
|
||||
role="listbox"
|
||||
aria-label="Choose frontend"
|
||||
className="shell-docs-radius-surface shell-docs-picker-menu absolute left-0 right-0 top-full z-50 mt-1 border border-[var(--border)] bg-[var(--bg-surface)] p-2"
|
||||
>
|
||||
{FRONTEND_OPTIONS.map((option) => {
|
||||
const isActive = option.id === selectedFrontend.id;
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isActive}
|
||||
onClick={() => selectFrontend(option.id)}
|
||||
className={`shell-docs-radius-control flex w-full cursor-pointer items-center gap-2 px-2 py-1.5 text-[13px] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] ${
|
||||
isActive
|
||||
? "bg-[var(--accent-dim)] text-[var(--accent)]"
|
||||
: "text-[var(--text-secondary)] hover:bg-[var(--bg-elevated)] hover:text-[var(--text)]"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="shell-docs-picker-icon-chip flex h-7 w-7 shrink-0 items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<FrontendLogo icon={option.icon} size={17} />
|
||||
</span>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-2 text-left font-medium">
|
||||
<span className="truncate">{option.name}</span>
|
||||
{isFrontendEarlyAccess(option.id) && (
|
||||
<FrontendEarlyAccessBadge />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openMenu === "backend" && (
|
||||
<div
|
||||
role="listbox"
|
||||
aria-label="Choose agent backend"
|
||||
className="shell-docs-radius-surface shell-docs-picker-menu absolute left-0 top-full z-50 mt-1 max-h-[60vh] w-[320px] max-w-[calc(100vw-2rem)] overflow-y-auto border border-[var(--border)] bg-[var(--bg-surface)] p-2"
|
||||
>
|
||||
{backendOptions(
|
||||
true,
|
||||
"hover:bg-[var(--bg-elevated)] hover:text-[var(--text)]",
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOpenMenu((menu) => (menu === "backend" ? null : "backend"))
|
||||
}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={openMenu === "backend"}
|
||||
className={topbarBtnClasses}
|
||||
>
|
||||
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--accent)]" />
|
||||
<span className="truncate">
|
||||
{current ? (
|
||||
<>
|
||||
<span className="mr-1 font-mono text-[10px] uppercase tracking-wider text-[var(--text-faint)]">
|
||||
Backend
|
||||
</span>
|
||||
{label}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-[var(--text-muted)]">{label}</span>
|
||||
)}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className="h-3 w-3 shrink-0 text-[var(--text-muted)]"
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{openMenu === "backend" && (
|
||||
<div
|
||||
role="listbox"
|
||||
className="shell-docs-radius-surface shell-docs-picker-menu absolute left-0 top-full z-50 mt-1 max-h-[70vh] w-[340px] overflow-y-auto border border-[var(--border)] bg-[var(--bg-surface)] p-2"
|
||||
>
|
||||
{backendOptions(
|
||||
false,
|
||||
"hover:bg-[var(--bg-elevated)] hover:text-[var(--text)]",
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user