@barodoc/theme-docs 0.0.1
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/package.json +68 -0
- package/src/components/CodeCopy.astro +173 -0
- package/src/components/DocHeader.tsx +166 -0
- package/src/components/DocsSidebar.tsx +84 -0
- package/src/components/Header.astro +32 -0
- package/src/components/LanguageSwitcher.astro +77 -0
- package/src/components/MobileNav.astro +61 -0
- package/src/components/MobileNavSheet.tsx +73 -0
- package/src/components/Search.tsx +210 -0
- package/src/components/SearchDialog.tsx +83 -0
- package/src/components/Sidebar.astro +56 -0
- package/src/components/SidebarWrapper.tsx +24 -0
- package/src/components/TableOfContents.astro +98 -0
- package/src/components/ThemeScript.astro +11 -0
- package/src/components/ThemeToggle.tsx +57 -0
- package/src/components/api/ApiEndpoint.astro +36 -0
- package/src/components/api/ApiParam.astro +26 -0
- package/src/components/api/ApiParams.astro +16 -0
- package/src/components/api/ApiResponse.astro +35 -0
- package/src/components/index.ts +30 -0
- package/src/components/mdx/Accordion.tsx +61 -0
- package/src/components/mdx/Badge.tsx +33 -0
- package/src/components/mdx/Callout.astro +79 -0
- package/src/components/mdx/Card.astro +66 -0
- package/src/components/mdx/CardGroup.astro +18 -0
- package/src/components/mdx/CodeGroup.astro +63 -0
- package/src/components/mdx/CodeGroup.tsx +51 -0
- package/src/components/mdx/Columns.tsx +31 -0
- package/src/components/mdx/DocAccordion.tsx +87 -0
- package/src/components/mdx/DocCallout.tsx +65 -0
- package/src/components/mdx/DocCard.tsx +70 -0
- package/src/components/mdx/DocTabs.tsx +48 -0
- package/src/components/mdx/Expandable.tsx +107 -0
- package/src/components/mdx/FileTree.tsx +72 -0
- package/src/components/mdx/Frame.tsx +23 -0
- package/src/components/mdx/Icon.tsx +59 -0
- package/src/components/mdx/Mermaid.tsx +94 -0
- package/src/components/mdx/ParamField.tsx +76 -0
- package/src/components/mdx/ResponseField.tsx +62 -0
- package/src/components/mdx/Step.astro +14 -0
- package/src/components/mdx/Steps.astro +37 -0
- package/src/components/mdx/Steps.tsx +49 -0
- package/src/components/mdx/Tabs.tsx +67 -0
- package/src/components/mdx/Tooltip.tsx +36 -0
- package/src/components/ui/accordion.tsx +54 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/dialog.tsx +119 -0
- package/src/components/ui/index.ts +11 -0
- package/src/components/ui/scroll-area.tsx +45 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +137 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/tooltip.tsx +29 -0
- package/src/index.ts +74 -0
- package/src/layouts/BaseLayout.astro +28 -0
- package/src/layouts/DocsLayout.astro +121 -0
- package/src/lib/utils.ts +6 -0
- package/src/pages/docs/[...slug].astro +116 -0
- package/src/pages/index.astro +217 -0
- package/src/styles/global.css +342 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Sheet,
|
|
4
|
+
SheetContent,
|
|
5
|
+
SheetHeader,
|
|
6
|
+
SheetTitle,
|
|
7
|
+
} from "./ui/sheet";
|
|
8
|
+
import { ScrollArea } from "./ui/scroll-area";
|
|
9
|
+
import { DocsSidebar } from "./DocsSidebar";
|
|
10
|
+
import { cn } from "../lib/utils";
|
|
11
|
+
|
|
12
|
+
interface NavItem {
|
|
13
|
+
title: string;
|
|
14
|
+
href: string;
|
|
15
|
+
isActive?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface NavGroup {
|
|
19
|
+
title: string;
|
|
20
|
+
items: NavItem[];
|
|
21
|
+
defaultOpen?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface MobileNavSheetProps {
|
|
25
|
+
groups: NavGroup[];
|
|
26
|
+
siteName: string;
|
|
27
|
+
logo?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function MobileNavSheet({ groups, siteName, logo }: MobileNavSheetProps) {
|
|
31
|
+
const [open, setOpen] = React.useState(false);
|
|
32
|
+
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
const handleToggle = () => setOpen((prev) => !prev);
|
|
35
|
+
document.addEventListener("toggle-mobile-nav", handleToggle);
|
|
36
|
+
return () => document.removeEventListener("toggle-mobile-nav", handleToggle);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
// Close on navigation
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
const handleClick = (e: MouseEvent) => {
|
|
42
|
+
const target = e.target as HTMLElement;
|
|
43
|
+
if (target.closest("a")) {
|
|
44
|
+
setOpen(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (open) {
|
|
49
|
+
document.addEventListener("click", handleClick);
|
|
50
|
+
return () => document.removeEventListener("click", handleClick);
|
|
51
|
+
}
|
|
52
|
+
}, [open]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
56
|
+
<SheetContent side="left" className="w-80 p-0">
|
|
57
|
+
<SheetHeader className="px-6 py-4 border-b border-[var(--color-border)]">
|
|
58
|
+
<SheetTitle className="flex items-center gap-2">
|
|
59
|
+
{logo && <img src={logo} alt={siteName} className="h-6 w-6" />}
|
|
60
|
+
<span>{siteName}</span>
|
|
61
|
+
</SheetTitle>
|
|
62
|
+
</SheetHeader>
|
|
63
|
+
<ScrollArea className="h-[calc(100vh-65px)]">
|
|
64
|
+
<div className="px-2 py-4">
|
|
65
|
+
<DocsSidebar groups={groups} />
|
|
66
|
+
</div>
|
|
67
|
+
</ScrollArea>
|
|
68
|
+
</SheetContent>
|
|
69
|
+
</Sheet>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default MobileNavSheet;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
interface SearchResult {
|
|
4
|
+
url: string;
|
|
5
|
+
meta: {
|
|
6
|
+
title: string;
|
|
7
|
+
};
|
|
8
|
+
excerpt: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Pagefind loader - only runs on client side
|
|
12
|
+
async function loadPagefind() {
|
|
13
|
+
if (typeof window === "undefined") return null;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// @ts-expect-error - Pagefind exposes itself on window when loaded
|
|
17
|
+
if (window.pagefind) {
|
|
18
|
+
// @ts-expect-error
|
|
19
|
+
return window.pagefind;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Dynamic import using Function constructor to avoid Vite bundling
|
|
23
|
+
const pagefindPath = "/pagefind/pagefind.js";
|
|
24
|
+
const pagefind = await new Function(`return import("${pagefindPath}")`)();
|
|
25
|
+
await pagefind.init();
|
|
26
|
+
// @ts-expect-error
|
|
27
|
+
window.pagefind = pagefind;
|
|
28
|
+
return pagefind;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn("Pagefind not available:", e);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Search() {
|
|
36
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
37
|
+
const [query, setQuery] = useState("");
|
|
38
|
+
const [results, setResults] = useState<SearchResult[]>([]);
|
|
39
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
40
|
+
const [pagefind, setPagefind] = useState<any>(null);
|
|
41
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
42
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
43
|
+
|
|
44
|
+
// Load Pagefind
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
loadPagefind().then(setPagefind);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
// Listen for toggle-search event
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const handler = () => setIsOpen(true);
|
|
52
|
+
document.addEventListener("toggle-search", handler);
|
|
53
|
+
return () => document.removeEventListener("toggle-search", handler);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
// Handle keyboard shortcuts
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
59
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
setIsOpen(true);
|
|
62
|
+
}
|
|
63
|
+
if (e.key === "Escape") {
|
|
64
|
+
setIsOpen(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
68
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
// Focus input when opening
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (isOpen) {
|
|
74
|
+
inputRef.current?.focus();
|
|
75
|
+
dialogRef.current?.showModal();
|
|
76
|
+
} else {
|
|
77
|
+
dialogRef.current?.close();
|
|
78
|
+
setQuery("");
|
|
79
|
+
setResults([]);
|
|
80
|
+
setSelectedIndex(0);
|
|
81
|
+
}
|
|
82
|
+
}, [isOpen]);
|
|
83
|
+
|
|
84
|
+
// Search with debounce
|
|
85
|
+
const search = useCallback(
|
|
86
|
+
async (searchQuery: string) => {
|
|
87
|
+
if (!pagefind || !searchQuery.trim()) {
|
|
88
|
+
setResults([]);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const searchResults = await pagefind.search(searchQuery);
|
|
93
|
+
const data = await Promise.all(
|
|
94
|
+
searchResults.results.slice(0, 8).map((r: any) => r.data())
|
|
95
|
+
);
|
|
96
|
+
setResults(data);
|
|
97
|
+
setSelectedIndex(0);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error("Search error:", e);
|
|
100
|
+
setResults([]);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[pagefind]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const timer = setTimeout(() => search(query), 150);
|
|
108
|
+
return () => clearTimeout(timer);
|
|
109
|
+
}, [query, search]);
|
|
110
|
+
|
|
111
|
+
// Navigate results with keyboard
|
|
112
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
113
|
+
if (e.key === "ArrowDown") {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
setSelectedIndex((prev) => Math.min(prev + 1, results.length - 1));
|
|
116
|
+
} else if (e.key === "ArrowUp") {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
|
119
|
+
} else if (e.key === "Enter" && results[selectedIndex]) {
|
|
120
|
+
window.location.href = results[selectedIndex].url;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (!isOpen) return null;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<dialog
|
|
128
|
+
ref={dialogRef}
|
|
129
|
+
className="fixed inset-0 z-50 bg-transparent p-0 m-0 max-w-none max-h-none w-full h-full"
|
|
130
|
+
onClick={(e) => {
|
|
131
|
+
if (e.target === dialogRef.current) setIsOpen(false);
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
|
|
135
|
+
<div className="fixed inset-x-4 top-[15%] md:inset-x-auto md:left-1/2 md:-translate-x-1/2 md:w-full md:max-w-lg">
|
|
136
|
+
<div className="bg-[var(--color-bg)] rounded-xl shadow-2xl border border-[var(--color-border)] overflow-hidden">
|
|
137
|
+
<div className="flex items-center px-4 border-b border-[var(--color-border)]">
|
|
138
|
+
<svg
|
|
139
|
+
className="w-5 h-5 text-[var(--color-text-secondary)]"
|
|
140
|
+
fill="none"
|
|
141
|
+
stroke="currentColor"
|
|
142
|
+
viewBox="0 0 24 24"
|
|
143
|
+
>
|
|
144
|
+
<path
|
|
145
|
+
strokeLinecap="round"
|
|
146
|
+
strokeLinejoin="round"
|
|
147
|
+
strokeWidth={2}
|
|
148
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
149
|
+
/>
|
|
150
|
+
</svg>
|
|
151
|
+
<input
|
|
152
|
+
ref={inputRef}
|
|
153
|
+
type="text"
|
|
154
|
+
value={query}
|
|
155
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
156
|
+
onKeyDown={handleKeyDown}
|
|
157
|
+
placeholder="Search documentation..."
|
|
158
|
+
className="flex-1 px-4 py-4 bg-transparent text-[var(--color-text)] placeholder-[var(--color-text-secondary)] focus:outline-none"
|
|
159
|
+
/>
|
|
160
|
+
<button
|
|
161
|
+
onClick={() => setIsOpen(false)}
|
|
162
|
+
className="px-2 py-1 text-xs text-[var(--color-text-secondary)] bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded"
|
|
163
|
+
>
|
|
164
|
+
ESC
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{query.trim() && (
|
|
169
|
+
<div className="max-h-[60vh] overflow-y-auto">
|
|
170
|
+
{results.length === 0 ? (
|
|
171
|
+
<div className="px-4 py-8 text-center text-[var(--color-text-secondary)]">
|
|
172
|
+
{pagefind ? "No results found" : "Search index not available"}
|
|
173
|
+
</div>
|
|
174
|
+
) : (
|
|
175
|
+
<ul>
|
|
176
|
+
{results.map((result, index) => (
|
|
177
|
+
<li key={result.url}>
|
|
178
|
+
<a
|
|
179
|
+
href={result.url}
|
|
180
|
+
className={`block px-4 py-3 transition-colors ${
|
|
181
|
+
index === selectedIndex
|
|
182
|
+
? "bg-primary-50 dark:bg-primary-900/30"
|
|
183
|
+
: "hover:bg-[var(--color-bg-secondary)]"
|
|
184
|
+
}`}
|
|
185
|
+
>
|
|
186
|
+
<div className="font-medium text-[var(--color-text)]">
|
|
187
|
+
{result.meta.title}
|
|
188
|
+
</div>
|
|
189
|
+
<div
|
|
190
|
+
className="text-sm text-[var(--color-text-secondary)] line-clamp-2 mt-0.5"
|
|
191
|
+
dangerouslySetInnerHTML={{ __html: result.excerpt }}
|
|
192
|
+
/>
|
|
193
|
+
</a>
|
|
194
|
+
</li>
|
|
195
|
+
))}
|
|
196
|
+
</ul>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{!query.trim() && (
|
|
202
|
+
<div className="px-4 py-6 text-center text-[var(--color-text-secondary)] text-sm">
|
|
203
|
+
Start typing to search...
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</dialog>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Search as SearchIcon } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogHeader,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
} from "./ui/dialog";
|
|
9
|
+
import { cn } from "../lib/utils";
|
|
10
|
+
|
|
11
|
+
interface SearchDialogProps {
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SearchDialog({ className }: SearchDialogProps) {
|
|
16
|
+
const [open, setOpen] = React.useState(false);
|
|
17
|
+
const [query, setQuery] = React.useState("");
|
|
18
|
+
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
const handleToggle = () => setOpen((prev) => !prev);
|
|
21
|
+
document.addEventListener("toggle-search", handleToggle);
|
|
22
|
+
|
|
23
|
+
const down = (e: KeyboardEvent) => {
|
|
24
|
+
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
setOpen((prev) => !prev);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
document.addEventListener("keydown", down);
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
document.removeEventListener("toggle-search", handleToggle);
|
|
33
|
+
document.removeEventListener("keydown", down);
|
|
34
|
+
};
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (open && typeof window !== "undefined" && (window as any).pagefind) {
|
|
39
|
+
// Initialize pagefind when dialog opens
|
|
40
|
+
}
|
|
41
|
+
}, [open]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
45
|
+
<DialogContent className={cn("sm:max-w-2xl p-0 gap-0", className)}>
|
|
46
|
+
<DialogHeader className="px-4 py-3 border-b border-[var(--color-border)]">
|
|
47
|
+
<DialogTitle className="sr-only">Search documentation</DialogTitle>
|
|
48
|
+
<div className="flex items-center gap-3">
|
|
49
|
+
<SearchIcon className="h-5 w-5 text-[var(--color-text-muted)]" />
|
|
50
|
+
<input
|
|
51
|
+
type="text"
|
|
52
|
+
placeholder="Search documentation..."
|
|
53
|
+
value={query}
|
|
54
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
55
|
+
className="flex-1 bg-transparent text-base outline-none placeholder:text-[var(--color-text-muted)]"
|
|
56
|
+
autoFocus
|
|
57
|
+
/>
|
|
58
|
+
<kbd className="hidden sm:inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded text-[var(--color-text-muted)]">
|
|
59
|
+
Esc
|
|
60
|
+
</kbd>
|
|
61
|
+
</div>
|
|
62
|
+
</DialogHeader>
|
|
63
|
+
<div className="min-h-[300px] max-h-[60vh] overflow-y-auto p-4">
|
|
64
|
+
{query ? (
|
|
65
|
+
<div id="pagefind-results" className="pagefind-ui" />
|
|
66
|
+
) : (
|
|
67
|
+
<div className="flex flex-col items-center justify-center h-full py-12 text-center">
|
|
68
|
+
<SearchIcon className="h-12 w-12 text-[var(--color-text-muted)] mb-4" />
|
|
69
|
+
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
70
|
+
Start typing to search the documentation
|
|
71
|
+
</p>
|
|
72
|
+
<p className="text-xs text-[var(--color-text-muted)] mt-2">
|
|
73
|
+
Press <kbd className="px-1.5 py-0.5 bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded text-xs">⌘K</kbd> to open search
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
</DialogContent>
|
|
79
|
+
</Dialog>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default SearchDialog;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from "virtual:barodoc/config";
|
|
3
|
+
import { defaultLocale } from "virtual:barodoc/i18n";
|
|
4
|
+
import { getLocalizedNavGroup } from "@barodoc/core";
|
|
5
|
+
import { SidebarWrapper } from "./SidebarWrapper";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
currentPath: string;
|
|
9
|
+
currentLocale?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { currentPath, currentLocale = defaultLocale } = Astro.props;
|
|
13
|
+
|
|
14
|
+
// Normalize path for comparison
|
|
15
|
+
function normalizePath(path: string): string {
|
|
16
|
+
return path.replace(/\/$/, '').replace(/^\//, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isActive(page: string): boolean {
|
|
20
|
+
const normalized = normalizePath(currentPath);
|
|
21
|
+
const pagePath = normalizePath(page);
|
|
22
|
+
return normalized === pagePath ||
|
|
23
|
+
normalized === `docs/${pagePath}` ||
|
|
24
|
+
normalized === `${currentLocale}/docs/${pagePath}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getPageHref(page: string): string {
|
|
28
|
+
if (currentLocale === defaultLocale) {
|
|
29
|
+
return `/docs/${page}`;
|
|
30
|
+
}
|
|
31
|
+
return `/${currentLocale}/docs/${page}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get page title from the page slug
|
|
35
|
+
function getPageTitle(page: string): string {
|
|
36
|
+
const parts = page.split('/');
|
|
37
|
+
const name = parts[parts.length - 1];
|
|
38
|
+
return name
|
|
39
|
+
.split('-')
|
|
40
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
41
|
+
.join(' ');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Transform config.navigation to the format expected by DocsSidebar
|
|
45
|
+
const groups = config.navigation.map((group) => ({
|
|
46
|
+
title: getLocalizedNavGroup(group, currentLocale, defaultLocale),
|
|
47
|
+
defaultOpen: true,
|
|
48
|
+
items: group.pages.map((page) => ({
|
|
49
|
+
title: getPageTitle(page),
|
|
50
|
+
href: getPageHref(page),
|
|
51
|
+
isActive: isActive(page),
|
|
52
|
+
})),
|
|
53
|
+
}));
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<SidebarWrapper groups={groups} client:load />
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { DocsSidebar } from "./DocsSidebar";
|
|
3
|
+
|
|
4
|
+
interface NavItem {
|
|
5
|
+
title: string;
|
|
6
|
+
href: string;
|
|
7
|
+
isActive?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface NavGroup {
|
|
11
|
+
title: string;
|
|
12
|
+
items: NavItem[];
|
|
13
|
+
defaultOpen?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SidebarWrapperProps {
|
|
17
|
+
groups: NavGroup[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SidebarWrapper({ groups }: SidebarWrapperProps) {
|
|
21
|
+
return <DocsSidebar groups={groups} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default SidebarWrapper;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Heading {
|
|
3
|
+
depth: number;
|
|
4
|
+
slug: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
headings: Heading[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { headings } = Astro.props;
|
|
13
|
+
|
|
14
|
+
// Filter to only show h2 and h3
|
|
15
|
+
const filteredHeadings = headings.filter(h => h.depth >= 2 && h.depth <= 3);
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
{filteredHeadings.length > 0 && (
|
|
19
|
+
<div class="toc-container">
|
|
20
|
+
<h4 class="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-muted)] mb-4">
|
|
21
|
+
On this page
|
|
22
|
+
</h4>
|
|
23
|
+
<nav class="relative">
|
|
24
|
+
<div class="absolute left-0 top-0 bottom-0 w-px bg-[var(--color-border)]"></div>
|
|
25
|
+
<ul class="space-y-1">
|
|
26
|
+
{filteredHeadings.map((heading) => (
|
|
27
|
+
<li class="relative">
|
|
28
|
+
<a
|
|
29
|
+
href={`#${heading.slug}`}
|
|
30
|
+
class:list={[
|
|
31
|
+
"toc-link block text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-all duration-150",
|
|
32
|
+
heading.depth === 2 ? "py-1.5 pl-4" : "py-1 pl-6 text-xs"
|
|
33
|
+
]}
|
|
34
|
+
data-heading={heading.slug}
|
|
35
|
+
>
|
|
36
|
+
<span class="relative">{heading.text}</span>
|
|
37
|
+
</a>
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
</nav>
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
<style>
|
|
46
|
+
.toc-link.active {
|
|
47
|
+
color: var(--color-primary-600);
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.toc-link.active::before {
|
|
52
|
+
content: '';
|
|
53
|
+
position: absolute;
|
|
54
|
+
left: 0;
|
|
55
|
+
top: 50%;
|
|
56
|
+
transform: translateY(-50%);
|
|
57
|
+
width: 2px;
|
|
58
|
+
height: 1.25rem;
|
|
59
|
+
background: var(--color-primary-500);
|
|
60
|
+
border-radius: 1px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
:global(.dark) .toc-link.active {
|
|
64
|
+
color: var(--color-primary-400);
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
67
|
+
|
|
68
|
+
<script>
|
|
69
|
+
function initTOCHighlight() {
|
|
70
|
+
const headings = document.querySelectorAll('article h2, article h3');
|
|
71
|
+
const tocLinks = document.querySelectorAll('.toc-link');
|
|
72
|
+
|
|
73
|
+
if (headings.length === 0 || tocLinks.length === 0) return;
|
|
74
|
+
|
|
75
|
+
const observer = new IntersectionObserver(
|
|
76
|
+
(entries) => {
|
|
77
|
+
entries.forEach((entry) => {
|
|
78
|
+
if (entry.isIntersecting) {
|
|
79
|
+
const id = entry.target.id;
|
|
80
|
+
tocLinks.forEach((link) => {
|
|
81
|
+
const isActive = link.getAttribute('data-heading') === id;
|
|
82
|
+
link.classList.toggle('active', isActive);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
rootMargin: '-80px 0px -80% 0px',
|
|
89
|
+
threshold: 0
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
headings.forEach((heading) => observer.observe(heading));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
initTOCHighlight();
|
|
97
|
+
document.addEventListener('astro:page-load', initTOCHighlight);
|
|
98
|
+
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
// This script runs before the page renders to prevent flash of wrong theme
|
|
3
|
+
---
|
|
4
|
+
<script is:inline>
|
|
5
|
+
(function() {
|
|
6
|
+
const theme = localStorage.getItem('theme');
|
|
7
|
+
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
8
|
+
document.documentElement.classList.add('dark');
|
|
9
|
+
}
|
|
10
|
+
})();
|
|
11
|
+
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Moon, Sun } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export function ThemeToggle() {
|
|
5
|
+
const [theme, setTheme] = useState<"light" | "dark">("light");
|
|
6
|
+
const [mounted, setMounted] = useState(false);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
setMounted(true);
|
|
10
|
+
const stored = localStorage.getItem("theme");
|
|
11
|
+
if (stored === "dark" || stored === "light") {
|
|
12
|
+
setTheme(stored);
|
|
13
|
+
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
14
|
+
setTheme("dark");
|
|
15
|
+
}
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!mounted) return;
|
|
20
|
+
|
|
21
|
+
if (theme === "dark") {
|
|
22
|
+
document.documentElement.classList.add("dark");
|
|
23
|
+
} else {
|
|
24
|
+
document.documentElement.classList.remove("dark");
|
|
25
|
+
}
|
|
26
|
+
localStorage.setItem("theme", theme);
|
|
27
|
+
}, [theme, mounted]);
|
|
28
|
+
|
|
29
|
+
const toggleTheme = () => {
|
|
30
|
+
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (!mounted) {
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
37
|
+
aria-label="Toggle theme"
|
|
38
|
+
>
|
|
39
|
+
<div className="w-5 h-5" />
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
onClick={toggleTheme}
|
|
47
|
+
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
48
|
+
aria-label={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
|
|
49
|
+
>
|
|
50
|
+
{theme === "light" ? (
|
|
51
|
+
<Moon className="w-5 h-5 text-gray-600" />
|
|
52
|
+
) : (
|
|
53
|
+
<Sun className="w-5 h-5 text-gray-300" />
|
|
54
|
+
)}
|
|
55
|
+
</button>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
|
+
path: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { method, path, description } = Astro.props;
|
|
9
|
+
|
|
10
|
+
const methodColors = {
|
|
11
|
+
GET: "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400",
|
|
12
|
+
POST: "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400",
|
|
13
|
+
PUT: "bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400",
|
|
14
|
+
PATCH: "bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400",
|
|
15
|
+
DELETE: "bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400",
|
|
16
|
+
};
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<div class="not-prose my-6 rounded-lg border border-[var(--color-border)] overflow-hidden">
|
|
20
|
+
<div class="flex items-center gap-3 px-4 py-3 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border)]">
|
|
21
|
+
<span class:list={["px-2 py-1 text-xs font-bold rounded uppercase", methodColors[method]]}>
|
|
22
|
+
{method}
|
|
23
|
+
</span>
|
|
24
|
+
<code class="text-sm font-mono text-[var(--color-text)]">{path}</code>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{description && (
|
|
28
|
+
<div class="px-4 py-3 border-b border-[var(--color-border)]">
|
|
29
|
+
<p class="text-sm text-[var(--color-text-secondary)]">{description}</p>
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
|
|
33
|
+
<div class="p-4">
|
|
34
|
+
<slot />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { name, type, required = false, description } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="flex flex-col gap-1 py-3 border-b border-[var(--color-border)] last:border-b-0">
|
|
13
|
+
<div class="flex items-center gap-2">
|
|
14
|
+
<code class="text-sm font-semibold text-[var(--color-text)]">{name}</code>
|
|
15
|
+
<span class="text-xs text-[var(--color-text-secondary)]">{type}</span>
|
|
16
|
+
{required && (
|
|
17
|
+
<span class="px-1.5 py-0.5 text-xs font-medium text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/30 rounded">
|
|
18
|
+
required
|
|
19
|
+
</span>
|
|
20
|
+
)}
|
|
21
|
+
</div>
|
|
22
|
+
{description && (
|
|
23
|
+
<p class="text-sm text-[var(--color-text-secondary)]">{description}</p>
|
|
24
|
+
)}
|
|
25
|
+
<slot />
|
|
26
|
+
</div>
|