Files
copilotkit__copilotkit/docs/components/layout/sidebar.tsx
Tyler Slaton 760fc65187 feat(docs): scope sidebar to /learn with header + Overview landing
Add a Learn-only sidebar mode that mirrors how /reference works: when a user
is under /learn, the framework picker is hidden and the sidebar shows only
learn content (What's New, Agentic Protocols, Generative UI, Specs,
Architecture, Tutorials).

- Add `learn` to the root meta.json so the learn folder is discoverable in
  the page tree (was missing, which is why ConditionalSidebar previously
  fell through to the default sidebar on /learn routes)
- Add a static LearnHeader (book icon + "Learn") visually matching the
  Reference version selector, wired in via Sidebar's headerSlot
- Rename the learn index page title to "Overview" so the landing page reads
  cleanly under the new header
- Drop the redundant `h-6` spacer in Sidebar — every header component
  (IntegrationSelector, VersionSelector, LearnHeader) already supplies its
  own `mb-3`, and the spacer was producing a 24px phantom gap on /learn
  while collapsing to 0 on /reference under flex shrink

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 20:49:48 -07:00

99 lines
3.1 KiB
TypeScript

"use client";
import { useState, useEffect, type ReactNode } from "react";
import { DocsLayoutProps } from "fumadocs-ui/layouts/docs";
import Separator from "../ui/sidebar/separator";
import Page from "../ui/sidebar/page";
import Folder from "../ui/sidebar/folder";
import IntegrationLink from "../ui/sidebar/integration-link";
import IntegrationSelector, {
Integration,
} from "../ui/integrations-sidebar/integration-selector";
import { OpenedFoldersProvider } from "@/lib/hooks/use-opened-folders";
import { INTEGRATION_ORDER } from "@/lib/integrations";
type Node = DocsLayoutProps["tree"]["children"][number] & {
url: string;
index?: { url: string };
};
const NODE_COMPONENTS = {
separator: Separator,
page: Page,
folder: Folder,
integrationLink: IntegrationLink,
};
const isIntegrationFolder = (node: Node): boolean => {
if (node.type !== "folder") return false;
const url = node.index?.url || node.url;
if (!url) return false;
// Integration landing pages are at /{integration} (e.g., /langgraph)
// Check if the URL matches a known integration ID
const integrationId = url.replace(/^\//, "").split("/")[0];
return INTEGRATION_ORDER.includes(
integrationId as (typeof INTEGRATION_ORDER)[number],
);
};
const Sidebar = ({
pageTree,
showIntegrationSelector = true,
headerSlot,
}: {
pageTree: DocsLayoutProps["tree"];
showIntegrationSelector?: boolean;
headerSlot?: ReactNode;
}) => {
const pages = pageTree.children;
const [selectedIntegration, setSelectedIntegration] =
useState<Integration | null>(null);
// Dispatch pageTree update for OpenedFoldersProvider
useEffect(() => {
if (pages.length > 0) {
const event = new CustomEvent("pageTreeUpdate", { detail: pages });
window.dispatchEvent(event);
}
}, [pages]);
return (
<OpenedFoldersProvider>
<aside
id="nd-sidebar"
className={`w-full max-w-[260px] h-full border backdrop-blur-lg border-r-0 border-border rounded-l-2xl pl-3 ${showIntegrationSelector ? "pr-3" : "pr-1"} flex flex-col`}
style={{ backgroundColor: "var(--sidebar)" }}
>
{showIntegrationSelector && (
<IntegrationSelector
selectedIntegration={selectedIntegration}
setSelectedIntegration={setSelectedIntegration}
/>
)}
{headerSlot && <div className="pr-2">{headerSlot}</div>}
<ul
className={`flex overflow-y-auto flex-col pr-1 max-h-full custom-scrollbar`}
>
{pages.map((page, index) => {
const nodeType = isIntegrationFolder(page as Node)
? "integrationLink"
: page.type;
const Component = NODE_COMPONENTS[nodeType];
// Use stable key based on page data to avoid hydration mismatches
const pageUrl =
(page as Node).index?.url ||
(page as Node).url ||
`page-${index}`;
const key = `${nodeType}-${pageUrl}`;
return <Component key={key} node={page as Node} />;
})}
</ul>
</aside>
</OpenedFoldersProvider>
);
};
export default Sidebar;