@assistant-ui/docs-theme 0.0.2 → 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/README.md +89 -5
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- 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 +21 -11
- package/src/config.ts +1 -1
- package/src/index.ts +1 -1
- 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
|
@@ -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
|
+
}
|
|
@@ -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
|
+
});
|