@raybase/docs 0.0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +35 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +29 -0
- package/dist/install.js.map +1 -0
- package/package.json +38 -8
- package/templates/astro-emdash/content/docs/getting-started.mdx +13 -0
- package/templates/astro-emdash/content/docs/index.mdx +13 -0
- package/templates/astro-emdash/content/docs/mcp.mdx +11 -0
- package/templates/astro-emdash/content/docs/meta.json +4 -0
- package/templates/astro-emdash/site/content/docs/getting-started.mdx +12 -0
- package/templates/astro-emdash/site/content/docs/index.mdx +12 -0
- package/templates/astro-emdash/site/content/docs/mcp.mdx +10 -0
- package/templates/astro-emdash/site/content/docs/meta.json +4 -0
- package/templates/astro-emdash/site/source.config.ts +10 -0
- package/templates/astro-emdash/site/src/docs/docs-assets.ts +1 -0
- package/templates/astro-emdash/site/src/docs/docs-client.tsx +22 -0
- package/templates/astro-emdash/site/src/docs/docs-data.ts +24 -0
- package/templates/astro-emdash/site/src/docs/docs-handler.tsx +105 -0
- package/templates/astro-emdash/site/src/docs/docs-layout.tsx +56 -0
- package/templates/astro-emdash/site/src/docs/docs-mdx.tsx +79 -0
- package/templates/astro-emdash/site/src/docs/docs-page-actions.tsx +146 -0
- package/templates/astro-emdash/site/src/docs/docs-page.tsx +70 -0
- package/templates/astro-emdash/site/src/docs/llm-text.ts +61 -0
- package/templates/astro-emdash/site/src/docs/source.ts +7 -0
- package/templates/astro-emdash/site/src/pages/docs/[...slug].ts +6 -0
- package/templates/astro-emdash/site/src/pages/docs/api/markdown.ts +4 -0
- package/templates/astro-emdash/site/src/pages/docs/api/search.ts +4 -0
- package/templates/astro-emdash/site/src/pages/llms.txt.ts +8 -0
- package/templates/astro-emdash/site/src/styles-docs.css +3 -0
- package/templates/astro-emdash/source.config.ts +10 -0
- package/templates/astro-emdash/src/docs/docs-assets.ts +1 -0
- package/templates/astro-emdash/src/docs/docs-client.tsx +22 -0
- package/templates/astro-emdash/src/docs/docs-data.ts +24 -0
- package/templates/astro-emdash/src/docs/docs-handler.tsx +105 -0
- package/templates/astro-emdash/src/docs/docs-layout.tsx +56 -0
- package/templates/astro-emdash/src/docs/docs-mdx.tsx +79 -0
- package/templates/astro-emdash/src/docs/docs-page-actions.tsx +146 -0
- package/templates/astro-emdash/src/docs/docs-page.tsx +70 -0
- package/templates/astro-emdash/src/docs/llm-text.ts +61 -0
- package/templates/astro-emdash/src/docs/source.ts +7 -0
- package/templates/astro-emdash/src/routes/docs/$.tsx +22 -0
- package/templates/astro-emdash/src/routes/docs/api/markdown.ts +12 -0
- package/templates/astro-emdash/src/routes/docs/api/search.ts +12 -0
- package/templates/astro-emdash/src/routes/docs.index.tsx +22 -0
- package/templates/astro-emdash/src/routes/docs.tsx +22 -0
- package/templates/astro-emdash/src/styles-docs.css +3 -0
- package/templates/react-router/app/components/mdx.tsx +79 -0
- package/templates/react-router/app/lib/layout.shared.tsx +54 -0
- package/templates/react-router/app/lib/source.ts +7 -0
- package/templates/react-router/app/routes/docs.$.tsx +49 -0
- package/templates/react-router/app/routes/docs.api.search.ts +10 -0
- package/templates/react-router/app/routes/docs.tsx +13 -0
- package/templates/react-router/content/docs/getting-started.mdx +8 -0
- package/templates/react-router/content/docs/index.mdx +8 -0
- package/templates/react-router/content/docs/mcp.mdx +121 -0
- package/templates/react-router/content/docs/meta.json +4 -0
- package/templates/react-router/source.config.ts +5 -0
- package/templates/tanstack-start/content/docs/getting-started.mdx +10 -0
- package/templates/tanstack-start/content/docs/index.mdx +8 -0
- package/templates/tanstack-start/content/docs/mcp.mdx +121 -0
- package/templates/tanstack-start/content/docs/meta.json +4 -0
- package/templates/tanstack-start/source.config.ts +10 -0
- package/templates/tanstack-start/src/docs/docs-assets.ts +1 -0
- package/templates/tanstack-start/src/docs/docs-client.tsx +22 -0
- package/templates/tanstack-start/src/docs/docs-data.ts +24 -0
- package/templates/tanstack-start/src/docs/docs-handler.tsx +105 -0
- package/templates/tanstack-start/src/docs/docs-layout.tsx +56 -0
- package/templates/tanstack-start/src/docs/docs-mdx.tsx +79 -0
- package/templates/tanstack-start/src/docs/docs-page-actions.tsx +146 -0
- package/templates/tanstack-start/src/docs/docs-page.tsx +70 -0
- package/templates/tanstack-start/src/docs/llm-text.ts +61 -0
- package/templates/tanstack-start/src/docs/source.ts +7 -0
- package/templates/tanstack-start/src/routes/docs/$.tsx +22 -0
- package/templates/tanstack-start/src/routes/docs/api/markdown.ts +12 -0
- package/templates/tanstack-start/src/routes/docs/api/search.ts +12 -0
- package/templates/tanstack-start/src/routes/docs.index.tsx +22 -0
- package/templates/tanstack-start/src/routes/docs.tsx +22 -0
- package/templates/tanstack-start/src/styles-docs.css +3 -0
- package/README.md +0 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Check, ChevronDown, Copy, ExternalLink, FileText, Sparkles } from "lucide-react";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
interface DocsPageActionsProps {
|
|
5
|
+
markdownUrl: string;
|
|
6
|
+
title: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DocsPageActions(props: DocsPageActionsProps) {
|
|
10
|
+
const [copyState, setCopyState] = useState<"idle" | "copying" | "copied" | "failed">("idle");
|
|
11
|
+
const [open, setOpen] = useState(false);
|
|
12
|
+
const [origin, setOrigin] = useState("");
|
|
13
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setOrigin(window.location.origin);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!open) return;
|
|
21
|
+
|
|
22
|
+
function onPointerDown(event: PointerEvent) {
|
|
23
|
+
if (!menuRef.current?.contains(event.target as Node)) {
|
|
24
|
+
setOpen(false);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
29
|
+
if (event.key === "Escape") {
|
|
30
|
+
setOpen(false);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
35
|
+
document.addEventListener("keydown", onKeyDown);
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
39
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
40
|
+
};
|
|
41
|
+
}, [open]);
|
|
42
|
+
|
|
43
|
+
const absoluteMarkdownUrl = useMemo(() => {
|
|
44
|
+
if (!origin) return props.markdownUrl;
|
|
45
|
+
return new URL(props.markdownUrl, origin).toString();
|
|
46
|
+
}, [origin, props.markdownUrl]);
|
|
47
|
+
|
|
48
|
+
const aiPrompt = useMemo(() => {
|
|
49
|
+
return `Read this documentation page and help me use it:\n\n${absoluteMarkdownUrl}`;
|
|
50
|
+
}, [absoluteMarkdownUrl]);
|
|
51
|
+
|
|
52
|
+
const options = [
|
|
53
|
+
{
|
|
54
|
+
label: "View as Markdown",
|
|
55
|
+
href: props.markdownUrl,
|
|
56
|
+
icon: FileText
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: "Open in ChatGPT",
|
|
60
|
+
href: `https://chatgpt.com/?q=${encodeURIComponent(aiPrompt)}`,
|
|
61
|
+
icon: Sparkles
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "Open in Claude",
|
|
65
|
+
href: `https://claude.ai/new?q=${encodeURIComponent(aiPrompt)}`,
|
|
66
|
+
icon: Sparkles
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const copyLabel = {
|
|
71
|
+
idle: "Copy as Markdown",
|
|
72
|
+
copying: "Copying",
|
|
73
|
+
copied: "Copied",
|
|
74
|
+
failed: "Copy failed"
|
|
75
|
+
}[copyState];
|
|
76
|
+
|
|
77
|
+
async function copyMarkdown() {
|
|
78
|
+
setCopyState("copying");
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(props.markdownUrl);
|
|
82
|
+
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`Failed to load Markdown for ${props.title}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await navigator.clipboard.writeText(await response.text());
|
|
88
|
+
setCopyState("copied");
|
|
89
|
+
} catch {
|
|
90
|
+
setCopyState("failed");
|
|
91
|
+
} finally {
|
|
92
|
+
window.setTimeout(() => setCopyState("idle"), 1600);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="not-prose my-6 flex flex-wrap items-center gap-2 border-y border-fd-border/70 py-3 text-sm">
|
|
98
|
+
<button
|
|
99
|
+
className="inline-flex h-9 items-center gap-2 rounded-md border border-fd-border bg-fd-background px-3 font-medium text-fd-foreground shadow-xs transition hover:bg-fd-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
|
|
100
|
+
disabled={copyState === "copying"}
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() => void copyMarkdown()}
|
|
103
|
+
>
|
|
104
|
+
{copyState === "copied" ? <Check className="size-4" aria-hidden="true" /> : <Copy className="size-4" aria-hidden="true" />}
|
|
105
|
+
{copyLabel}
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
<div className="relative" ref={menuRef}>
|
|
109
|
+
<button
|
|
110
|
+
aria-expanded={open}
|
|
111
|
+
aria-haspopup="menu"
|
|
112
|
+
className="inline-flex h-9 items-center gap-2 rounded-md border border-fd-border bg-fd-background px-3 font-medium text-fd-foreground shadow-xs transition hover:bg-fd-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={() => setOpen((value) => !value)}
|
|
115
|
+
>
|
|
116
|
+
Open
|
|
117
|
+
<ChevronDown className="size-4 text-fd-muted-foreground" aria-hidden="true" />
|
|
118
|
+
</button>
|
|
119
|
+
|
|
120
|
+
{open ? (
|
|
121
|
+
<div className="absolute left-0 top-10 z-20 w-56 overflow-hidden rounded-md border border-fd-border bg-fd-popover p-1 text-fd-popover-foreground shadow-lg" role="menu">
|
|
122
|
+
{options.map((option) => {
|
|
123
|
+
const Icon = option.icon;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<a
|
|
127
|
+
className="flex min-h-9 items-center gap-2 rounded-sm px-2.5 py-2 text-sm outline-none transition hover:bg-fd-muted focus-visible:bg-fd-muted"
|
|
128
|
+
href={option.href}
|
|
129
|
+
key={option.label}
|
|
130
|
+
onClick={() => setOpen(false)}
|
|
131
|
+
rel="noreferrer"
|
|
132
|
+
role="menuitem"
|
|
133
|
+
target="_blank"
|
|
134
|
+
>
|
|
135
|
+
<Icon className="size-4 text-fd-muted-foreground" aria-hidden="true" />
|
|
136
|
+
<span className="min-w-0 flex-1 truncate">{option.label}</span>
|
|
137
|
+
<ExternalLink className="size-3.5 text-fd-muted-foreground" aria-hidden="true" />
|
|
138
|
+
</a>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
) : null}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import browserCollections from "collections/browser";
|
|
2
|
+
import { FrameworkProvider } from "fumadocs-core/framework";
|
|
3
|
+
import { useFumadocsLoader, type SerializedPageTree } from "fumadocs-core/source/client";
|
|
4
|
+
import { DocsLayout } from "fumadocs-ui/layouts/docs";
|
|
5
|
+
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
|
|
6
|
+
import { RootProvider } from "fumadocs-ui/provider/base";
|
|
7
|
+
import { DocsPageActions } from "./docs-page-actions";
|
|
8
|
+
import { baseOptions } from "./docs-layout";
|
|
9
|
+
import { getMDXComponents } from "./docs-mdx";
|
|
10
|
+
|
|
11
|
+
export type DocsPageData = {
|
|
12
|
+
path: string;
|
|
13
|
+
url: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
pageTree: SerializedPageTree;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const clientLoader = browserCollections.docs.createClientLoader({
|
|
20
|
+
component({ toc, frontmatter, default: MDX }) {
|
|
21
|
+
const pageUrl = currentDocsData.url.replace(/\/$/, "") || "/docs";
|
|
22
|
+
const markdownUrl = pageUrl === "/docs" ? "/docs/index.md" : `${pageUrl}.md`;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<DocsPage toc={toc}>
|
|
26
|
+
<DocsTitle>{frontmatter.title}</DocsTitle>
|
|
27
|
+
<DocsDescription>{frontmatter.description}</DocsDescription>
|
|
28
|
+
<DocsPageActions markdownUrl={markdownUrl} title={frontmatter.title} />
|
|
29
|
+
<DocsBody>
|
|
30
|
+
<MDX components={getMDXComponents()} />
|
|
31
|
+
</DocsBody>
|
|
32
|
+
</DocsPage>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let currentDocsData: { url: string } = { url: "/docs" };
|
|
38
|
+
|
|
39
|
+
export async function preloadDocsContent(data: DocsPageData): Promise<void> {
|
|
40
|
+
currentDocsData = data;
|
|
41
|
+
await clientLoader.preload(data.path);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function DocsRoot({ data }: { data: DocsPageData }) {
|
|
45
|
+
currentDocsData = data;
|
|
46
|
+
const loaderData = useFumadocsLoader(data);
|
|
47
|
+
const content = clientLoader.useContent(loaderData.path);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<FrameworkProvider
|
|
51
|
+
Link={({ href, prefetch: _prefetch, ...props }) => <a href={href} {...props} />}
|
|
52
|
+
useParams={() => ({})}
|
|
53
|
+
usePathname={() => currentDocsData.url}
|
|
54
|
+
useRouter={() => ({
|
|
55
|
+
push(url) {
|
|
56
|
+
if (typeof window !== "undefined") window.location.href = url;
|
|
57
|
+
},
|
|
58
|
+
refresh() {
|
|
59
|
+
if (typeof window !== "undefined") window.location.reload();
|
|
60
|
+
}
|
|
61
|
+
})}
|
|
62
|
+
>
|
|
63
|
+
<RootProvider search={{ options: { api: "/docs/api/search" } }}>
|
|
64
|
+
<DocsLayout {...baseOptions()} tree={loaderData.pageTree}>
|
|
65
|
+
{content}
|
|
66
|
+
</DocsLayout>
|
|
67
|
+
</RootProvider>
|
|
68
|
+
</FrameworkProvider>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { source } from "./source";
|
|
2
|
+
|
|
3
|
+
type DocsPage = ReturnType<typeof source.getPage>;
|
|
4
|
+
|
|
5
|
+
export async function getDocsMarkdownResponse(path: string | null) {
|
|
6
|
+
const page = source.getPage(getDocsSlugs(path));
|
|
7
|
+
|
|
8
|
+
if (!page) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return markdownResponse(await getLLMText(page));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function getLLMText(page: NonNullable<DocsPage>) {
|
|
16
|
+
const markdown = renderRuntimeMdxPlaceholders((await page.data.getText("processed")) ?? "");
|
|
17
|
+
|
|
18
|
+
return `# ${page.data.title} (${page.url})\n\n${markdown}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderRuntimeMdxPlaceholders(markdown: string) {
|
|
22
|
+
return markdown
|
|
23
|
+
.replace(/<McpServerUrl(Block)?([^>]*)\/>/g, (_match, block: string | undefined, attrs: string) => {
|
|
24
|
+
const value = `[current-app-origin]${mdxStringAttribute(attrs, "path") ?? "/mcp"}`;
|
|
25
|
+
return block ? `\`\`\`text\n${value}\n\`\`\`` : `\`${value}\``;
|
|
26
|
+
})
|
|
27
|
+
.replace(/<ClaudeMcpAddCommand([^>]*)\/>/g, (_match, attrs: string) => {
|
|
28
|
+
const path = mdxStringAttribute(attrs, "path") ?? "/mcp";
|
|
29
|
+
const name = mdxStringAttribute(attrs, "name") ?? "this-app";
|
|
30
|
+
return `\`\`\`bash\nclaude mcp add --transport http ${name} "[current-app-origin]${path}"\n\`\`\``;
|
|
31
|
+
})
|
|
32
|
+
.replace(/<CodexMcpAddCommand([^>]*)\/>/g, (_match, attrs: string) => {
|
|
33
|
+
const path = mdxStringAttribute(attrs, "path") ?? "/mcp";
|
|
34
|
+
const name = mdxStringAttribute(attrs, "name") ?? "this-app";
|
|
35
|
+
return `\`\`\`bash\ncodex mcp add ${name} --url "[current-app-origin]${path}"\n\`\`\``;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mdxStringAttribute(attrs: string, name: string) {
|
|
40
|
+
const match = attrs.match(new RegExp(`\\b${name}="([^"]*)"`));
|
|
41
|
+
return match?.[1] ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function markdownResponse(markdown: string) {
|
|
45
|
+
return new Response(markdown, {
|
|
46
|
+
headers: {
|
|
47
|
+
"content-type": "text/markdown; charset=utf-8"
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getDocsSlugs(path: string | null) {
|
|
53
|
+
const pathname = path?.trim() || "/docs";
|
|
54
|
+
const withoutBase = pathname
|
|
55
|
+
.replace(/\.mdx?$/, "")
|
|
56
|
+
.replace(/^\/docs(?:\/|$)/, "")
|
|
57
|
+
.replace(/^\/+|\/+$/g, "");
|
|
58
|
+
const withoutIndex = withoutBase === "index" ? "" : withoutBase.replace(/\/index$/, "");
|
|
59
|
+
|
|
60
|
+
return withoutIndex ? withoutIndex.split("/").filter(Boolean) : [];
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/docs/$")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
GET: async ({ request }) => {
|
|
8
|
+
const { handleDocsRequest } = await import("../../docs/docs-handler");
|
|
9
|
+
return handleDocsRequest(request);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
component: DocumentNavigationFallback
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function DocumentNavigationFallback() {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
window.location.assign(window.location.href);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
|
|
3
|
+
export const Route = createFileRoute("/docs/api/markdown")({
|
|
4
|
+
server: {
|
|
5
|
+
handlers: {
|
|
6
|
+
GET: async ({ request }) => {
|
|
7
|
+
const { handleDocsRequest } = await import("../../../docs/docs-handler");
|
|
8
|
+
return handleDocsRequest(request);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
|
|
3
|
+
export const Route = createFileRoute("/docs/api/search")({
|
|
4
|
+
server: {
|
|
5
|
+
handlers: {
|
|
6
|
+
GET: async ({ request }) => {
|
|
7
|
+
const { handleDocsRequest } = await import("../../../docs/docs-handler");
|
|
8
|
+
return handleDocsRequest(request);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/docs/")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
GET: async ({ request }) => {
|
|
8
|
+
const { handleDocsRequest } = await import("../docs/docs-handler");
|
|
9
|
+
return handleDocsRequest(request);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
component: DocumentNavigationFallback
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function DocumentNavigationFallback() {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
window.location.assign(window.location.href);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/docs")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
GET: async ({ request }) => {
|
|
8
|
+
const { handleDocsRequest } = await import("../docs/docs-handler");
|
|
9
|
+
return handleDocsRequest(request);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
component: DocumentNavigationFallback
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function DocumentNavigationFallback() {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
window.location.assign(window.location.href);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock";
|
|
2
|
+
import defaultMdxComponents from "fumadocs-ui/mdx";
|
|
3
|
+
import type { MDXComponents } from "mdx/types";
|
|
4
|
+
|
|
5
|
+
interface McpServerUrlProps {
|
|
6
|
+
path?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ClaudeMcpAddCommandProps extends McpServerUrlProps {
|
|
10
|
+
name?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CodexMcpAddCommandProps extends McpServerUrlProps {
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function browserOrigin() {
|
|
18
|
+
return typeof window === "undefined" ? "" : window.location.origin;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function currentUrl(path = "/mcp") {
|
|
22
|
+
const origin = browserOrigin();
|
|
23
|
+
return origin ? new URL(path, origin).toString() : path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function McpServerUrl(props: McpServerUrlProps) {
|
|
27
|
+
return <code suppressHydrationWarning>{currentUrl(props.path)}</code>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function McpServerUrlBlock(props: McpServerUrlProps) {
|
|
31
|
+
return (
|
|
32
|
+
<CodeBlock>
|
|
33
|
+
<Pre>
|
|
34
|
+
<code suppressHydrationWarning>{currentUrl(props.path)}</code>
|
|
35
|
+
</Pre>
|
|
36
|
+
</CodeBlock>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ClaudeMcpAddCommand(props: ClaudeMcpAddCommandProps) {
|
|
41
|
+
const command = `claude mcp add --transport http ${props.name ?? "this-app"} "${currentUrl(props.path)}"`;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<CodeBlock>
|
|
45
|
+
<Pre>
|
|
46
|
+
<code suppressHydrationWarning>{command}</code>
|
|
47
|
+
</Pre>
|
|
48
|
+
</CodeBlock>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function CodexMcpAddCommand(props: CodexMcpAddCommandProps) {
|
|
53
|
+
const command = `codex mcp add ${props.name ?? "this-app"} --url "${currentUrl(props.path)}"`;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<CodeBlock>
|
|
57
|
+
<Pre>
|
|
58
|
+
<code suppressHydrationWarning>{command}</code>
|
|
59
|
+
</Pre>
|
|
60
|
+
</CodeBlock>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getMDXComponents(components?: MDXComponents) {
|
|
65
|
+
return {
|
|
66
|
+
...defaultMdxComponents,
|
|
67
|
+
McpServerUrl,
|
|
68
|
+
McpServerUrlBlock,
|
|
69
|
+
ClaudeMcpAddCommand,
|
|
70
|
+
CodexMcpAddCommand,
|
|
71
|
+
...components
|
|
72
|
+
} satisfies MDXComponents;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const useMDXComponents = getMDXComponents;
|
|
76
|
+
|
|
77
|
+
declare global {
|
|
78
|
+
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
|
|
79
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { DocsLayoutProps } from "fumadocs-ui/layouts/docs";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
import { ArrowLeft } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
const appName = "__APP_NAME__";
|
|
6
|
+
const appHomeHref = "/";
|
|
7
|
+
|
|
8
|
+
export function baseOptions(): Omit<DocsLayoutProps, "children" | "tree"> {
|
|
9
|
+
return {
|
|
10
|
+
nav: {
|
|
11
|
+
title: DocsNavTitle,
|
|
12
|
+
url: appHomeHref
|
|
13
|
+
},
|
|
14
|
+
sidebar: {
|
|
15
|
+
footer: <DocsSidebarFooter />
|
|
16
|
+
},
|
|
17
|
+
themeSwitch: {
|
|
18
|
+
enabled: false
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function DocsNavTitle({ className, href = appHomeHref, ...props }: ComponentProps<"a">) {
|
|
24
|
+
return (
|
|
25
|
+
<div className={className}>
|
|
26
|
+
<div className="inline-flex min-w-0 flex-col items-start gap-0.5 leading-none">
|
|
27
|
+
<a
|
|
28
|
+
{...props}
|
|
29
|
+
href={href}
|
|
30
|
+
title={`Back to ${appName}`}
|
|
31
|
+
className="max-w-44 truncate rounded-sm text-[0.9375rem] font-semibold leading-none text-fd-foreground underline-offset-4 transition-colors hover:text-fd-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
|
|
32
|
+
>
|
|
33
|
+
{appName}
|
|
34
|
+
</a>
|
|
35
|
+
<span className="text-[0.6875rem] font-medium leading-none text-fd-muted-foreground">Docs</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function DocsSidebarFooter() {
|
|
42
|
+
return (
|
|
43
|
+
<div className="mt-2 flex min-w-0 items-center border-t pt-3">
|
|
44
|
+
<a
|
|
45
|
+
href={appHomeHref}
|
|
46
|
+
title={`Back to ${appName}`}
|
|
47
|
+
className="inline-flex min-w-0 items-center gap-1.5 rounded-md px-1 py-1 text-xs font-medium text-fd-muted-foreground transition-colors hover:text-fd-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
|
|
48
|
+
>
|
|
49
|
+
<ArrowLeft className="size-3.5 shrink-0" aria-hidden="true" />
|
|
50
|
+
<span className="truncate">Back to app</span>
|
|
51
|
+
</a>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useLoaderData } from "react-router";
|
|
2
|
+
import { useFumadocsLoader } from "fumadocs-core/source/client";
|
|
3
|
+
import browserCollections from "collections/browser";
|
|
4
|
+
import { DocsLayout } from "fumadocs-ui/layouts/docs";
|
|
5
|
+
import {
|
|
6
|
+
DocsBody,
|
|
7
|
+
DocsDescription,
|
|
8
|
+
DocsPage,
|
|
9
|
+
DocsTitle
|
|
10
|
+
} from "fumadocs-ui/layouts/docs/page";
|
|
11
|
+
import { getMDXComponents } from "../components/mdx";
|
|
12
|
+
import { baseOptions } from "../lib/layout.shared";
|
|
13
|
+
import { source } from "../lib/source";
|
|
14
|
+
|
|
15
|
+
export async function loader({ params }: { params: { "*": string | undefined } }) {
|
|
16
|
+
const slugs = params["*"]?.split("/").filter(Boolean) ?? [];
|
|
17
|
+
const page = source.getPage(slugs);
|
|
18
|
+
if (!page) throw new Response("Not found", { status: 404 });
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
path: page.path,
|
|
22
|
+
pageTree: await source.serializePageTree(source.getPageTree())
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const clientLoader = browserCollections.docs.createClientLoader({
|
|
27
|
+
component({ toc, frontmatter, default: MDX }) {
|
|
28
|
+
return (
|
|
29
|
+
<DocsPage toc={toc}>
|
|
30
|
+
<DocsTitle>{frontmatter.title}</DocsTitle>
|
|
31
|
+
<DocsDescription>{frontmatter.description}</DocsDescription>
|
|
32
|
+
<DocsBody>
|
|
33
|
+
<MDX components={getMDXComponents()} />
|
|
34
|
+
</DocsBody>
|
|
35
|
+
</DocsPage>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export default function DocsRoute() {
|
|
41
|
+
const data = useFumadocsLoader(useLoaderData<typeof loader>());
|
|
42
|
+
const content = clientLoader.useContent(data.path);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<DocsLayout {...baseOptions()} tree={data.pageTree}>
|
|
46
|
+
{content}
|
|
47
|
+
</DocsLayout>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFromSource } from "fumadocs-core/search/server";
|
|
2
|
+
import { source } from "../lib/source";
|
|
3
|
+
|
|
4
|
+
const search = createFromSource(source, {
|
|
5
|
+
language: "english"
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export async function loader({ request }: { request: Request }) {
|
|
9
|
+
return search.GET(request);
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Outlet } from "react-router";
|
|
2
|
+
import { RootProvider } from "fumadocs-ui/provider/react-router";
|
|
3
|
+
|
|
4
|
+
export default function DocsLayoutRoute() {
|
|
5
|
+
return (
|
|
6
|
+
<RootProvider
|
|
7
|
+
search={{ options: { api: "/docs/api/search" } }}
|
|
8
|
+
theme={{ enabled: false }}
|
|
9
|
+
>
|
|
10
|
+
<Outlet />
|
|
11
|
+
</RootProvider>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Getting started
|
|
3
|
+
description: Replace this page with the first real workflow guide.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Replace this placeholder with the first workflow a real user or operator needs.
|
|
7
|
+
|
|
8
|
+
Document behavior only after it exists in the app. Avoid promising screens, APIs, permissions, or automations that have not been implemented.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Welcome to the docs
|
|
3
|
+
description: Start here when replacing the placeholder documentation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use this space for product, operator, and API documentation once those workflows exist.
|
|
7
|
+
|
|
8
|
+
Keep docs in `content/docs`, and update the page tree as the documentation grows.
|