@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,33 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { DOCS_SIDEBAR_WIDTH } from "./constants";
|
|
3
|
+
|
|
4
|
+
export function DocsShell({ children }: { children: ReactNode }): ReactNode {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
className="docs-shell"
|
|
8
|
+
style={
|
|
9
|
+
{
|
|
10
|
+
"--sidebar-width": `${DOCS_SIDEBAR_WIDTH}px`,
|
|
11
|
+
} as React.CSSProperties
|
|
12
|
+
}
|
|
13
|
+
>
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function DocsContent({ children }: { children: ReactNode }): ReactNode {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
data-docs-content=""
|
|
23
|
+
className="@container md:ml-(--sidebar-width)"
|
|
24
|
+
style={
|
|
25
|
+
{
|
|
26
|
+
"--sidebar-width": `${DOCS_SIDEBAR_WIDTH}px`,
|
|
27
|
+
} as React.CSSProperties
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { cn } from "./utils";
|
|
3
|
+
|
|
4
|
+
export function DocsPageShell({
|
|
5
|
+
toc,
|
|
6
|
+
footer,
|
|
7
|
+
children,
|
|
8
|
+
mainProps,
|
|
9
|
+
}: {
|
|
10
|
+
toc?: ReactNode;
|
|
11
|
+
footer?: ReactNode;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
mainProps?: Omit<ComponentProps<"main">, "children">;
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="docs-layout">
|
|
17
|
+
<main
|
|
18
|
+
{...mainProps}
|
|
19
|
+
className={cn(
|
|
20
|
+
"mx-auto w-full max-w-(--docs-article-width) justify-self-center px-4 pt-4 pb-10 md:px-6",
|
|
21
|
+
mainProps?.className,
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</main>
|
|
26
|
+
{toc}
|
|
27
|
+
{footer && (
|
|
28
|
+
<footer className="mx-auto w-full max-w-(--docs-article-width) justify-self-center px-4 pb-10 md:px-6">
|
|
29
|
+
<div className="border-t pt-6">{footer}</div>
|
|
30
|
+
</footer>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function DocsBody({ className, ...props }: ComponentProps<"article">) {
|
|
37
|
+
return <article {...props} className={cn("docs-prose prose", className)} />;
|
|
38
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
const pagerLabel = (direction: "Previous" | "Next", name: ReactNode) =>
|
|
6
|
+
typeof name === "string" && name !== ""
|
|
7
|
+
? `${direction}: ${name}`
|
|
8
|
+
: `${direction} page`;
|
|
9
|
+
|
|
10
|
+
export function DocsPagerNavigation({
|
|
11
|
+
previous,
|
|
12
|
+
next,
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
previous?: { url: string; name?: ReactNode } | undefined;
|
|
16
|
+
next?: { url: string; name?: ReactNode } | undefined;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
}) {
|
|
19
|
+
const buttonClass =
|
|
20
|
+
"flex size-7 items-center justify-center rounded-md bg-muted/50 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground sm:size-8";
|
|
21
|
+
const disabledClass =
|
|
22
|
+
"flex size-7 items-center justify-center rounded-md bg-muted/30 text-muted-foreground/40 cursor-not-allowed sm:size-8";
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex items-center gap-1">
|
|
25
|
+
{previous ? (
|
|
26
|
+
<Link
|
|
27
|
+
href={previous.url}
|
|
28
|
+
className={buttonClass}
|
|
29
|
+
aria-label={pagerLabel("Previous", previous.name)}
|
|
30
|
+
>
|
|
31
|
+
<ChevronLeft className="size-4" />
|
|
32
|
+
</Link>
|
|
33
|
+
) : (
|
|
34
|
+
<div className={disabledClass}>
|
|
35
|
+
<ChevronLeft className="size-4" />
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
{next ? (
|
|
39
|
+
<Link
|
|
40
|
+
href={next.url}
|
|
41
|
+
className={buttonClass}
|
|
42
|
+
aria-label={pagerLabel("Next", next.name)}
|
|
43
|
+
>
|
|
44
|
+
<ChevronRight className="size-4" />
|
|
45
|
+
</Link>
|
|
46
|
+
) : (
|
|
47
|
+
<div className={disabledClass}>
|
|
48
|
+
<ChevronRight className="size-4" />
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
6
|
+
import { DocsSidebarNavigation } from "./sidebar-navigation";
|
|
7
|
+
import type { DocsFolder } from "./tree";
|
|
8
|
+
|
|
9
|
+
const sections: DocsFolder[] = [
|
|
10
|
+
{
|
|
11
|
+
type: "folder",
|
|
12
|
+
$id: "start",
|
|
13
|
+
name: "Getting started",
|
|
14
|
+
children: [
|
|
15
|
+
{ type: "page", $id: "intro", name: "Overview", url: "/docs" },
|
|
16
|
+
{
|
|
17
|
+
type: "folder",
|
|
18
|
+
$id: "install",
|
|
19
|
+
name: "Install",
|
|
20
|
+
index: { type: "page", name: "Install", url: "/docs/install" },
|
|
21
|
+
children: [
|
|
22
|
+
{
|
|
23
|
+
type: "page",
|
|
24
|
+
$id: "next",
|
|
25
|
+
name: "Next.js",
|
|
26
|
+
url: "/docs/install/next",
|
|
27
|
+
},
|
|
28
|
+
{ type: "page", $id: "vue", name: "Vue", url: "/docs/install/vue" },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: "folder",
|
|
35
|
+
$id: "guides",
|
|
36
|
+
name: "Guides",
|
|
37
|
+
children: [
|
|
38
|
+
{ type: "page", $id: "tools", name: "Tools", url: "/docs/tools" },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
44
|
+
|
|
45
|
+
it("opens the active hierarchy, filters host-specific pages, and follows route changes after manual collapse", async () => {
|
|
46
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
47
|
+
const container = document.createElement("div");
|
|
48
|
+
document.body.append(container);
|
|
49
|
+
const root = createRoot(container);
|
|
50
|
+
const onNavigate = vi.fn();
|
|
51
|
+
const render = (pathname: string, activeSectionId: string) =>
|
|
52
|
+
act(async () => {
|
|
53
|
+
root.render(
|
|
54
|
+
<DocsSidebarNavigation
|
|
55
|
+
sections={sections}
|
|
56
|
+
pathname={pathname}
|
|
57
|
+
activeSectionId={activeSectionId}
|
|
58
|
+
isVisible={(node) => node.$id !== "vue"}
|
|
59
|
+
onNavigate={onNavigate}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
await render("/docs/install/next", "start");
|
|
65
|
+
expect(container.querySelector('[data-active="true"]')?.textContent).toBe(
|
|
66
|
+
"Next.js",
|
|
67
|
+
);
|
|
68
|
+
expect(container.textContent).not.toContain("Vue");
|
|
69
|
+
expect(container.querySelector('a[href="/docs/tools"]')).toBeNull();
|
|
70
|
+
const section = container.querySelector<HTMLButtonElement>("button")!;
|
|
71
|
+
await act(async () => section.click());
|
|
72
|
+
expect(section.getAttribute("aria-expanded")).toBe("false");
|
|
73
|
+
await render("/docs/install", "start");
|
|
74
|
+
expect(section.getAttribute("aria-expanded")).toBe("true");
|
|
75
|
+
expect(container.querySelector('[data-active="true"]')?.textContent).toBe(
|
|
76
|
+
"Install",
|
|
77
|
+
);
|
|
78
|
+
await render("/docs/tools", "guides");
|
|
79
|
+
expect(
|
|
80
|
+
container.querySelectorAll("button")[1]?.getAttribute("aria-expanded"),
|
|
81
|
+
).toBe("true");
|
|
82
|
+
const link = container.querySelector<HTMLAnchorElement>(
|
|
83
|
+
'a[href="/docs/tools"]',
|
|
84
|
+
)!;
|
|
85
|
+
link.addEventListener("click", (event) => event.preventDefault());
|
|
86
|
+
await act(async () => link.click());
|
|
87
|
+
expect(onNavigate).toHaveBeenCalledOnce();
|
|
88
|
+
} finally {
|
|
89
|
+
await act(async () => root.unmount());
|
|
90
|
+
container.remove();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import type { DocsNode, DocsFolder } from "./tree";
|
|
6
|
+
import { findActivePath } from "./tree";
|
|
7
|
+
import { ChevronDown } from "lucide-react";
|
|
8
|
+
import { AnimatePresence, domAnimation, LazyMotion } from "motion/react";
|
|
9
|
+
import * as m from "motion/react-m";
|
|
10
|
+
import { cn } from "./utils";
|
|
11
|
+
|
|
12
|
+
function SectionItem({
|
|
13
|
+
item,
|
|
14
|
+
onNavigate,
|
|
15
|
+
pathname,
|
|
16
|
+
activePath,
|
|
17
|
+
isVisible,
|
|
18
|
+
}: {
|
|
19
|
+
item: DocsNode;
|
|
20
|
+
onNavigate: () => void;
|
|
21
|
+
pathname: string;
|
|
22
|
+
activePath: ReadonlySet<DocsNode>;
|
|
23
|
+
isVisible: (node: DocsNode) => boolean;
|
|
24
|
+
}) {
|
|
25
|
+
if (!isVisible(item)) return null;
|
|
26
|
+
|
|
27
|
+
if (item.type === "separator") {
|
|
28
|
+
return (
|
|
29
|
+
<p className="text-muted-foreground mt-4 mb-1 px-2 text-xs font-medium first:mt-1">
|
|
30
|
+
{item.name}
|
|
31
|
+
</p>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (item.type === "folder") {
|
|
36
|
+
const isActive = item.index && pathname === item.index.url;
|
|
37
|
+
const containsActive = activePath.has(item);
|
|
38
|
+
const hasChildren = item.children.length > 0;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
{item.index ? (
|
|
43
|
+
<Link
|
|
44
|
+
href={item.index.url}
|
|
45
|
+
onClick={onNavigate}
|
|
46
|
+
data-active={isActive ? "true" : "false"}
|
|
47
|
+
className={cn(
|
|
48
|
+
"flex items-center gap-2 rounded-md px-2 py-1.5 text-[13px] transition-colors duration-150",
|
|
49
|
+
isActive
|
|
50
|
+
? "bg-accent/20 text-foreground dark:bg-accent/50 font-medium"
|
|
51
|
+
: "text-muted-foreground hover:bg-accent/30 hover:text-foreground/90 dark:hover:bg-accent/40",
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{item.icon}
|
|
55
|
+
<span className="truncate">{item.name}</span>
|
|
56
|
+
</Link>
|
|
57
|
+
) : (
|
|
58
|
+
<p className="text-muted-foreground mt-3 mb-1 flex items-center gap-2 px-2 text-xs font-medium first:mt-1">
|
|
59
|
+
{item.icon}
|
|
60
|
+
{item.name}
|
|
61
|
+
</p>
|
|
62
|
+
)}
|
|
63
|
+
{hasChildren && (containsActive || !item.index) && (
|
|
64
|
+
<div className="border-border/50 ml-2 flex flex-col gap-0.5 border-l pl-2">
|
|
65
|
+
{item.children.map((child) => (
|
|
66
|
+
<SectionItem
|
|
67
|
+
key={child.$id}
|
|
68
|
+
item={child}
|
|
69
|
+
onNavigate={onNavigate}
|
|
70
|
+
pathname={pathname}
|
|
71
|
+
activePath={activePath}
|
|
72
|
+
isVisible={isVisible}
|
|
73
|
+
/>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isActive = pathname === item.url;
|
|
82
|
+
return (
|
|
83
|
+
<Link
|
|
84
|
+
href={item.url}
|
|
85
|
+
onClick={onNavigate}
|
|
86
|
+
data-active={isActive ? "true" : "false"}
|
|
87
|
+
className={cn(
|
|
88
|
+
"flex items-center gap-2 rounded-md px-2 py-1.5 text-[13px] transition-colors duration-150",
|
|
89
|
+
isActive
|
|
90
|
+
? "bg-accent/20 text-foreground dark:bg-accent/50 font-medium"
|
|
91
|
+
: "text-muted-foreground hover:bg-accent/30 hover:text-foreground/90 dark:hover:bg-accent/40",
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{item.icon}
|
|
95
|
+
<span className="truncate">{item.name}</span>
|
|
96
|
+
</Link>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function SidebarSection({
|
|
101
|
+
folder,
|
|
102
|
+
isOpen,
|
|
103
|
+
onToggle,
|
|
104
|
+
onNavigate,
|
|
105
|
+
pathname,
|
|
106
|
+
activePath,
|
|
107
|
+
isVisible,
|
|
108
|
+
}: {
|
|
109
|
+
folder: DocsFolder;
|
|
110
|
+
isOpen: boolean;
|
|
111
|
+
onToggle: () => void;
|
|
112
|
+
onNavigate: () => void;
|
|
113
|
+
pathname: string;
|
|
114
|
+
activePath: ReadonlySet<DocsNode>;
|
|
115
|
+
isVisible: (node: DocsNode) => boolean;
|
|
116
|
+
}) {
|
|
117
|
+
const isActive = folder.index && pathname === folder.index.url;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={onToggle}
|
|
124
|
+
aria-expanded={isOpen}
|
|
125
|
+
className={cn(
|
|
126
|
+
"hover:bg-accent/30 dark:hover:bg-accent/40 flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-[13px] transition-colors duration-150",
|
|
127
|
+
isActive || isOpen
|
|
128
|
+
? "text-foreground font-medium"
|
|
129
|
+
: "text-muted-foreground/90",
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
<span className="text-muted-foreground flex size-4 shrink-0 items-center justify-center">
|
|
133
|
+
{folder.icon}
|
|
134
|
+
</span>
|
|
135
|
+
<span className="flex-1 truncate">{folder.name}</span>
|
|
136
|
+
<ChevronDown
|
|
137
|
+
className={cn(
|
|
138
|
+
"text-muted-foreground/60 size-3.5 shrink-0 transition-transform duration-200",
|
|
139
|
+
!isOpen && "-rotate-90",
|
|
140
|
+
)}
|
|
141
|
+
/>
|
|
142
|
+
</button>
|
|
143
|
+
<AnimatePresence initial={false}>
|
|
144
|
+
{isOpen && (
|
|
145
|
+
<m.div
|
|
146
|
+
initial={{ height: 0, opacity: 0 }}
|
|
147
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
148
|
+
exit={{ height: 0, opacity: 0 }}
|
|
149
|
+
transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }}
|
|
150
|
+
className="overflow-hidden"
|
|
151
|
+
>
|
|
152
|
+
<div className="border-border/50 mt-0.5 mb-1 ml-4 flex flex-col gap-0.5 border-l pl-2">
|
|
153
|
+
{folder.children.map((child) => (
|
|
154
|
+
<SectionItem
|
|
155
|
+
key={child.$id}
|
|
156
|
+
item={child}
|
|
157
|
+
onNavigate={onNavigate}
|
|
158
|
+
pathname={pathname}
|
|
159
|
+
activePath={activePath}
|
|
160
|
+
isVisible={isVisible}
|
|
161
|
+
/>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</m.div>
|
|
165
|
+
)}
|
|
166
|
+
</AnimatePresence>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const visible = () => true;
|
|
172
|
+
|
|
173
|
+
export function DocsSidebarNavigation({
|
|
174
|
+
sections,
|
|
175
|
+
pathname,
|
|
176
|
+
activeSectionId,
|
|
177
|
+
isVisible = visible,
|
|
178
|
+
onNavigate,
|
|
179
|
+
banner,
|
|
180
|
+
footer,
|
|
181
|
+
platform,
|
|
182
|
+
}: {
|
|
183
|
+
sections: readonly DocsFolder[];
|
|
184
|
+
pathname: string;
|
|
185
|
+
activeSectionId: string | null;
|
|
186
|
+
isVisible?: (node: DocsNode) => boolean;
|
|
187
|
+
onNavigate: () => void;
|
|
188
|
+
banner?: ReactNode;
|
|
189
|
+
footer?: ReactNode;
|
|
190
|
+
platform?: string;
|
|
191
|
+
}) {
|
|
192
|
+
const navRef = useRef<HTMLElement>(null);
|
|
193
|
+
const activePath = useMemo(
|
|
194
|
+
() => new Set(findActivePath(sections, pathname)),
|
|
195
|
+
[sections, pathname],
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const [openSectionId, setOpenSectionId] = useState<string | null>(
|
|
199
|
+
activeSectionId,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const [sectionScope, setSectionScope] = useState({
|
|
203
|
+
activeSectionId,
|
|
204
|
+
pathname,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
sectionScope.activeSectionId !== activeSectionId ||
|
|
209
|
+
sectionScope.pathname !== pathname
|
|
210
|
+
) {
|
|
211
|
+
setSectionScope({ activeSectionId, pathname });
|
|
212
|
+
if (activeSectionId) setOpenSectionId(activeSectionId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (openSectionId !== activeSectionId) return;
|
|
217
|
+
const timer = setTimeout(() => {
|
|
218
|
+
const nav = navRef.current;
|
|
219
|
+
if (!nav) return;
|
|
220
|
+
const active = nav.querySelector<HTMLElement>("[data-active='true']");
|
|
221
|
+
if (!active) return;
|
|
222
|
+
const navRect = nav.getBoundingClientRect();
|
|
223
|
+
const elRect = active.getBoundingClientRect();
|
|
224
|
+
if (elRect.top < navRect.top || elRect.bottom > navRect.bottom) {
|
|
225
|
+
active.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
226
|
+
}
|
|
227
|
+
}, 260);
|
|
228
|
+
return () => clearTimeout(timer);
|
|
229
|
+
}, [pathname, openSectionId, activeSectionId]);
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div className="flex h-full flex-col">
|
|
233
|
+
{banner && <div className="shrink-0 px-3 pt-4 lg:hidden">{banner}</div>}
|
|
234
|
+
<nav
|
|
235
|
+
aria-label="Docs"
|
|
236
|
+
ref={navRef}
|
|
237
|
+
data-docs-platform={platform}
|
|
238
|
+
className={cn(
|
|
239
|
+
"sidebar-tree-content flex min-h-0 flex-1 flex-col gap-0.5 overflow-x-hidden overflow-y-auto px-3 pt-2 pb-4 lg:pt-4",
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
<LazyMotion features={domAnimation}>
|
|
243
|
+
{sections.map((section) => (
|
|
244
|
+
<SidebarSection
|
|
245
|
+
key={section.$id}
|
|
246
|
+
folder={section}
|
|
247
|
+
isOpen={openSectionId === section.$id}
|
|
248
|
+
onToggle={() => {
|
|
249
|
+
const id = section.$id ?? null;
|
|
250
|
+
setOpenSectionId(openSectionId !== id ? id : null);
|
|
251
|
+
}}
|
|
252
|
+
onNavigate={onNavigate}
|
|
253
|
+
pathname={pathname}
|
|
254
|
+
activePath={activePath}
|
|
255
|
+
isVisible={isVisible}
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
</LazyMotion>
|
|
259
|
+
</nav>
|
|
260
|
+
{footer && (
|
|
261
|
+
<div className="border-border/50 flex shrink-0 items-center gap-1 border-t px-3 py-2">
|
|
262
|
+
{footer}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useState,
|
|
8
|
+
useSyncExternalStore,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { cn } from "./utils";
|
|
12
|
+
import { DOCS_SIDEBAR_WIDTH } from "./constants";
|
|
13
|
+
|
|
14
|
+
export const DOCS_SIDEBAR_ID = "docs-sidebar";
|
|
15
|
+
|
|
16
|
+
const subscribeToViewport = (onChange: () => void) => {
|
|
17
|
+
const viewport = window.matchMedia("(max-width: 767px)");
|
|
18
|
+
viewport.addEventListener("change", onChange);
|
|
19
|
+
return () => viewport.removeEventListener("change", onChange);
|
|
20
|
+
};
|
|
21
|
+
const isMobileViewport = () => window.innerWidth < 768;
|
|
22
|
+
const serverViewport = () => false;
|
|
23
|
+
|
|
24
|
+
interface DocsSidebarContextValue {
|
|
25
|
+
open: boolean;
|
|
26
|
+
setOpen: (open: boolean) => void;
|
|
27
|
+
toggle: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DocsSidebarContext = createContext<DocsSidebarContextValue | null>(null);
|
|
31
|
+
|
|
32
|
+
export function useDocsSidebar() {
|
|
33
|
+
const ctx = useContext(DocsSidebarContext);
|
|
34
|
+
if (!ctx) {
|
|
35
|
+
throw new Error("useDocsSidebar must be used within DocsSidebarProvider");
|
|
36
|
+
}
|
|
37
|
+
return ctx;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DocsSidebarProvider({ children }: { children: ReactNode }) {
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const toggle = useCallback(() => setOpen((prev) => !prev), []);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<DocsSidebarContext.Provider value={{ open, setOpen, toggle }}>
|
|
46
|
+
{children}
|
|
47
|
+
</DocsSidebarContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function DocsSidebar({ children }: { children: ReactNode }) {
|
|
52
|
+
const { open } = useDocsSidebar();
|
|
53
|
+
const isMobile = useSyncExternalStore(
|
|
54
|
+
subscribeToViewport,
|
|
55
|
+
isMobileViewport,
|
|
56
|
+
serverViewport,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<aside
|
|
61
|
+
id={DOCS_SIDEBAR_ID}
|
|
62
|
+
inert={!open && isMobile}
|
|
63
|
+
className={cn(
|
|
64
|
+
"bg-background fixed inset-x-0 top-12 bottom-0 z-40 transition-[opacity,visibility] duration-200",
|
|
65
|
+
open ? "opacity-100" : "pointer-events-none opacity-0 max-md:invisible",
|
|
66
|
+
"md:pointer-events-auto md:visible md:right-auto md:left-0 md:z-30 md:w-(--sidebar-width) md:opacity-100",
|
|
67
|
+
)}
|
|
68
|
+
style={
|
|
69
|
+
{
|
|
70
|
+
"--sidebar-width": `${DOCS_SIDEBAR_WIDTH}px`,
|
|
71
|
+
} as React.CSSProperties
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</aside>
|
|
76
|
+
);
|
|
77
|
+
}
|