Skip to content

Commit ec239b1

Browse files
committed
Add v1 reference selector and content
1 parent 9756697 commit ec239b1

29 files changed

Lines changed: 2609 additions & 187 deletions

showcase/shell-docs/src/app/reference/[...slug]/page.tsx

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { Metadata } from "next";
2+
import type React from "react";
23
import Link from "next/link";
34
import { notFound } from "next/navigation";
45
import { MDXRemote } from "next-mdx-remote/rsc";
56
import matter from "gray-matter";
7+
import { LinkIcon } from "lucide-react";
68
import { PropertyReference } from "@/components/property-reference";
79
import {
810
Callout,
@@ -19,14 +21,16 @@ import {
1921
DocsDescription,
2022
} from "fumadocs-ui/page";
2123
import { ShellDocsLayout } from "@/components/shell-docs-layout";
22-
import type * as PageTree from "fumadocs-core/page-tree";
24+
import { ReferenceVersionSelector } from "@/components/reference-version-selector";
2325
import {
24-
REFERENCE_CONTENT_DIR,
25-
loadAllReferenceItems,
26+
REFERENCE_VERSIONS,
27+
buildReferencePageTree,
28+
referenceHref,
2629
referenceStaticParams,
30+
referenceVersionHref,
31+
resolveReferencePage,
2732
} from "@/lib/reference-items";
2833
import { stripLeadingImports } from "@/lib/docs-render";
29-
import { safeReadFileSync } from "@/lib/safe-fs";
3034
import { buildDocMetadata } from "@/lib/seo-metadata";
3135

3236
// Self-canonical for /reference/<slug>. Reference pages are not
@@ -41,11 +45,8 @@ export async function generateMetadata({
4145
params: Promise<{ slug: string[] }>;
4246
}): Promise<Metadata> {
4347
const { slug } = await params;
44-
const slugPath = slug.join("/");
45-
const canonicalPath = `/reference/${slugPath}`;
46-
// Read the reference MDX directly to extract frontmatter. Reuse
47-
// safeReadFileSync so a crafted slug can't escape REFERENCE_CONTENT_DIR.
48-
const raw = safeReadFileSync(REFERENCE_CONTENT_DIR, `${slugPath}.mdx`);
48+
const resolved = resolveReferencePage(slug);
49+
const raw = resolved?.raw ?? null;
4950
let title: string | undefined;
5051
let description: string | undefined;
5152
if (raw !== null) {
@@ -64,7 +65,9 @@ export async function generateMetadata({
6465
return buildDocMetadata({
6566
title: title ?? slug[slug.length - 1],
6667
description,
67-
canonicalPath,
68+
canonicalPath: resolved
69+
? referenceHref(resolved.version, resolved.pageSlug)
70+
: `/reference/${slug.join("/")}`,
6871
});
6972
}
7073

@@ -77,6 +80,12 @@ const mdxComponents = {
7780
Accordions,
7881
Accordion,
7982
OpsPlatformCTA,
83+
LinkIcon,
84+
Frame: ({ children }: { children: React.ReactNode }) => (
85+
<div className="my-6 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-4">
86+
{children}
87+
</div>
88+
),
8089
// Strip unknown imports — MDX import statements become no-ops in next-mdx-remote
8190
};
8291

@@ -90,15 +99,12 @@ export default async function ReferenceSlugPage({
9099
params: Promise<{ slug: string[] }>;
91100
}) {
92101
const { slug } = await params;
93-
const slugPath = slug.join("/");
94-
// slugPath is user-supplied (URL segments). Route the filesystem read
95-
// through safeReadFileSync so crafted paths like `..%2F..%2Fsecrets`
96-
// can't escape REFERENCE_CONTENT_DIR.
97-
const raw = safeReadFileSync(REFERENCE_CONTENT_DIR, `${slugPath}.mdx`);
98-
if (raw === null) {
102+
const resolved = resolveReferencePage(slug);
103+
if (resolved === null) {
99104
notFound();
100105
}
101106

107+
const { version, pageSlug, contentSlug, raw } = resolved;
102108
let content = "";
103109
let data: Record<string, unknown> = {};
104110
try {
@@ -107,43 +113,36 @@ export default async function ReferenceSlugPage({
107113
data = parsed.data;
108114
} catch (err) {
109115
console.error(
110-
`[reference] Failed to parse frontmatter in ${slugPath}.mdx:`,
116+
`[reference] Failed to parse frontmatter in ${contentSlug}.mdx:`,
111117
err,
112118
);
113119
notFound();
114120
}
115121

116122
const cleanedContent = stripLeadingImports(content);
117123

118-
const allItems = loadAllReferenceItems();
119124
const title =
120125
typeof data.title === "string" && data.title.length > 0
121126
? data.title
122127
: slug[slug.length - 1];
123128
const description =
124129
typeof data.description === "string" ? data.description : undefined;
125-
126-
// Build a Fumadocs PageTree from the reference items, grouped by
127-
// category. Reference's IA is its own (Components / Hooks) — we don't
128-
// share the docs nav tree here.
129-
const pageTree: PageTree.Root = {
130-
name: "Reference",
131-
children: ["Components", "Hooks"].flatMap((cat) => [
132-
{ type: "separator" as const, name: cat },
133-
...allItems
134-
.filter((i) => i.category === cat)
135-
.map(
136-
(item): PageTree.Item => ({
137-
type: "page",
138-
name: item.title,
139-
url: `/reference/${item.slug}`,
140-
}),
141-
),
142-
]),
143-
};
130+
const pageTree = buildReferencePageTree(version);
131+
const versionOptions = REFERENCE_VERSIONS.map((referenceVersion) => ({
132+
version: referenceVersion,
133+
href: referenceVersionHref(referenceVersion, pageSlug),
134+
}));
144135

145136
return (
146-
<ShellDocsLayout tree={pageTree}>
137+
<ShellDocsLayout
138+
tree={pageTree}
139+
banner={
140+
<ReferenceVersionSelector
141+
activeVersion={version}
142+
options={versionOptions}
143+
/>
144+
}
145+
>
147146
<DocsPage
148147
toc={[]}
149148
tableOfContent={{ enabled: false }}
@@ -161,7 +160,15 @@ export default async function ReferenceSlugPage({
161160
Reference
162161
</Link>
163162
{" / "}
164-
<span className="capitalize">{slug[0]}</span>
163+
<span>{version}</span>
164+
{pageSlug && (
165+
<>
166+
{" / "}
167+
<span className="capitalize">
168+
{pageSlug.split("/")[0]}
169+
</span>
170+
</>
171+
)}
165172
</div>
166173
<DocsTitle className="text-2xl font-bold">{title}</DocsTitle>
167174
{description && (

showcase/shell-docs/src/app/reference/page.tsx

Lines changed: 66 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,50 @@
11
import Link from "next/link";
2-
import fs from "fs";
3-
import path from "path";
4-
import matter from "gray-matter";
52
import { DocsPage } from "fumadocs-ui/page";
63
import { ShellDocsLayout } from "@/components/shell-docs-layout";
7-
import type * as PageTree from "fumadocs-core/page-tree";
4+
import { ReferenceVersionSelector } from "@/components/reference-version-selector";
85
import {
9-
REFERENCE_CONTENT_DIR,
10-
loadReferenceItems,
11-
loadAllReferenceItems,
6+
REFERENCE_CATEGORIES,
7+
REFERENCE_VERSIONS,
8+
buildReferencePageTree,
9+
loadReferenceVersionItems,
10+
referenceVersionHref,
11+
readReferenceIndexDescription,
1212
} from "@/lib/reference-items";
13+
import type { ReferenceCategory, ReferenceItem } from "@/lib/reference-items";
1314

14-
export default function ReferencePage() {
15-
const components = loadReferenceItems("components");
16-
const hooks = loadReferenceItems("hooks");
17-
const allItems = loadAllReferenceItems();
15+
function displayTitle(item: ReferenceItem): string {
16+
if (item.category === "Components") return `<${item.title} />`;
17+
if (item.category === "Hooks") return `${item.title}()`;
18+
return item.title;
19+
}
1820

19-
// Mirror the PageTree built by `/reference/[...slug]` so the sidebar
20-
// chrome is identical between the index and the per-item pages.
21-
const pageTree: PageTree.Root = {
22-
name: "Reference",
23-
children: ["Components", "Hooks"].flatMap((cat) => [
24-
{ type: "separator" as const, name: cat },
25-
...allItems
26-
.filter((i) => i.category === cat)
27-
.map(
28-
(item): PageTree.Item => ({
29-
type: "page",
30-
name: item.title,
31-
url: `/reference/${item.slug}`,
32-
}),
33-
),
34-
]),
35-
};
21+
function categoryItems(
22+
items: ReferenceItem[],
23+
category: ReferenceCategory,
24+
): ReferenceItem[] {
25+
return items.filter((item) => item.category === category);
26+
}
3627

37-
// Also load the index page frontmatter for the intro. Guarded so a
38-
// malformed frontmatter block falls back to a default rather than
39-
// crashing the whole index page.
40-
let intro = "API Reference for the next-generation CopilotKit React API.";
41-
const indexPath = path.join(REFERENCE_CONTENT_DIR, "index.mdx");
42-
if (fs.existsSync(indexPath)) {
43-
try {
44-
const { data } = matter(fs.readFileSync(indexPath, "utf-8"));
45-
if (typeof data.description === "string" && data.description.length > 0) {
46-
intro = data.description;
47-
}
48-
} catch (err) {
49-
console.error(
50-
`[reference] Failed to parse frontmatter in ${indexPath}:`,
51-
err,
52-
);
53-
}
54-
}
28+
export default function ReferencePage() {
29+
const activeVersion = "v2";
30+
const allItems = loadReferenceVersionItems(activeVersion);
31+
const pageTree = buildReferencePageTree(activeVersion);
32+
const intro = readReferenceIndexDescription(activeVersion);
33+
const versionOptions = REFERENCE_VERSIONS.map((version) => ({
34+
version,
35+
href: referenceVersionHref(version),
36+
}));
5537

5638
return (
57-
<ShellDocsLayout tree={pageTree}>
39+
<ShellDocsLayout
40+
tree={pageTree}
41+
banner={
42+
<ReferenceVersionSelector
43+
activeVersion={activeVersion}
44+
options={versionOptions}
45+
/>
46+
}
47+
>
5848
<DocsPage
5949
toc={[]}
6050
tableOfContent={{ enabled: false }}
@@ -68,55 +58,36 @@ export default function ReferencePage() {
6858
</h1>
6959
<p className="text-[var(--text-muted)] text-sm mb-10">{intro}</p>
7060

71-
<section className="mb-10">
72-
<h2 className="text-lg font-semibold text-[var(--text)] mb-4">
73-
UI Components
74-
</h2>
75-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
76-
{components.map((item) => (
77-
<Link
78-
key={item.slug}
79-
href={`/reference/${item.slug}`}
80-
className="block rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-4 hover:bg-[var(--bg-elevated)] transition-colors"
81-
>
82-
<div className="font-mono text-sm font-semibold text-[var(--accent)]">
83-
{"<"}
84-
{item.title}
85-
{" />"}
86-
</div>
87-
{item.description && (
88-
<div className="text-xs text-[var(--text-muted)] mt-1">
89-
{item.description}
90-
</div>
91-
)}
92-
</Link>
93-
))}
94-
</div>
95-
</section>
61+
{REFERENCE_CATEGORIES.map((category) => {
62+
const items = categoryItems(allItems, category);
63+
if (items.length === 0) return null;
9664

97-
<section>
98-
<h2 className="text-lg font-semibold text-[var(--text)] mb-4">
99-
Hooks
100-
</h2>
101-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
102-
{hooks.map((item) => (
103-
<Link
104-
key={item.slug}
105-
href={`/reference/${item.slug}`}
106-
className="block rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-4 hover:bg-[var(--bg-elevated)] transition-colors"
107-
>
108-
<div className="font-mono text-sm font-semibold text-[var(--accent)]">
109-
{item.title}()
110-
</div>
111-
{item.description && (
112-
<div className="text-xs text-[var(--text-muted)] mt-1">
113-
{item.description}
114-
</div>
115-
)}
116-
</Link>
117-
))}
118-
</div>
119-
</section>
65+
return (
66+
<section key={category} className="mb-10 last:mb-0">
67+
<h2 className="text-lg font-semibold text-[var(--text)] mb-4">
68+
{category === "Components" ? "UI Components" : category}
69+
</h2>
70+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
71+
{items.map((item) => (
72+
<Link
73+
key={item.slug}
74+
href={item.url}
75+
className="block rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-4 hover:bg-[var(--bg-elevated)] transition-colors"
76+
>
77+
<div className="font-mono text-sm font-semibold text-[var(--accent)]">
78+
{displayTitle(item)}
79+
</div>
80+
{item.description && (
81+
<div className="text-xs text-[var(--text-muted)] mt-1">
82+
{item.description}
83+
</div>
84+
)}
85+
</Link>
86+
))}
87+
</div>
88+
</section>
89+
);
90+
})}
12091
</div>
12192
</DocsPage>
12293
</ShellDocsLayout>

0 commit comments

Comments
 (0)