diff --git a/website/src/app/docs/[...slug]/page.tsx b/website/src/app/docs/[...slug]/page.tsx index 55693042..9d1369a8 100644 --- a/website/src/app/docs/[...slug]/page.tsx +++ b/website/src/app/docs/[...slug]/page.tsx @@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm'; import rehypeSlug from 'rehype-slug'; import { getDocBySlug, getDocSlugs, getAdjacentDocs, extractHeadings, stripCodeFences } from '@/lib/docs'; import { DOCS_SIDEBAR } from '@/lib/constants'; +import { PROJECT } from '@/data/metrics'; import { Sidebar } from '@/components/docs/Sidebar'; import { Toc } from '@/components/docs/Toc'; import { DocNavigation } from '@/components/docs/DocNavigation'; @@ -18,6 +19,15 @@ function getReadingTime(text: string): number { return Math.max(1, Math.ceil(words / 200)); } +function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }); +} + interface PageProps { params: Promise<{ slug: string[] }>; } @@ -104,44 +114,57 @@ export default async function DocPage({ params }: PageProps) { <>
- {/* Sidebar */} -
- -
- {/* Mobile sidebar */} -
- -
+
+ {/* Main content */}
{/* Breadcrumbs */} - {/* Reading time */} -
+ {/* Meta: reading time, last updated, version, edit link */} +
+ {readingTime} min read + + + Last updated{' '} + + + + {PROJECT.version} + + + Edit this page +
+ + {/* Content */}
{content} @@ -156,6 +179,7 @@ export default async function DocPage({ params }: PageProps) {
+
); } diff --git a/website/src/app/docs/search-index.json/route.ts b/website/src/app/docs/search-index.json/route.ts new file mode 100644 index 00000000..3ed4eb13 --- /dev/null +++ b/website/src/app/docs/search-index.json/route.ts @@ -0,0 +1,30 @@ +import { DOCS_SIDEBAR } from '@/lib/constants'; +import { extractHeadings, getDocBySlug } from '@/lib/docs'; +import type { SearchEntry } from '@/lib/search-index'; + +// Generated once at build time; fetched lazily by the search modal. +export const dynamic = 'force-static'; + +export function GET() { + const entries: SearchEntry[] = []; + + for (const group of DOCS_SIDEBAR) { + for (const item of group.items) { + const base = { + slug: item.slug, + title: item.label, + category: group.category, + description: item.description, + }; + entries.push(base); + + const doc = getDocBySlug(item.slug); + if (!doc) continue; + for (const h of extractHeadings(doc.content)) { + entries.push({ slug: base.slug, title: base.title, category: base.category, heading: h.text, anchor: h.id }); + } + } + } + + return Response.json(entries); +} diff --git a/website/src/components/docs/DocNavigation.tsx b/website/src/components/docs/DocNavigation.tsx index 86f405a2..b0f287ef 100644 --- a/website/src/components/docs/DocNavigation.tsx +++ b/website/src/components/docs/DocNavigation.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; interface NavItem { slug: string; title: string; + category?: string; } interface DocNavigationProps { @@ -33,6 +34,7 @@ export function DocNavigation({ prev, next }: DocNavigationProps) { {prev.title} + {prev.category && {prev.category}} ) : (
@@ -57,6 +59,7 @@ export function DocNavigation({ prev, next }: DocNavigationProps) { + {next.category && {next.category}} ) : (
diff --git a/website/src/components/docs/Sidebar.tsx b/website/src/components/docs/Sidebar.tsx index ef2c5baa..94dc6609 100644 --- a/website/src/components/docs/Sidebar.tsx +++ b/website/src/components/docs/Sidebar.tsx @@ -76,36 +76,60 @@ export function Sidebar() { ); + useEffect(() => { + if (!mobileOpen) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setMobileOpen(false); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [mobileOpen]); + return ( - <> - {/* Mobile toggle */} - + + Docs menu + +
{/* Mobile overlay */} {mobileOpen && (
setMobileOpen(false)} /> )} - {/* Sidebar */} + {/* Sidebar (sticky column on lg, drawer below) */} - +
); } diff --git a/website/src/components/docs/Toc.tsx b/website/src/components/docs/Toc.tsx index b0d8da39..3c975308 100644 --- a/website/src/components/docs/Toc.tsx +++ b/website/src/components/docs/Toc.tsx @@ -4,9 +4,11 @@ import { useEffect, useState } from 'react'; interface TocProps { headings: { id: string; text: string; level: number }[]; + /** `rail` is the sticky xl sidebar; `collapsible` is the inline disclosure shown below xl. */ + variant?: 'rail' | 'collapsible'; } -export function Toc({ headings }: TocProps) { +export function Toc({ headings, variant = 'rail' }: TocProps) { const [activeId, setActiveId] = useState(''); useEffect(() => { @@ -36,11 +38,7 @@ export function Toc({ headings }: TocProps) { if (headings.length === 0) return null; - return ( -