@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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/dist/action-icons.d.ts +5 -0
  4. package/dist/action-icons.d.ts.map +1 -0
  5. package/dist/action-icons.js +52 -0
  6. package/dist/action-icons.js.map +1 -0
  7. package/dist/brand.d.ts +7 -0
  8. package/dist/brand.d.ts.map +1 -0
  9. package/dist/brand.js +37 -0
  10. package/dist/brand.js.map +1 -0
  11. package/dist/config.d.ts +3 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +22 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/header.d.ts +12 -0
  16. package/dist/header.d.ts.map +1 -0
  17. package/dist/header.js +73 -0
  18. package/dist/header.js.map +1 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +48 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/navigation.d.ts +23 -0
  24. package/dist/navigation.d.ts.map +1 -0
  25. package/dist/navigation.js +41 -0
  26. package/dist/navigation.js.map +1 -0
  27. package/dist/next.d.ts +29 -0
  28. package/dist/next.d.ts.map +1 -0
  29. package/dist/next.js +63 -0
  30. package/dist/next.js.map +1 -0
  31. package/dist/page-content.d.ts +12 -0
  32. package/dist/page-content.d.ts.map +1 -0
  33. package/dist/page-content.js +274 -0
  34. package/dist/page-content.js.map +1 -0
  35. package/dist/provider.d.ts +2 -0
  36. package/dist/provider.d.ts.map +1 -0
  37. package/dist/provider.js +1 -0
  38. package/dist/site/constants.d.ts +2 -0
  39. package/dist/site/constants.d.ts.map +1 -0
  40. package/dist/site/constants.js +5 -0
  41. package/dist/site/constants.js.map +1 -0
  42. package/dist/site/footer.d.ts +13 -0
  43. package/dist/site/footer.d.ts.map +1 -0
  44. package/dist/site/footer.js +35 -0
  45. package/dist/site/footer.js.map +1 -0
  46. package/dist/site/header.d.ts +13 -0
  47. package/dist/site/header.d.ts.map +1 -0
  48. package/dist/site/header.js +81 -0
  49. package/dist/site/header.js.map +1 -0
  50. package/dist/site/layout.d.ts +8 -0
  51. package/dist/site/layout.d.ts.map +1 -0
  52. package/dist/site/layout.js +21 -0
  53. package/dist/site/layout.js.map +1 -0
  54. package/dist/site/page.d.ts +9 -0
  55. package/dist/site/page.d.ts.map +1 -0
  56. package/dist/site/page.js +32 -0
  57. package/dist/site/page.js.map +1 -0
  58. package/dist/site/pager.d.ts +13 -0
  59. package/dist/site/pager.d.ts.map +1 -0
  60. package/dist/site/pager.js +36 -0
  61. package/dist/site/pager.js.map +1 -0
  62. package/dist/site/sidebar-navigation.d.ts +13 -0
  63. package/dist/site/sidebar-navigation.d.ts.map +1 -0
  64. package/dist/site/sidebar-navigation.js +185 -0
  65. package/dist/site/sidebar-navigation.js.map +1 -0
  66. package/dist/site/sidebar.d.ts +16 -0
  67. package/dist/site/sidebar.d.ts.map +1 -0
  68. package/dist/site/sidebar.js +46 -0
  69. package/dist/site/sidebar.js.map +1 -0
  70. package/dist/site/toc.d.ts +12 -0
  71. package/dist/site/toc.d.ts.map +1 -0
  72. package/dist/site/toc.js +92 -0
  73. package/dist/site/toc.js.map +1 -0
  74. package/dist/site/tree.d.ts +24 -0
  75. package/dist/site/tree.d.ts.map +1 -0
  76. package/dist/site/tree.js +14 -0
  77. package/dist/site/tree.js.map +1 -0
  78. package/dist/site/utils.d.ts +2 -0
  79. package/dist/site/utils.d.ts.map +1 -0
  80. package/dist/site/utils.js +5 -0
  81. package/dist/site/utils.js.map +1 -0
  82. package/dist/ui.d.ts +10 -0
  83. package/dist/ui.d.ts.map +1 -0
  84. package/dist/ui.js +9 -0
  85. package/package.json +101 -1
  86. package/src/action-icons.tsx +48 -0
  87. package/src/brand.tsx +36 -0
  88. package/src/config.ts +23 -0
  89. package/src/header.test.tsx +41 -0
  90. package/src/header.tsx +94 -0
  91. package/src/index.test.ts +37 -0
  92. package/src/index.ts +38 -0
  93. package/src/layout-hosts.test.tsx +98 -0
  94. package/src/navigation.test.ts +123 -0
  95. package/src/navigation.ts +61 -0
  96. package/src/next.test.tsx +110 -0
  97. package/src/next.tsx +101 -0
  98. package/src/page-content.test.tsx +87 -0
  99. package/src/page-content.tsx +416 -0
  100. package/src/provider.ts +1 -0
  101. package/src/site/constants.ts +1 -0
  102. package/src/site/footer.tsx +59 -0
  103. package/src/site/header.test.tsx +89 -0
  104. package/src/site/header.tsx +117 -0
  105. package/src/site/layout.tsx +33 -0
  106. package/src/site/page.tsx +38 -0
  107. package/src/site/pager.tsx +54 -0
  108. package/src/site/sidebar-navigation.test.tsx +92 -0
  109. package/src/site/sidebar-navigation.tsx +267 -0
  110. package/src/site/sidebar.tsx +77 -0
  111. package/src/site/toc.test.tsx +187 -0
  112. package/src/site/toc.tsx +118 -0
  113. package/src/site/tree.ts +36 -0
  114. package/src/site/utils.ts +2 -0
  115. package/src/ui.ts +19 -0
  116. package/styles/site.css +223 -0
  117. 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
+ }