diff --git a/apps/website/src/app/blog/[slug]/page.tsx b/apps/website/src/app/blog/[slug]/page.tsx index be00ccd02..becb59648 100644 --- a/apps/website/src/app/blog/[slug]/page.tsx +++ b/apps/website/src/app/blog/[slug]/page.tsx @@ -110,13 +110,7 @@ export default async function BlogPostPage({ params }: Params) { ) : null} - + diff --git a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx index ae6bdac44..5bc411490 100644 --- a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx +++ b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx @@ -107,13 +107,7 @@ export default async function DocsPage({ params }: DocsRouteProps) { />
- +
{section === 'api' && (() => { const entries = loadApiDocs(library); diff --git a/apps/website/src/app/docs/choosing-an-adapter/page.tsx b/apps/website/src/app/docs/choosing-an-adapter/page.tsx index ee0b161d2..08f62b3d0 100644 --- a/apps/website/src/app/docs/choosing-an-adapter/page.tsx +++ b/apps/website/src/app/docs/choosing-an-adapter/page.tsx @@ -1,24 +1,14 @@ import fs from 'fs'; import path from 'path'; import { notFound } from 'next/navigation'; -import { MDXRemote } from 'next-mdx-remote/rsc'; -import rehypePrettyCode from 'rehype-pretty-code'; -import rehypeSlug from 'rehype-slug'; -import remarkGfm from 'remark-gfm'; -import { tokens } from '@threadplane/design-tokens'; -import { Container } from '../../../components/ui/Container'; -import { Section } from '../../../components/ui/Section'; -import { Eyebrow } from '../../../components/ui/Eyebrow'; -import { Callout } from '../../../components/docs/mdx/Callout'; -import { Steps, Step } from '../../../components/docs/mdx/Steps'; -import { Tabs, Tab } from '../../../components/docs/mdx/Tabs'; -import { Card, CardGroup } from '../../../components/docs/mdx/Card'; -import { CodeGroup } from '../../../components/docs/mdx/CodeGroup'; -import { Pre } from '../../../components/docs/mdx/CodeBlock'; -import { mdxHeadingComponents } from '../../../components/docs/mdx/headings'; +import { DocsControlPlane } from '../../../components/docs/DocsControlPlane'; +import { DocsSearch } from '../../../components/docs/DocsSearch'; +import { MdxRenderer } from '../../../components/docs/MdxRenderer'; import { createPageMetadata } from '../../../lib/site-metadata'; import { stripFrontmatter } from '../../../lib/docs'; +const PAGE_TITLE = 'Choosing an adapter'; + export const metadata = createPageMetadata({ title: 'Choosing an adapter — Threadplane', description: 'Decide between @threadplane/langgraph and @threadplane/ag-ui.', @@ -26,29 +16,6 @@ export const metadata = createPageMetadata({ type: 'website', }); -const mdxComponents = { - Callout, - Steps, - Step, - Tabs, - Tab, - Card, - CardGroup, - CodeGroup, - pre: Pre, - table: ({ children, ...rest }: React.HTMLAttributes) => ( -
- {children}
-
- ), - ...mdxHeadingComponents, -}; - -const rehypeOptions = { - theme: 'tokyo-night', - keepBackground: true, -}; - function resolveContentFile(): string | null { const candidates = [ path.join(process.cwd(), 'apps', 'website', 'content', 'docs', 'choosing-an-adapter', 'index.mdx'), @@ -64,49 +31,29 @@ export default function ChoosingAnAdapterPage() { const filePath = resolveContentFile(); if (!filePath) notFound(); - const raw = fs.readFileSync(filePath, 'utf8'); - const source = stripFrontmatter(raw); + const source = stripFrontmatter(fs.readFileSync(filePath, 'utf8')); return ( - <> -
- -
- - Documentation - -
-
- -
- -
- +
+ + {/* This page is deliberately library-neutral: it is the page that helps + * you pick one, so the picker opens with nothing selected. */} + +
+
- +
- -
- + + + ); } diff --git a/apps/website/src/components/docs/DocsControlPlane.spec.tsx b/apps/website/src/components/docs/DocsControlPlane.spec.tsx index 2ef5dbb4b..96c17f484 100644 --- a/apps/website/src/components/docs/DocsControlPlane.spec.tsx +++ b/apps/website/src/components/docs/DocsControlPlane.spec.tsx @@ -232,6 +232,45 @@ describe('DocsControlPlane', () => { }); }); +describe('DocsControlPlane — library-neutral', () => { + it('states only what it knows in Scope', () => { + render( + , + ); + + const scope = screen.getByRole('heading', { name: 'Scope' }).closest('section'); + if (!scope) throw new Error('Expected Scope section'); + expect(within(scope).getByText('Choosing an adapter')).toBeTruthy(); + expect(within(scope).queryByText('LangGraph')).toBeNull(); + expect(within(scope).queryByText('Getting Started')).toBeNull(); + }); + + it('offers an unselected picker and no section tree', () => { + render( + , + ); + + const trigger = screen.getByRole('button', { name: 'Choose a library' }); + fireEvent.click(trigger); + const items = screen.getAllByRole('menuitemradio'); + expect(items.length).toBeGreaterThan(0); + expect(items.every((i) => i.getAttribute('aria-checked') === 'false')).toBe(true); + + // No library means there is no section tree to show. + expect(screen.queryByRole('button', { name: 'Getting Started' })).toBeNull(); + }); +}); + describe('DocsContextContent', () => { it('reuses the same sentence-case navigation content for mobile', () => { render( diff --git a/apps/website/src/components/docs/DocsControlPlane.tsx b/apps/website/src/components/docs/DocsControlPlane.tsx index b4c8b62b7..8c15ffc47 100644 --- a/apps/website/src/components/docs/DocsControlPlane.tsx +++ b/apps/website/src/components/docs/DocsControlPlane.tsx @@ -28,7 +28,8 @@ import { buildCockpitModeHref } from '../../lib/cockpit-links'; import { DocsNavigation } from './DocsSidebar'; export interface DocsControlPlaneProps { - activeLibrary: LibraryId; + /** `null` on a library-neutral docs page, e.g. /docs/choosing-an-adapter. */ + activeLibrary: LibraryId | null; activeSection: string; activeSlug: string; pageTitle: string; @@ -46,8 +47,8 @@ export function DocsContextContent({ onNavigate, }: DocsControlPlaneProps & { mobile?: boolean; onNavigate?: () => void }) { const preferences = useControlPlanePreferences('docs'); - const library = getLibraryConfig(activeLibrary); - const section = getDocsSection(activeLibrary, activeSection); + const library = activeLibrary ? getLibraryConfig(activeLibrary) : undefined; + const section = activeLibrary ? getDocsSection(activeLibrary, activeSection) : undefined; const openSearch = () => { if (!mobile) { dispatchSearch(); @@ -65,8 +66,11 @@ export function DocsContextContent({
- {library?.title ?? activeLibrary} - {section?.title ?? activeSection} + {/* A neutral page has no library and no section. Say only what is + * true — inventing them is how the mobile drawer came to claim + * "LangGraph / Getting Started" on the adapter-comparison page. */} + {library?.title ?? 'Docs'} + {library && section ? {section.title} : null} {pageTitle}
@@ -118,11 +122,13 @@ export function DocsContextContent({ export function DocsControlPlane(props: DocsControlPlaneProps) { const identity = { - library: props.activeLibrary, + library: props.activeLibrary ?? '', section: props.activeSection, slug: props.activeSlug, }; - const currentPath = `/docs/${props.activeLibrary}/${props.activeSection}/${props.activeSlug}`; + const currentPath = props.activeLibrary + ? `/docs/${props.activeLibrary}/${props.activeSection}/${props.activeSlug}` + : '/docs'; return (
diff --git a/apps/website/src/components/docs/DocsSidebar.tsx b/apps/website/src/components/docs/DocsSidebar.tsx index d85d8c406..3e7fa2b14 100644 --- a/apps/website/src/components/docs/DocsSidebar.tsx +++ b/apps/website/src/components/docs/DocsSidebar.tsx @@ -33,7 +33,8 @@ import { import { LibraryMark } from './LibraryMark'; export interface DocsNavigationProps { - activeLibrary: LibraryId; + /** `null` on a library-neutral docs page. */ + activeLibrary: LibraryId | null; activeSection: string; activeSlug: string; expanded?: Record; @@ -62,7 +63,7 @@ function LibraryDropdown({ activeLibrary, onNavigate, }: { - activeLibrary: LibraryId; + activeLibrary: LibraryId | null; onNavigate?: () => void; }) { const [open, setOpen] = useState(false); @@ -74,7 +75,8 @@ function LibraryDropdown({ useEffect(() => { const handler = (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false); + if (ref.current && !ref.current.contains(event.target as Node)) + setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); @@ -82,7 +84,8 @@ function LibraryDropdown({ useEffect(() => { if (!open) return; - const items = menuRef.current?.querySelectorAll(MENU_ITEM_SELECTOR); + const items = + menuRef.current?.querySelectorAll(MENU_ITEM_SELECTOR); items?.[initialFocusRef.current]?.focus(); }, [open]); @@ -98,7 +101,10 @@ function LibraryDropdown({ const trigger = triggerRef.current; const menu = menuRef.current; if (!trigger || !menu) return; - const available = window.innerHeight - trigger.getBoundingClientRect().bottom - MENU_VIEWPORT_GUTTER; + const available = + window.innerHeight - + trigger.getBoundingClientRect().bottom - + MENU_VIEWPORT_GUTTER; menu.style.maxHeight = `${Math.max(MENU_MIN_HEIGHT, available)}px`; }; resize(); @@ -118,19 +124,22 @@ function LibraryDropdown({ const onMenuKeyDown = (event: KeyboardEvent) => { const items = Array.from( - menuRef.current?.querySelectorAll(MENU_ITEM_SELECTOR) ?? [], + menuRef.current?.querySelectorAll(MENU_ITEM_SELECTOR) ?? [] + ); + const current = Math.max( + 0, + items.indexOf(document.activeElement as HTMLElement) ); - const current = Math.max(0, items.indexOf(document.activeElement as HTMLElement)); const nextIndex = event.key === 'Home' ? 0 : event.key === 'End' - ? items.length - 1 - : event.key === 'ArrowDown' - ? (current + 1) % items.length - : event.key === 'ArrowUp' - ? (current - 1 + items.length) % items.length - : -1; + ? items.length - 1 + : event.key === 'ArrowDown' + ? (current + 1) % items.length + : event.key === 'ArrowUp' + ? (current - 1 + items.length) % items.length + : -1; if (nextIndex >= 0) { event.preventDefault(); items[nextIndex]?.focus(); @@ -144,7 +153,9 @@ function LibraryDropdown({ } }; - const currentLibrary = getLibraryConfig(activeLibrary); + const currentLibrary = activeLibrary + ? getLibraryConfig(activeLibrary) + : undefined; return (
@@ -167,12 +178,22 @@ function LibraryDropdown({ className="docs-sidebar-lib-trigger" > - - - {currentLibrary?.title ?? activeLibrary} + {activeLibrary ? ( + + ) : null} + + {currentLibrary?.title ?? 'Choose a library'} -