import React from "react"; import { DocsLayout } from "fumadocs-ui/layouts/docs"; import type * as PageTree from "fumadocs-core/page-tree"; import { MobileTopNav } from "./mobile-top-nav"; import { SidebarScrollPreserver } from "./sidebar-scroll-preserver"; import { SidebarFolderStatePreserver } from "./sidebar-folder-state-preserver"; import GithubIcon from "./icons/github"; import DiscordIcon from "./icons/discord"; import { MobileSidebarFooterTalk } from "./mobile-sidebar-footer-talk"; import { PrimaryDocsTabs } from "./primary-docs-tabs"; // Shared Fumadocs `DocsLayout` chrome used by every shell-docs route. // All five callers (home overview, framework root, framework-scoped MDX // via DocsPageView, /reference, /ag-ui) pass the same nav/search/sidebar // config — only `tree` and `sidebar.banner` vary. Centralizing keeps // sidebar behavior, mobile nav slot, and container className from drifting // across routes when one is tweaked. export function ShellDocsLayout({ tree, banner, children, }: { tree: PageTree.Root; banner?: React.ReactNode; children: React.ReactNode; }) { return ( }} searchToggle={{ enabled: false }} // Suppress fumadocs's auto-injected ThemeSwitch — `slots.themeSwitch` // is populated by default even when `themeSwitch` isn't passed, so // we have to pass `enabled: false` explicitly to keep the // `iconLinks.length > 0 || slots.themeSwitch` branch in // `sidebar.js` from rendering the default rounded pill. Our own // single-toggle `` is mounted in BrandNav instead. themeSwitch={{ enabled: false }} // We intentionally do NOT pass `links` here either. Fumadocs would // funnel `type: "icon"` entries into the same auto-injected pill // we just disabled — but the icons need to live in our custom // footer row anyway. Rendering them inline keeps a single source // of truth (the JSX below) and avoids the auto layout fighting // our custom one. sidebar={{ banner: (
{banner}
), // Hide Fumadocs's collapse toggle — shell-docs has its own chrome. collapsible: false, className: "shell-docs-sidebar", // Note: `key` is required here because fumadocs's Sidebar // passes the `footer` ReactNode into a `jsxs(children: [a, b, // footer])` array, and React's dev-mode warning insists every // top-level child of an array carry a stable key. We don't // control fumadocs's render, so we hand it a keyed element // directly. The key is a literal string — there's only ever // one footer per sidebar. footer: (
), }} containerProps={{ // The outer .docs-content-wrapper gradient + scroll behavior is // applied to DocsLayout's container so the scroll context, border, // and gradient match what shell-docs has always rendered. className: "docs-content-wrapper", }} > {/* Preserve sidebar scroll across navigations — the sidebar is * rendered per-page (this layout sits inside each page component * rather than in a Next.js layout file), so without explicit * restoration the Radix ScrollAreaViewport resets to 0 every * time the user clicks a link further down the list. */} {/* Persist sidebar folder open/closed state across navigations — * without this, Fumadocs resets each Radix Collapsible to its * default state on every page mount, undoing the user's * "I want this section hidden" choice. */} {children}
); }