@assistant-ui/docs-theme 0.0.0 → 0.0.3
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/README.md +225 -0
- package/dist/action-icons.d.ts +5 -0
- package/dist/action-icons.d.ts.map +1 -0
- package/dist/action-icons.js +52 -0
- package/dist/action-icons.js.map +1 -0
- package/dist/brand.d.ts +7 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +37 -0
- package/dist/brand.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -0
- package/dist/header.d.ts +12 -0
- package/dist/header.d.ts.map +1 -0
- package/dist/header.js +73 -0
- package/dist/header.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation.d.ts +23 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +41 -0
- package/dist/navigation.js.map +1 -0
- package/dist/next.d.ts +29 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +63 -0
- package/dist/next.js.map +1 -0
- package/dist/page-content.d.ts +12 -0
- package/dist/page-content.d.ts.map +1 -0
- package/dist/page-content.js +274 -0
- package/dist/page-content.js.map +1 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +1 -0
- package/dist/site/constants.d.ts +2 -0
- package/dist/site/constants.d.ts.map +1 -0
- package/dist/site/constants.js +5 -0
- package/dist/site/constants.js.map +1 -0
- package/dist/site/footer.d.ts +13 -0
- package/dist/site/footer.d.ts.map +1 -0
- package/dist/site/footer.js +35 -0
- package/dist/site/footer.js.map +1 -0
- package/dist/site/header.d.ts +13 -0
- package/dist/site/header.d.ts.map +1 -0
- package/dist/site/header.js +81 -0
- package/dist/site/header.js.map +1 -0
- package/dist/site/layout.d.ts +8 -0
- package/dist/site/layout.d.ts.map +1 -0
- package/dist/site/layout.js +21 -0
- package/dist/site/layout.js.map +1 -0
- package/dist/site/page.d.ts +9 -0
- package/dist/site/page.d.ts.map +1 -0
- package/dist/site/page.js +32 -0
- package/dist/site/page.js.map +1 -0
- package/dist/site/pager.d.ts +13 -0
- package/dist/site/pager.d.ts.map +1 -0
- package/dist/site/pager.js +36 -0
- package/dist/site/pager.js.map +1 -0
- package/dist/site/sidebar-navigation.d.ts +13 -0
- package/dist/site/sidebar-navigation.d.ts.map +1 -0
- package/dist/site/sidebar-navigation.js +185 -0
- package/dist/site/sidebar-navigation.js.map +1 -0
- package/dist/site/sidebar.d.ts +16 -0
- package/dist/site/sidebar.d.ts.map +1 -0
- package/dist/site/sidebar.js +46 -0
- package/dist/site/sidebar.js.map +1 -0
- package/dist/site/toc.d.ts +12 -0
- package/dist/site/toc.d.ts.map +1 -0
- package/dist/site/toc.js +92 -0
- package/dist/site/toc.js.map +1 -0
- package/dist/site/tree.d.ts +24 -0
- package/dist/site/tree.d.ts.map +1 -0
- package/dist/site/tree.js +14 -0
- package/dist/site/tree.js.map +1 -0
- package/dist/site/utils.d.ts +2 -0
- package/dist/site/utils.d.ts.map +1 -0
- package/dist/site/utils.js +5 -0
- package/dist/site/utils.js.map +1 -0
- package/dist/ui.d.ts +10 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +9 -0
- package/package.json +101 -1
- package/src/action-icons.tsx +48 -0
- package/src/brand.tsx +36 -0
- package/src/config.ts +23 -0
- package/src/header.test.tsx +41 -0
- package/src/header.tsx +94 -0
- package/src/index.test.ts +37 -0
- package/src/index.ts +38 -0
- package/src/layout-hosts.test.tsx +98 -0
- package/src/navigation.test.ts +123 -0
- package/src/navigation.ts +61 -0
- package/src/next.test.tsx +110 -0
- package/src/next.tsx +101 -0
- package/src/page-content.test.tsx +87 -0
- package/src/page-content.tsx +416 -0
- package/src/provider.ts +1 -0
- package/src/site/constants.ts +1 -0
- package/src/site/footer.tsx +59 -0
- package/src/site/header.test.tsx +89 -0
- package/src/site/header.tsx +117 -0
- package/src/site/layout.tsx +33 -0
- package/src/site/page.tsx +38 -0
- package/src/site/pager.tsx +54 -0
- package/src/site/sidebar-navigation.test.tsx +92 -0
- package/src/site/sidebar-navigation.tsx +267 -0
- package/src/site/sidebar.tsx +77 -0
- package/src/site/toc.test.tsx +187 -0
- package/src/site/toc.tsx +118 -0
- package/src/site/tree.ts +36 -0
- package/src/site/utils.ts +2 -0
- package/src/ui.ts +19 -0
- package/styles/site.css +223 -0
- package/styles/theme.css +858 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
useSyncExternalStore,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
11
|
+
import Link from "next/link";
|
|
12
|
+
import { usePathname } from "next/navigation";
|
|
13
|
+
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
14
|
+
import { useTreeContext } from "fumadocs-ui/contexts/tree";
|
|
15
|
+
import { useTOCItems } from "fumadocs-ui/components/toc";
|
|
16
|
+
import {
|
|
17
|
+
Check,
|
|
18
|
+
CircleAlert,
|
|
19
|
+
ChevronLeft,
|
|
20
|
+
ChevronRight,
|
|
21
|
+
Copy,
|
|
22
|
+
FileText,
|
|
23
|
+
MoreHorizontal,
|
|
24
|
+
Pencil,
|
|
25
|
+
} from "lucide-react";
|
|
26
|
+
import { ClaudeIcon, McpIcon, OpenAIIcon } from "./action-icons";
|
|
27
|
+
|
|
28
|
+
import { getEditUrl, getPageNeighbors } from "./navigation";
|
|
29
|
+
import type { createDocsLayout } from "./next";
|
|
30
|
+
|
|
31
|
+
type PageLink = getPageNeighbors.Link;
|
|
32
|
+
|
|
33
|
+
const documentSubscribers = new Set<() => void>();
|
|
34
|
+
let documentObserver: MutationObserver | undefined;
|
|
35
|
+
|
|
36
|
+
const subscribeToDocument = (onChange: () => void) => {
|
|
37
|
+
documentSubscribers.add(onChange);
|
|
38
|
+
if (!documentObserver) {
|
|
39
|
+
documentObserver = new MutationObserver(() => {
|
|
40
|
+
for (const subscriber of documentSubscribers) subscriber();
|
|
41
|
+
});
|
|
42
|
+
documentObserver.observe(document.body, { childList: true, subtree: true });
|
|
43
|
+
}
|
|
44
|
+
return () => {
|
|
45
|
+
documentSubscribers.delete(onChange);
|
|
46
|
+
if (documentSubscribers.size === 0) {
|
|
47
|
+
documentObserver?.disconnect();
|
|
48
|
+
documentObserver = undefined;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const useDocumentElement = (selector: string | null) =>
|
|
54
|
+
useSyncExternalStore(
|
|
55
|
+
subscribeToDocument,
|
|
56
|
+
() => (selector ? document.querySelector<HTMLElement>(selector) : null),
|
|
57
|
+
() => null,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const subscribeToOrigin = () => () => {};
|
|
61
|
+
const getOrigin = () => window.location.origin;
|
|
62
|
+
const getServerOrigin = () => "";
|
|
63
|
+
|
|
64
|
+
const PageActionLabel = ({
|
|
65
|
+
title,
|
|
66
|
+
description,
|
|
67
|
+
}: {
|
|
68
|
+
title: string;
|
|
69
|
+
description: string;
|
|
70
|
+
}) => (
|
|
71
|
+
<span className="aui-docs-action-label">
|
|
72
|
+
<span>{title}</span>
|
|
73
|
+
<span>{description}</span>
|
|
74
|
+
</span>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const DocsPageControls = ({
|
|
78
|
+
previous,
|
|
79
|
+
next,
|
|
80
|
+
title,
|
|
81
|
+
pathname,
|
|
82
|
+
markdownUrl,
|
|
83
|
+
siteName,
|
|
84
|
+
editUrl,
|
|
85
|
+
mcpUrl,
|
|
86
|
+
}: {
|
|
87
|
+
previous: PageLink | undefined;
|
|
88
|
+
next: PageLink | undefined;
|
|
89
|
+
title: string;
|
|
90
|
+
pathname: string;
|
|
91
|
+
markdownUrl: string;
|
|
92
|
+
siteName: string;
|
|
93
|
+
editUrl: string | undefined;
|
|
94
|
+
mcpUrl: string | undefined;
|
|
95
|
+
}) => {
|
|
96
|
+
const toc = useTOCItems();
|
|
97
|
+
// The upstream Farming Labs and Fumadocs layouts supply these portal hosts.
|
|
98
|
+
const headerHost = useDocumentElement("#nd-page .fd-generated-page-header");
|
|
99
|
+
const tocHost = useDocumentElement(toc.length > 0 ? "#nd-toc" : null);
|
|
100
|
+
const [message, setMessage] = useState("");
|
|
101
|
+
const [copying, setCopying] = useState(false);
|
|
102
|
+
const origin = useSyncExternalStore(
|
|
103
|
+
subscribeToOrigin,
|
|
104
|
+
getOrigin,
|
|
105
|
+
getServerOrigin,
|
|
106
|
+
);
|
|
107
|
+
const markdown = useRef<Promise<string> | null>(null);
|
|
108
|
+
const githubUrl = editUrl?.replaceAll("{path}", pathname);
|
|
109
|
+
const publishedUrl = `${origin}${markdownUrl}`;
|
|
110
|
+
const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(`Read ${publishedUrl} (the ${siteName} documentation page "${title}") so I can ask questions about it.`)}`;
|
|
111
|
+
|
|
112
|
+
const loadMarkdown = () => {
|
|
113
|
+
markdown.current ??= fetch(markdownUrl)
|
|
114
|
+
.then((response) => {
|
|
115
|
+
if (!response.ok) throw new Error("Could not load this page");
|
|
116
|
+
return response.text();
|
|
117
|
+
})
|
|
118
|
+
.catch((error: unknown) => {
|
|
119
|
+
markdown.current = null;
|
|
120
|
+
throw error;
|
|
121
|
+
});
|
|
122
|
+
return markdown.current;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const copy = async (kind: "page" | "mcp") => {
|
|
126
|
+
setCopying(true);
|
|
127
|
+
try {
|
|
128
|
+
const text =
|
|
129
|
+
kind === "page"
|
|
130
|
+
? await loadMarkdown()
|
|
131
|
+
: new URL(mcpUrl ?? "", window.location.origin).href;
|
|
132
|
+
await navigator.clipboard.writeText(text);
|
|
133
|
+
setMessage(kind === "page" ? "Page copied" : "MCP server URL copied");
|
|
134
|
+
} catch {
|
|
135
|
+
setMessage("Could not copy. Please try again.");
|
|
136
|
+
} finally {
|
|
137
|
+
setCopying(false);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!message) return;
|
|
143
|
+
const timer = window.setTimeout(() => setMessage(""), 3000);
|
|
144
|
+
return () => window.clearTimeout(timer);
|
|
145
|
+
}, [message]);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<>
|
|
149
|
+
{headerHost &&
|
|
150
|
+
createPortal(
|
|
151
|
+
<nav
|
|
152
|
+
className="aui-docs-page-controls not-prose"
|
|
153
|
+
aria-label="Page controls"
|
|
154
|
+
>
|
|
155
|
+
{previous ? (
|
|
156
|
+
<Link
|
|
157
|
+
href={previous.url}
|
|
158
|
+
className="aui-docs-pager-button"
|
|
159
|
+
aria-label="Previous page"
|
|
160
|
+
title={
|
|
161
|
+
typeof previous.name === "string" ||
|
|
162
|
+
typeof previous.name === "number"
|
|
163
|
+
? `Previous: ${previous.name}`
|
|
164
|
+
: "Previous page"
|
|
165
|
+
}
|
|
166
|
+
>
|
|
167
|
+
<ChevronLeft aria-hidden="true" />
|
|
168
|
+
</Link>
|
|
169
|
+
) : (
|
|
170
|
+
<button
|
|
171
|
+
className="aui-docs-pager-button"
|
|
172
|
+
aria-label="Previous page"
|
|
173
|
+
disabled
|
|
174
|
+
>
|
|
175
|
+
<ChevronLeft aria-hidden="true" />
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
{next ? (
|
|
179
|
+
<Link
|
|
180
|
+
href={next.url}
|
|
181
|
+
className="aui-docs-pager-button"
|
|
182
|
+
aria-label="Next page"
|
|
183
|
+
title={
|
|
184
|
+
typeof next.name === "string" || typeof next.name === "number"
|
|
185
|
+
? `Next: ${next.name}`
|
|
186
|
+
: "Next page"
|
|
187
|
+
}
|
|
188
|
+
>
|
|
189
|
+
<ChevronRight aria-hidden="true" />
|
|
190
|
+
</Link>
|
|
191
|
+
) : (
|
|
192
|
+
<button
|
|
193
|
+
className="aui-docs-pager-button"
|
|
194
|
+
aria-label="Next page"
|
|
195
|
+
disabled
|
|
196
|
+
>
|
|
197
|
+
<ChevronRight aria-hidden="true" />
|
|
198
|
+
</button>
|
|
199
|
+
)}
|
|
200
|
+
<DropdownMenu.Root
|
|
201
|
+
onOpenChange={(open) => {
|
|
202
|
+
if (open) void loadMarkdown().catch(() => {});
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<DropdownMenu.Trigger
|
|
206
|
+
className="aui-docs-pager-button"
|
|
207
|
+
aria-label="More page actions"
|
|
208
|
+
>
|
|
209
|
+
<MoreHorizontal aria-hidden="true" />
|
|
210
|
+
</DropdownMenu.Trigger>
|
|
211
|
+
<DropdownMenu.Portal>
|
|
212
|
+
<DropdownMenu.Content
|
|
213
|
+
className="aui-docs-actions-menu"
|
|
214
|
+
align="end"
|
|
215
|
+
sideOffset={4}
|
|
216
|
+
collisionPadding={16}
|
|
217
|
+
>
|
|
218
|
+
<DropdownMenu.Item
|
|
219
|
+
className="aui-docs-actions-menu-item"
|
|
220
|
+
disabled={copying}
|
|
221
|
+
onSelect={() => void copy("page")}
|
|
222
|
+
>
|
|
223
|
+
<Copy aria-hidden="true" />
|
|
224
|
+
<PageActionLabel
|
|
225
|
+
title={copying ? "Copying..." : "Copy page"}
|
|
226
|
+
description="Markdown, ready to paste into an LLM"
|
|
227
|
+
/>
|
|
228
|
+
</DropdownMenu.Item>
|
|
229
|
+
<DropdownMenu.Item
|
|
230
|
+
className="aui-docs-actions-menu-item"
|
|
231
|
+
asChild
|
|
232
|
+
>
|
|
233
|
+
<a
|
|
234
|
+
href={markdownUrl}
|
|
235
|
+
target="_blank"
|
|
236
|
+
rel="noreferrer noopener"
|
|
237
|
+
>
|
|
238
|
+
<FileText aria-hidden="true" />
|
|
239
|
+
<PageActionLabel
|
|
240
|
+
title="View as Markdown"
|
|
241
|
+
description="This page as plain text"
|
|
242
|
+
/>
|
|
243
|
+
</a>
|
|
244
|
+
</DropdownMenu.Item>
|
|
245
|
+
<DropdownMenu.Item
|
|
246
|
+
className="aui-docs-actions-menu-item"
|
|
247
|
+
asChild
|
|
248
|
+
>
|
|
249
|
+
<a
|
|
250
|
+
href={claudeUrl}
|
|
251
|
+
target="_blank"
|
|
252
|
+
rel="noreferrer noopener"
|
|
253
|
+
>
|
|
254
|
+
<ClaudeIcon />
|
|
255
|
+
<PageActionLabel
|
|
256
|
+
title="Open in Claude"
|
|
257
|
+
description="Ask Claude about this page"
|
|
258
|
+
/>
|
|
259
|
+
</a>
|
|
260
|
+
</DropdownMenu.Item>
|
|
261
|
+
<DropdownMenu.Item
|
|
262
|
+
className="aui-docs-actions-menu-item"
|
|
263
|
+
asChild
|
|
264
|
+
>
|
|
265
|
+
<a
|
|
266
|
+
href="https://chatgpt.com/codex/"
|
|
267
|
+
target="_blank"
|
|
268
|
+
rel="noreferrer noopener"
|
|
269
|
+
>
|
|
270
|
+
<OpenAIIcon />
|
|
271
|
+
<PageActionLabel
|
|
272
|
+
title="Open in Codex"
|
|
273
|
+
description="Open Codex in ChatGPT"
|
|
274
|
+
/>
|
|
275
|
+
</a>
|
|
276
|
+
</DropdownMenu.Item>
|
|
277
|
+
{mcpUrl && (
|
|
278
|
+
<>
|
|
279
|
+
<DropdownMenu.Separator className="aui-docs-actions-menu-separator" />
|
|
280
|
+
<DropdownMenu.Item
|
|
281
|
+
className="aui-docs-actions-menu-item"
|
|
282
|
+
onSelect={() => void copy("mcp")}
|
|
283
|
+
>
|
|
284
|
+
<McpIcon />
|
|
285
|
+
<PageActionLabel
|
|
286
|
+
title="Copy MCP server URL"
|
|
287
|
+
description="Use these docs from any MCP client"
|
|
288
|
+
/>
|
|
289
|
+
</DropdownMenu.Item>
|
|
290
|
+
</>
|
|
291
|
+
)}
|
|
292
|
+
</DropdownMenu.Content>
|
|
293
|
+
</DropdownMenu.Portal>
|
|
294
|
+
</DropdownMenu.Root>
|
|
295
|
+
</nav>,
|
|
296
|
+
headerHost,
|
|
297
|
+
)}
|
|
298
|
+
{tocHost &&
|
|
299
|
+
createPortal(
|
|
300
|
+
<div className="aui-docs-toc-actions not-prose">
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
onClick={() => void copy("page")}
|
|
304
|
+
disabled={copying}
|
|
305
|
+
>
|
|
306
|
+
<Copy aria-hidden="true" />
|
|
307
|
+
{copying ? "Copying..." : "Copy page"}
|
|
308
|
+
</button>
|
|
309
|
+
<a href={markdownUrl} target="_blank" rel="noreferrer noopener">
|
|
310
|
+
<FileText aria-hidden="true" />
|
|
311
|
+
View as Markdown
|
|
312
|
+
</a>
|
|
313
|
+
{githubUrl && (
|
|
314
|
+
<a href={githubUrl} target="_blank" rel="noreferrer noopener">
|
|
315
|
+
<Pencil aria-hidden="true" />
|
|
316
|
+
Edit on GitHub
|
|
317
|
+
</a>
|
|
318
|
+
)}
|
|
319
|
+
</div>,
|
|
320
|
+
tocHost,
|
|
321
|
+
)}
|
|
322
|
+
<output className="aui-docs-action-status" aria-live="polite">
|
|
323
|
+
{message && (
|
|
324
|
+
<span>
|
|
325
|
+
{message.startsWith("Could not") ? (
|
|
326
|
+
<CircleAlert aria-hidden="true" />
|
|
327
|
+
) : (
|
|
328
|
+
<Check aria-hidden="true" />
|
|
329
|
+
)}
|
|
330
|
+
{message}
|
|
331
|
+
</span>
|
|
332
|
+
)}
|
|
333
|
+
</output>
|
|
334
|
+
</>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export const DocsPageContent = ({
|
|
339
|
+
children,
|
|
340
|
+
name,
|
|
341
|
+
github,
|
|
342
|
+
editUrl,
|
|
343
|
+
mcpUrl,
|
|
344
|
+
footer,
|
|
345
|
+
}: {
|
|
346
|
+
children: ReactNode;
|
|
347
|
+
name: string;
|
|
348
|
+
github: getEditUrl.Options | undefined;
|
|
349
|
+
editUrl: string | undefined;
|
|
350
|
+
mcpUrl: string | undefined;
|
|
351
|
+
footer: createDocsLayout.Options["footer"];
|
|
352
|
+
}) => {
|
|
353
|
+
const pathname = usePathname().replace(/\/$/, "");
|
|
354
|
+
const { root } = useTreeContext();
|
|
355
|
+
const { previous, next, current } = getPageNeighbors(root.children, pathname);
|
|
356
|
+
const title = current?.name;
|
|
357
|
+
const pageUrl = new URL(current?.url ?? pathname, "https://docs.local");
|
|
358
|
+
const markdownUrl = `${pathname}.md${pageUrl.search}`;
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div className="aui-docs-page-content">
|
|
362
|
+
<DocsPageControls
|
|
363
|
+
key={markdownUrl}
|
|
364
|
+
pathname={pathname}
|
|
365
|
+
markdownUrl={markdownUrl}
|
|
366
|
+
title={typeof title === "string" ? title : name}
|
|
367
|
+
siteName={name}
|
|
368
|
+
editUrl={editUrl ?? getEditUrl(pathname, github)}
|
|
369
|
+
mcpUrl={mcpUrl}
|
|
370
|
+
previous={previous}
|
|
371
|
+
next={next}
|
|
372
|
+
/>
|
|
373
|
+
<div className="aui-docs-page-body">{children}</div>
|
|
374
|
+
{(previous || next) && (
|
|
375
|
+
<nav
|
|
376
|
+
className="aui-docs-page-navigation not-prose"
|
|
377
|
+
aria-label="Page navigation"
|
|
378
|
+
>
|
|
379
|
+
{previous ? (
|
|
380
|
+
<Link href={previous.url} rel="prev">
|
|
381
|
+
<ChevronLeft aria-hidden="true" />
|
|
382
|
+
<span>
|
|
383
|
+
{previous.section && (
|
|
384
|
+
<span className="aui-docs-page-section">
|
|
385
|
+
{previous.section} /{" "}
|
|
386
|
+
</span>
|
|
387
|
+
)}
|
|
388
|
+
{previous.name}
|
|
389
|
+
</span>
|
|
390
|
+
</Link>
|
|
391
|
+
) : (
|
|
392
|
+
<span />
|
|
393
|
+
)}
|
|
394
|
+
{next ? (
|
|
395
|
+
<Link href={next.url} rel="next">
|
|
396
|
+
<span>
|
|
397
|
+
{next.section && (
|
|
398
|
+
<span className="aui-docs-page-section">
|
|
399
|
+
{next.section} /{" "}
|
|
400
|
+
</span>
|
|
401
|
+
)}
|
|
402
|
+
{next.name}
|
|
403
|
+
</span>
|
|
404
|
+
<ChevronRight aria-hidden="true" />
|
|
405
|
+
</Link>
|
|
406
|
+
) : (
|
|
407
|
+
<span />
|
|
408
|
+
)}
|
|
409
|
+
</nav>
|
|
410
|
+
)}
|
|
411
|
+
{footer && (
|
|
412
|
+
<footer className="aui-docs-legal-footer not-prose">{footer}</footer>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
};
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RootProvider } from "@farming-labs/theme";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DOCS_SIDEBAR_WIDTH = 260;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
type FooterItem = {
|
|
6
|
+
name: ReactNode;
|
|
7
|
+
url: string;
|
|
8
|
+
section?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type DocsFooterProps = {
|
|
12
|
+
previous?: FooterItem | undefined;
|
|
13
|
+
next?: FooterItem | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function DocsFooter({ previous, next }: DocsFooterProps) {
|
|
17
|
+
if (!previous && !next) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<nav
|
|
21
|
+
aria-label="Previous and next page"
|
|
22
|
+
className="not-prose mt-16 flex items-center justify-between gap-4 text-sm"
|
|
23
|
+
>
|
|
24
|
+
{previous ? (
|
|
25
|
+
<Link
|
|
26
|
+
href={previous.url}
|
|
27
|
+
className="group text-muted-foreground hover:text-foreground inline-flex min-w-0 items-center gap-1.5 transition-colors"
|
|
28
|
+
>
|
|
29
|
+
<ChevronLeft className="size-4 shrink-0 transition-transform group-hover:-translate-x-0.5" />
|
|
30
|
+
<span className="min-w-0 truncate">
|
|
31
|
+
{previous.section ? (
|
|
32
|
+
<span className="opacity-60">{previous.section} / </span>
|
|
33
|
+
) : null}
|
|
34
|
+
{previous.name}
|
|
35
|
+
</span>
|
|
36
|
+
</Link>
|
|
37
|
+
) : (
|
|
38
|
+
<span />
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{next ? (
|
|
42
|
+
<Link
|
|
43
|
+
href={next.url}
|
|
44
|
+
className="group text-muted-foreground hover:text-foreground inline-flex min-w-0 items-center gap-1.5 transition-colors"
|
|
45
|
+
>
|
|
46
|
+
<span className="min-w-0 truncate">
|
|
47
|
+
{next.section ? (
|
|
48
|
+
<span className="opacity-60">{next.section} / </span>
|
|
49
|
+
) : null}
|
|
50
|
+
{next.name}
|
|
51
|
+
</span>
|
|
52
|
+
<ChevronRight className="size-4 shrink-0 transition-transform group-hover:translate-x-0.5" />
|
|
53
|
+
</Link>
|
|
54
|
+
) : (
|
|
55
|
+
<span />
|
|
56
|
+
)}
|
|
57
|
+
</nav>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, Fragment, useState } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { expect, it, vi } from "vitest";
|
|
6
|
+
import { DocsSiteHeader } from "./header";
|
|
7
|
+
import { DocsSidebar, DocsSidebarProvider } from "./sidebar";
|
|
8
|
+
|
|
9
|
+
function SidebarState() {
|
|
10
|
+
const [value, setValue] = useState(0);
|
|
11
|
+
return <button onClick={() => setValue(value + 1)}>Section {value}</button>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
it("keeps one sidebar mounted and closes the competing mobile menu", async () => {
|
|
15
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
16
|
+
vi.stubGlobal("matchMedia", () => ({
|
|
17
|
+
addEventListener() {},
|
|
18
|
+
removeEventListener() {},
|
|
19
|
+
}));
|
|
20
|
+
const container = document.createElement("div");
|
|
21
|
+
document.body.append(container);
|
|
22
|
+
const root = createRoot(container);
|
|
23
|
+
const button = (label: string) =>
|
|
24
|
+
container.querySelector<HTMLButtonElement>(
|
|
25
|
+
`button[aria-label="${label}"]`,
|
|
26
|
+
)!;
|
|
27
|
+
try {
|
|
28
|
+
await act(async () =>
|
|
29
|
+
root.render(
|
|
30
|
+
<DocsSidebarProvider>
|
|
31
|
+
<DocsSiteHeader
|
|
32
|
+
brand="Project"
|
|
33
|
+
mobileControls={null}
|
|
34
|
+
condensedNavigation={null}
|
|
35
|
+
desktopNavigation={null}
|
|
36
|
+
navigationRoot={Fragment}
|
|
37
|
+
scrolled={false}
|
|
38
|
+
mobileNavigation={(close) => (
|
|
39
|
+
<button aria-label="Navigate" onClick={close}>
|
|
40
|
+
Home
|
|
41
|
+
</button>
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
<DocsSidebar>
|
|
45
|
+
<SidebarState />
|
|
46
|
+
</DocsSidebar>
|
|
47
|
+
</DocsSidebarProvider>,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
expect(button("Toggle sidebar").getAttribute("aria-controls")).toBe(
|
|
51
|
+
container.querySelector("aside")?.id,
|
|
52
|
+
);
|
|
53
|
+
const navigation = document.getElementById(
|
|
54
|
+
button("Site navigation").getAttribute("aria-controls")!,
|
|
55
|
+
);
|
|
56
|
+
expect(navigation?.hasAttribute("inert")).toBe(true);
|
|
57
|
+
await act(async () => button("Toggle sidebar").click());
|
|
58
|
+
expect(button("Toggle sidebar").getAttribute("aria-expanded")).toBe("true");
|
|
59
|
+
await act(async () =>
|
|
60
|
+
container.querySelector<HTMLButtonElement>("aside button")!.click(),
|
|
61
|
+
);
|
|
62
|
+
await act(async () => button("Site navigation").click());
|
|
63
|
+
expect(button("Site navigation").getAttribute("aria-expanded")).toBe(
|
|
64
|
+
"true",
|
|
65
|
+
);
|
|
66
|
+
expect(navigation?.hasAttribute("inert")).toBe(false);
|
|
67
|
+
expect(button("Toggle sidebar").getAttribute("aria-expanded")).toBe(
|
|
68
|
+
"false",
|
|
69
|
+
);
|
|
70
|
+
await act(async () => button("Toggle sidebar").click());
|
|
71
|
+
expect(button("Site navigation").getAttribute("aria-expanded")).toBe(
|
|
72
|
+
"false",
|
|
73
|
+
);
|
|
74
|
+
expect(container.querySelectorAll("aside")).toHaveLength(1);
|
|
75
|
+
expect(container.querySelector("aside button")?.textContent).toBe(
|
|
76
|
+
"Section 1",
|
|
77
|
+
);
|
|
78
|
+
await act(async () => button("Site navigation").click());
|
|
79
|
+
await act(async () => button("Navigate").click());
|
|
80
|
+
expect(button("Site navigation").getAttribute("aria-expanded")).toBe(
|
|
81
|
+
"false",
|
|
82
|
+
);
|
|
83
|
+
expect(document.activeElement).toBe(button("Site navigation"));
|
|
84
|
+
} finally {
|
|
85
|
+
await act(async () => root.unmount());
|
|
86
|
+
container.remove();
|
|
87
|
+
vi.unstubAllGlobals();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useId,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ComponentType,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { Menu, PanelLeft, X } from "lucide-react";
|
|
11
|
+
import { DOCS_SIDEBAR_ID, useDocsSidebar } from "./sidebar";
|
|
12
|
+
import { cn } from "./utils";
|
|
13
|
+
|
|
14
|
+
export function DocsSiteHeader({
|
|
15
|
+
brand,
|
|
16
|
+
mobileControls,
|
|
17
|
+
condensedNavigation,
|
|
18
|
+
desktopNavigation,
|
|
19
|
+
mobileNavigation,
|
|
20
|
+
navigationRoot: NavigationRoot,
|
|
21
|
+
scrolled,
|
|
22
|
+
}: {
|
|
23
|
+
brand: ReactNode;
|
|
24
|
+
mobileControls: ReactNode;
|
|
25
|
+
condensedNavigation: ReactNode;
|
|
26
|
+
desktopNavigation: ReactNode;
|
|
27
|
+
mobileNavigation: (close: () => void) => ReactNode;
|
|
28
|
+
navigationRoot: ComponentType<{ children: ReactNode }>;
|
|
29
|
+
scrolled: boolean;
|
|
30
|
+
}) {
|
|
31
|
+
const {
|
|
32
|
+
open: sidebarOpen,
|
|
33
|
+
setOpen: setSidebarOpen,
|
|
34
|
+
toggle: toggleSidebar,
|
|
35
|
+
} = useDocsSidebar();
|
|
36
|
+
const [navMenuOpen, setNavMenuOpen] = useState(false);
|
|
37
|
+
const navMenuId = useId();
|
|
38
|
+
const navMenuToggle = useRef<HTMLButtonElement>(null);
|
|
39
|
+
const closeNavigation = () => {
|
|
40
|
+
setNavMenuOpen(false);
|
|
41
|
+
navMenuToggle.current?.focus();
|
|
42
|
+
};
|
|
43
|
+
const toggleNavigation = () => {
|
|
44
|
+
if (!navMenuOpen) setSidebarOpen(false);
|
|
45
|
+
setNavMenuOpen((open) => !open);
|
|
46
|
+
};
|
|
47
|
+
const toggleMenu = () => {
|
|
48
|
+
if (!sidebarOpen) setNavMenuOpen(false);
|
|
49
|
+
toggleSidebar();
|
|
50
|
+
};
|
|
51
|
+
return (
|
|
52
|
+
<header className="sticky top-0 z-50">
|
|
53
|
+
<NavigationRoot>
|
|
54
|
+
<div
|
|
55
|
+
className={cn(
|
|
56
|
+
"relative z-10 transition-colors duration-200",
|
|
57
|
+
scrolled ? "bg-background/80 backdrop-blur-md" : "bg-transparent",
|
|
58
|
+
"group-data-[menu-open=true]:bg-background",
|
|
59
|
+
"flex h-12 w-full items-center px-4",
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<div className="flex min-w-0 flex-1 items-center">{brand}</div>
|
|
63
|
+
<div className="ml-auto flex shrink-0 items-center gap-1 md:hidden">
|
|
64
|
+
{mobileControls}
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
ref={navMenuToggle}
|
|
68
|
+
onClick={toggleNavigation}
|
|
69
|
+
className="text-muted-foreground hover:text-foreground flex size-8 items-center justify-center transition-colors"
|
|
70
|
+
aria-label="Site navigation"
|
|
71
|
+
aria-expanded={navMenuOpen}
|
|
72
|
+
aria-controls={navMenuId}
|
|
73
|
+
>
|
|
74
|
+
{navMenuOpen ? (
|
|
75
|
+
<X className="size-4" />
|
|
76
|
+
) : (
|
|
77
|
+
<Menu className="size-4" />
|
|
78
|
+
)}
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={toggleMenu}
|
|
83
|
+
className="text-muted-foreground hover:text-foreground flex size-8 items-center justify-center transition-colors"
|
|
84
|
+
aria-label="Toggle sidebar"
|
|
85
|
+
aria-expanded={sidebarOpen}
|
|
86
|
+
aria-controls={DOCS_SIDEBAR_ID}
|
|
87
|
+
>
|
|
88
|
+
{sidebarOpen ? (
|
|
89
|
+
<X className="size-4" />
|
|
90
|
+
) : (
|
|
91
|
+
<PanelLeft className="size-4" />
|
|
92
|
+
)}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="ml-auto hidden items-center gap-4 md:flex lg:hidden">
|
|
96
|
+
{condensedNavigation}
|
|
97
|
+
</div>
|
|
98
|
+
<div className="ml-auto hidden items-center gap-4 lg:flex">
|
|
99
|
+
{desktopNavigation}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div
|
|
103
|
+
className={cn(
|
|
104
|
+
"bg-background fixed inset-x-0 top-12 bottom-0 z-40 transition-opacity duration-200 md:hidden",
|
|
105
|
+
navMenuOpen ? "opacity-100" : "pointer-events-none opacity-0",
|
|
106
|
+
)}
|
|
107
|
+
id={navMenuId}
|
|
108
|
+
inert={!navMenuOpen}
|
|
109
|
+
>
|
|
110
|
+
<div className="flex h-full flex-col gap-1 overflow-y-auto px-4 pt-4">
|
|
111
|
+
{mobileNavigation(closeNavigation)}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</NavigationRoot>
|
|
115
|
+
</header>
|
|
116
|
+
);
|
|
117
|
+
}
|