Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions apps/website/src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,7 @@ export default async function BlogPostPage({ params }: Params) {
<TagChips tags={post.frontmatter.tags} />
) : null}
</header>
<MdxRenderer
source={post.content}
library="langgraph"
section="blog"
slug={post.slug}
title={post.frontmatter.title}
/>
<MdxRenderer source={post.content} />
</article>
<DocsTOC headings={headings} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,7 @@ export default async function DocsPage({ params }: DocsRouteProps) {
/>
</div>
<article className="flex-1 py-8 px-4 sm:px-6 md:px-12 md:max-w-3xl overflow-x-hidden">
<MdxRenderer
source={doc.body}
library={library as LibraryId}
section={section}
slug={slug}
title={doc.title}
/>
<MdxRenderer source={doc.body} />
</article>
{section === 'api' && (() => {
const entries = loadApiDocs(library);
Expand Down
101 changes: 24 additions & 77 deletions apps/website/src/app/docs/choosing-an-adapter/page.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,21 @@
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.',
pathname: '/docs/choosing-an-adapter',
type: 'website',
});

const mdxComponents = {
Callout,
Steps,
Step,
Tabs,
Tab,
Card,
CardGroup,
CodeGroup,
pre: Pre,
table: ({ children, ...rest }: React.HTMLAttributes<HTMLTableElement>) => (
<div className="docs-table-scroll">
<table {...rest}>{children}</table>
</div>
),
...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'),
Expand All @@ -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 (
<>
<Section surface="canvas" ariaLabelledBy="choosing-an-adapter-heading">
<Container>
<div className="adapter-hero-inner">
<Eyebrow tone="accent" className="adapter-eyebrow-spaced">
Documentation
</Eyebrow>
<div id="choosing-an-adapter-heading" />
</div>
</Container>
</Section>

<Section surface="canvas">
<Container>
<div className="flex min-h-screen docs-shell-page">
<DocsSearch />
{/* This page is deliberately library-neutral: it is the page that helps
* you pick one, so the picker opens with nothing selected. */}
<DocsControlPlane
activeLibrary={null}
activeSection=""
activeSlug=""
pageTitle={PAGE_TITLE}
/>
<div className="flex-1 flex min-w-0 docs-shell-body">
<div className="flex-1 min-w-0">
<article
className="docs-prose prose prose-slate max-w-none adapter-article"
style={
{
'--tw-prose-body': tokens.colors.textSecondary,
'--tw-prose-headings': tokens.colors.textPrimary,
'--tw-prose-code': tokens.colors.accent,
'--tw-prose-links': tokens.colors.accent,
} as React.CSSProperties
}
aria-label={PAGE_TITLE}
className="flex-1 py-8 px-4 sm:px-6 md:px-12 md:max-w-3xl overflow-x-hidden"
>
<MDXRemote
source={source}
components={mdxComponents}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rehypePlugins: [rehypeSlug, [rehypePrettyCode, rehypeOptions] as any],
},
}}
/>
<MdxRenderer source={source} />
</article>
</Container>
</Section>
</>
</div>
</div>
</div>
);
}
39 changes: 39 additions & 0 deletions apps/website/src/components/docs/DocsControlPlane.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,45 @@ describe('DocsControlPlane', () => {
});
});

describe('DocsControlPlane — library-neutral', () => {
it('states only what it knows in Scope', () => {
render(
<DocsControlPlane
activeLibrary={null}
activeSection=""
activeSlug=""
pageTitle="Choosing an adapter"
/>,
);

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(
<DocsControlPlane
activeLibrary={null}
activeSection=""
activeSlug=""
pageTitle="Choosing an adapter"
/>,
);

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(
Expand Down
20 changes: 13 additions & 7 deletions apps/website/src/components/docs/DocsControlPlane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -65,8 +66,11 @@ export function DocsContextContent({
<div data-docs-control-plane-context data-mobile={mobile || undefined}>
<ControlPlaneSection title="Scope" collapsible={false}>
<div className="docs-control-plane-scope">
<span>{library?.title ?? activeLibrary}</span>
<span>{section?.title ?? activeSection}</span>
{/* 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. */}
<span>{library?.title ?? 'Docs'}</span>
{library && section ? <span>{section.title}</span> : null}
<strong>{pageTitle}</strong>
</div>
</ControlPlaneSection>
Expand Down Expand Up @@ -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 (
<div className="docs-control-plane" data-docs-control-plane>
Expand Down
Loading
Loading