@opendocsdev/cli 0.2.0
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 +661 -0
- package/README.md +300 -0
- package/dist/bin/opendocs.js +712 -0
- package/dist/bin/opendocs.js.map +1 -0
- package/dist/templates/api-reference.mdx +308 -0
- package/dist/templates/components.mdx +286 -0
- package/dist/templates/configuration.mdx +190 -0
- package/dist/templates/docs.json +27 -0
- package/dist/templates/introduction.mdx +25 -0
- package/dist/templates/logo.svg +4 -0
- package/dist/templates/quickstart.mdx +59 -0
- package/dist/templates/writing-content.mdx +236 -0
- package/package.json +92 -0
- package/src/engine/astro.config.ts +75 -0
- package/src/engine/src/components/Analytics.astro +57 -0
- package/src/engine/src/components/ApiPlayground.astro +24 -0
- package/src/engine/src/components/Callout.astro +66 -0
- package/src/engine/src/components/Card.astro +75 -0
- package/src/engine/src/components/CardGroup.astro +29 -0
- package/src/engine/src/components/CodeGroup.astro +231 -0
- package/src/engine/src/components/CopyButton.astro +179 -0
- package/src/engine/src/components/Steps.astro +27 -0
- package/src/engine/src/components/Tab.astro +21 -0
- package/src/engine/src/components/TableOfContents.astro +119 -0
- package/src/engine/src/components/Tabs.astro +135 -0
- package/src/engine/src/components/index.ts +107 -0
- package/src/engine/src/components/react/ApiPlayground/AuthSection.tsx +91 -0
- package/src/engine/src/components/react/ApiPlayground/CodeBlock.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CodeSnippets.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CollapsibleSection.tsx +26 -0
- package/src/engine/src/components/react/ApiPlayground/KeyValueEditor.tsx +58 -0
- package/src/engine/src/components/react/ApiPlayground/ResponseDisplay.tsx +109 -0
- package/src/engine/src/components/react/ApiPlayground/Spinner.tsx +6 -0
- package/src/engine/src/components/react/ApiPlayground/constants.ts +16 -0
- package/src/engine/src/components/react/ApiPlayground/generators.test.ts +130 -0
- package/src/engine/src/components/react/ApiPlayground/generators.ts +75 -0
- package/src/engine/src/components/react/ApiPlayground/index.tsx +490 -0
- package/src/engine/src/components/react/ApiPlayground/types.ts +35 -0
- package/src/engine/src/components/react/Callout.tsx +54 -0
- package/src/engine/src/components/react/Card.tsx +48 -0
- package/src/engine/src/components/react/CardGroup.tsx +24 -0
- package/src/engine/src/components/react/FeedbackWidget.tsx +166 -0
- package/src/engine/src/components/react/GitHubLink.tsx +28 -0
- package/src/engine/src/components/react/NavigationCard.tsx +53 -0
- package/src/engine/src/components/react/PageActions.tsx +124 -0
- package/src/engine/src/components/react/PageFooter.tsx +91 -0
- package/src/engine/src/components/react/SearchModal.tsx +358 -0
- package/src/engine/src/components/react/SearchProvider.tsx +37 -0
- package/src/engine/src/components/react/Sidebar.tsx +369 -0
- package/src/engine/src/components/react/SidebarSearchTrigger.tsx +57 -0
- package/src/engine/src/components/react/Steps.tsx +25 -0
- package/src/engine/src/components/react/ThemeToggle.tsx +72 -0
- package/src/engine/src/components/react/index.ts +14 -0
- package/src/engine/src/env.d.ts +10 -0
- package/src/engine/src/layouts/DocsLayout.astro +357 -0
- package/src/engine/src/lib/__tests__/markdown.test.ts +124 -0
- package/src/engine/src/lib/__tests__/mdx-loader.test.ts +205 -0
- package/src/engine/src/lib/config.ts +79 -0
- package/src/engine/src/lib/markdown.ts +54 -0
- package/src/engine/src/lib/mdx-loader.ts +143 -0
- package/src/engine/src/lib/mdx-utils.ts +72 -0
- package/src/engine/src/lib/remark-opendocs.ts +195 -0
- package/src/engine/src/lib/utils.ts +221 -0
- package/src/engine/src/pages/[...slug].astro +115 -0
- package/src/engine/src/pages/index.astro +71 -0
- package/src/engine/src/scripts/mobile-sidebar.ts +56 -0
- package/src/engine/src/scripts/theme-init.ts +25 -0
- package/src/engine/src/styles/global.css +703 -0
- package/src/engine/tailwind.config.mjs +60 -0
- package/src/engine/tsconfig.json +15 -0
- package/src/templates/api-reference.mdx +308 -0
- package/src/templates/components.mdx +286 -0
- package/src/templates/configuration.mdx +190 -0
- package/src/templates/docs.json +27 -0
- package/src/templates/introduction.mdx +25 -0
- package/src/templates/logo.svg +4 -0
- package/src/templates/quickstart.mdx +59 -0
- package/src/templates/writing-content.mdx +236 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { ChevronRight, ChevronDown } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
cn,
|
|
5
|
+
pageToUrl,
|
|
6
|
+
formatPageName,
|
|
7
|
+
isPageActive,
|
|
8
|
+
isPageWithChildren as isPageWithChildrenUtil,
|
|
9
|
+
isNestedGroup as isNestedGroupUtil,
|
|
10
|
+
type PageWithChildren,
|
|
11
|
+
type NestedGroup,
|
|
12
|
+
type NavigationGroup,
|
|
13
|
+
} from "../../lib/utils";
|
|
14
|
+
import { ThemeToggle } from "./ThemeToggle";
|
|
15
|
+
import { GitHubLink } from "./GitHubLink";
|
|
16
|
+
import { SidebarSearchTrigger } from "./SidebarSearchTrigger";
|
|
17
|
+
|
|
18
|
+
interface LogoConfig {
|
|
19
|
+
light?: string;
|
|
20
|
+
dark?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SidebarProps {
|
|
24
|
+
siteName: string;
|
|
25
|
+
navigation: NavigationGroup[];
|
|
26
|
+
logo?: string | LogoConfig;
|
|
27
|
+
currentPath: string;
|
|
28
|
+
githubUrl?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if any page in a group of children is active
|
|
32
|
+
function hasActiveChild(children: string[], currentPath: string): boolean {
|
|
33
|
+
return children.some((child) => isPageActive(child, currentPath));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get the page string from a PageItem (returns null for nested groups)
|
|
37
|
+
function getPageString(item: unknown): string | null {
|
|
38
|
+
if (typeof item === "string") return item;
|
|
39
|
+
if (isPageWithChildrenUtil(item)) return item.page;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if any page in a nested group or its children is active
|
|
44
|
+
function hasActiveInGroup(pages: readonly unknown[], currentPath: string): boolean {
|
|
45
|
+
return pages.some((item) => {
|
|
46
|
+
if (typeof item === "string") {
|
|
47
|
+
return isPageActive(item, currentPath);
|
|
48
|
+
}
|
|
49
|
+
if (isPageWithChildrenUtil(item)) {
|
|
50
|
+
return (
|
|
51
|
+
isPageActive(item.page, currentPath) ||
|
|
52
|
+
hasActiveChild(item.children, currentPath)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (isNestedGroupUtil(item)) {
|
|
56
|
+
return hasActiveInGroup(item.pages, currentPath);
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface CollapsibleNavItemProps {
|
|
63
|
+
item: PageWithChildren;
|
|
64
|
+
currentPath: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function CollapsibleNavItem({ item, currentPath }: CollapsibleNavItemProps) {
|
|
68
|
+
const isParentActive = isPageActive(item.page, currentPath);
|
|
69
|
+
const hasActiveChildPage = hasActiveChild(item.children, currentPath);
|
|
70
|
+
|
|
71
|
+
// Initialize expanded if this item or a child is active
|
|
72
|
+
const [isExpanded, setIsExpanded] = useState(isParentActive || hasActiveChildPage);
|
|
73
|
+
|
|
74
|
+
// Auto-expand when navigating to an active page
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (isParentActive || hasActiveChildPage) {
|
|
77
|
+
setIsExpanded(true);
|
|
78
|
+
}
|
|
79
|
+
}, [isParentActive, hasActiveChildPage]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<li>
|
|
83
|
+
{/* Parent item with chevron */}
|
|
84
|
+
<a
|
|
85
|
+
href={pageToUrl(item.page)}
|
|
86
|
+
className={cn(
|
|
87
|
+
"group flex items-center gap-2 px-2 py-1.5 text-sm rounded-md transition-colors",
|
|
88
|
+
isParentActive
|
|
89
|
+
? "bg-[var(--color-primary-light)] text-[var(--color-primary)] font-medium"
|
|
90
|
+
: "text-[var(--color-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-foreground)]",
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={(e) => {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
setIsExpanded(!isExpanded);
|
|
99
|
+
}}
|
|
100
|
+
className="flex items-center justify-center w-4 h-4 -ml-0.5"
|
|
101
|
+
aria-expanded={isExpanded}
|
|
102
|
+
aria-label={isExpanded ? "Collapse" : "Expand"}
|
|
103
|
+
>
|
|
104
|
+
<ChevronRight
|
|
105
|
+
className={cn(
|
|
106
|
+
"w-3 h-3 text-[var(--color-muted)] transition-transform duration-200",
|
|
107
|
+
isExpanded && "rotate-90",
|
|
108
|
+
)}
|
|
109
|
+
/>
|
|
110
|
+
</button>
|
|
111
|
+
<span>{formatPageName(item.page)}</span>
|
|
112
|
+
</a>
|
|
113
|
+
|
|
114
|
+
{/* Children */}
|
|
115
|
+
<div
|
|
116
|
+
className={cn(
|
|
117
|
+
"overflow-hidden transition-all duration-200",
|
|
118
|
+
isExpanded ? "max-h-96 opacity-100" : "max-h-0 opacity-0",
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
<ul className="mt-2 space-y-0.5">
|
|
122
|
+
{item.children.map((child) => {
|
|
123
|
+
const isActive = isPageActive(child, currentPath);
|
|
124
|
+
return (
|
|
125
|
+
<li key={child}>
|
|
126
|
+
<a
|
|
127
|
+
href={pageToUrl(child)}
|
|
128
|
+
className={cn(
|
|
129
|
+
"block py-1.5 pl-6 pr-2 text-sm rounded-r-md transition-colors border-l-2 -ml-px",
|
|
130
|
+
isActive
|
|
131
|
+
? "border-[var(--color-primary)] bg-[var(--color-primary-light)] text-[var(--color-primary)] font-medium"
|
|
132
|
+
: "border-transparent text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border)]",
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
{formatPageName(child)}
|
|
136
|
+
</a>
|
|
137
|
+
</li>
|
|
138
|
+
);
|
|
139
|
+
})}
|
|
140
|
+
</ul>
|
|
141
|
+
</div>
|
|
142
|
+
</li>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Component for Mintlify-style nested groups
|
|
147
|
+
interface NestedGroupNavItemProps {
|
|
148
|
+
item: NestedGroup;
|
|
149
|
+
currentPath: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function NestedGroupNavItem({ item, currentPath }: NestedGroupNavItemProps) {
|
|
153
|
+
const hasActivePage = hasActiveInGroup(item.pages, currentPath);
|
|
154
|
+
|
|
155
|
+
// Initialize expanded if any page in group is active
|
|
156
|
+
const [isExpanded, setIsExpanded] = useState(hasActivePage);
|
|
157
|
+
|
|
158
|
+
// Auto-expand when navigating to an active page
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (hasActivePage) {
|
|
161
|
+
setIsExpanded(true);
|
|
162
|
+
}
|
|
163
|
+
}, [hasActivePage]);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<li>
|
|
167
|
+
{/* Group header with chevron on far right */}
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
171
|
+
className="group flex items-center justify-between w-full px-2 py-1.5 text-sm rounded-md transition-colors text-[var(--color-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-foreground)]"
|
|
172
|
+
aria-expanded={isExpanded}
|
|
173
|
+
>
|
|
174
|
+
<span className="font-medium">{item.group}</span>
|
|
175
|
+
<ChevronDown
|
|
176
|
+
className={cn(
|
|
177
|
+
"w-4 h-4 transition-transform duration-200",
|
|
178
|
+
isExpanded && "rotate-180",
|
|
179
|
+
)}
|
|
180
|
+
/>
|
|
181
|
+
</button>
|
|
182
|
+
|
|
183
|
+
{/* Nested pages */}
|
|
184
|
+
<div
|
|
185
|
+
className={cn(
|
|
186
|
+
"overflow-hidden transition-all duration-200",
|
|
187
|
+
isExpanded ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0",
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
<ul className="mt-2 space-y-0.5">
|
|
191
|
+
{item.pages.map((nestedItem, index) => {
|
|
192
|
+
const pageString = getPageString(nestedItem);
|
|
193
|
+
|
|
194
|
+
// Handle nested groups recursively
|
|
195
|
+
if (isNestedGroupUtil(nestedItem)) {
|
|
196
|
+
return (
|
|
197
|
+
<NestedGroupNavItem
|
|
198
|
+
key={`nested-${nestedItem.group}-${index}`}
|
|
199
|
+
item={nestedItem}
|
|
200
|
+
currentPath={currentPath}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Handle legacy page with children
|
|
206
|
+
if (isPageWithChildrenUtil(nestedItem)) {
|
|
207
|
+
return (
|
|
208
|
+
<CollapsibleNavItem
|
|
209
|
+
key={pageString}
|
|
210
|
+
item={nestedItem}
|
|
211
|
+
currentPath={currentPath}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Handle simple string page
|
|
217
|
+
const isActive = isPageActive(pageString as string, currentPath);
|
|
218
|
+
return (
|
|
219
|
+
<li key={pageString}>
|
|
220
|
+
<a
|
|
221
|
+
href={pageToUrl(pageString as string)}
|
|
222
|
+
className={cn(
|
|
223
|
+
"block py-1.5 pl-6 pr-2 text-sm rounded-r-md transition-colors border-l-2 -ml-px",
|
|
224
|
+
isActive
|
|
225
|
+
? "border-[var(--color-primary)] bg-[var(--color-primary-light)] text-[var(--color-primary)] font-medium"
|
|
226
|
+
: "border-transparent text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border)]",
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
{formatPageName(pageString as string)}
|
|
230
|
+
</a>
|
|
231
|
+
</li>
|
|
232
|
+
);
|
|
233
|
+
})}
|
|
234
|
+
</ul>
|
|
235
|
+
</div>
|
|
236
|
+
</li>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Logo component that handles theme switching with CSS instead of JS state
|
|
241
|
+
function Logo({ logo, siteName }: { logo?: string | LogoConfig; siteName: string }) {
|
|
242
|
+
if (!logo) {
|
|
243
|
+
return (
|
|
244
|
+
<a
|
|
245
|
+
href="/"
|
|
246
|
+
className="text-lg font-semibold text-[var(--color-foreground)] hover:text-[var(--color-muted)] transition-colors"
|
|
247
|
+
>
|
|
248
|
+
{siteName}
|
|
249
|
+
</a>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// String logo - no theme switching needed
|
|
254
|
+
if (typeof logo === "string") {
|
|
255
|
+
return (
|
|
256
|
+
<a href="/" className="flex items-center gap-2">
|
|
257
|
+
<img src={logo} alt={siteName} className="h-8 w-auto" />
|
|
258
|
+
</a>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Object logo with light/dark variants - use CSS for theme switching
|
|
263
|
+
const { light, dark } = logo;
|
|
264
|
+
|
|
265
|
+
if (dark) {
|
|
266
|
+
return (
|
|
267
|
+
<a href="/" className="flex items-center gap-2">
|
|
268
|
+
<img src={light} alt={siteName} className="h-8 w-auto dark:hidden" />
|
|
269
|
+
<img src={dark} alt={siteName} className="h-8 w-auto hidden dark:block" />
|
|
270
|
+
</a>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Only light logo provided
|
|
275
|
+
return (
|
|
276
|
+
<a href="/" className="flex items-center gap-2">
|
|
277
|
+
<img src={light} alt={siteName} className="h-8 w-auto" />
|
|
278
|
+
</a>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function Sidebar({
|
|
283
|
+
siteName,
|
|
284
|
+
navigation,
|
|
285
|
+
logo,
|
|
286
|
+
currentPath,
|
|
287
|
+
githubUrl,
|
|
288
|
+
}: SidebarProps) {
|
|
289
|
+
// No more isDark state or MutationObserver - CSS handles theme switching
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div className="flex flex-col h-full">
|
|
293
|
+
{/* Logo/Site name + Theme toggle */}
|
|
294
|
+
<div className="flex items-center justify-between h-16 px-4">
|
|
295
|
+
<Logo logo={logo} siteName={siteName} />
|
|
296
|
+
<ThemeToggle />
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{/* Search trigger */}
|
|
300
|
+
<div className="px-4 py-3">
|
|
301
|
+
<SidebarSearchTrigger />
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{/* Navigation */}
|
|
305
|
+
<nav className="sidebar-nav flex-1 overflow-y-auto px-4 py-4">
|
|
306
|
+
{navigation.map((group) => (
|
|
307
|
+
<div key={group.group} className="mb-6">
|
|
308
|
+
<h3 className="mb-2 px-2 text-xs font-bold uppercase tracking-wider text-[var(--color-foreground)]">
|
|
309
|
+
{group.group}
|
|
310
|
+
</h3>
|
|
311
|
+
<ul className="space-y-0.5">
|
|
312
|
+
{group.pages.map((item, index) => {
|
|
313
|
+
const pageString = getPageString(item);
|
|
314
|
+
|
|
315
|
+
// Handle Mintlify-style nested groups
|
|
316
|
+
if (isNestedGroupUtil(item)) {
|
|
317
|
+
return (
|
|
318
|
+
<NestedGroupNavItem
|
|
319
|
+
key={`group-${item.group}-${index}`}
|
|
320
|
+
item={item}
|
|
321
|
+
currentPath={currentPath}
|
|
322
|
+
/>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Handle legacy page with children
|
|
327
|
+
if (isPageWithChildrenUtil(item)) {
|
|
328
|
+
return (
|
|
329
|
+
<CollapsibleNavItem
|
|
330
|
+
key={pageString}
|
|
331
|
+
item={item}
|
|
332
|
+
currentPath={currentPath}
|
|
333
|
+
/>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Handle simple string page
|
|
338
|
+
return (
|
|
339
|
+
<li key={pageString}>
|
|
340
|
+
<a
|
|
341
|
+
href={pageToUrl(pageString as string)}
|
|
342
|
+
className={cn(
|
|
343
|
+
"block px-2 py-1.5 text-sm rounded-md transition-colors",
|
|
344
|
+
isPageActive(pageString as string, currentPath)
|
|
345
|
+
? "bg-[var(--color-primary-light)] text-[var(--color-primary)] font-medium"
|
|
346
|
+
: "text-[var(--color-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-foreground)]",
|
|
347
|
+
)}
|
|
348
|
+
>
|
|
349
|
+
{formatPageName(pageString as string)}
|
|
350
|
+
</a>
|
|
351
|
+
</li>
|
|
352
|
+
);
|
|
353
|
+
})}
|
|
354
|
+
</ul>
|
|
355
|
+
</div>
|
|
356
|
+
))}
|
|
357
|
+
</nav>
|
|
358
|
+
|
|
359
|
+
{/* Bottom section with GitHub link */}
|
|
360
|
+
{githubUrl && (
|
|
361
|
+
<div className="px-4 py-3">
|
|
362
|
+
<GitHubLink url={githubUrl} />
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export default Sidebar;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { Search } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
interface SidebarSearchTriggerProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function SidebarSearchTrigger({ className }: SidebarSearchTriggerProps) {
|
|
10
|
+
// Start with null to indicate "not yet determined" - renders placeholder during SSR
|
|
11
|
+
const [isMac, setIsMac] = useState<boolean | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// Detect platform on client - runs once after hydration
|
|
15
|
+
const isMacOS =
|
|
16
|
+
navigator.userAgentData?.platform?.toLowerCase().includes("mac") ??
|
|
17
|
+
/mac/i.test(navigator.userAgent);
|
|
18
|
+
setIsMac(isMacOS);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const handleClick = () => {
|
|
22
|
+
// Dispatch custom event that SearchProvider listens for
|
|
23
|
+
// This is appropriate for Astro islands architecture where React trees are separate
|
|
24
|
+
document.dispatchEvent(new CustomEvent("open-search"));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
onClick={handleClick}
|
|
31
|
+
className={cn(
|
|
32
|
+
"flex items-center w-full gap-2 px-3 py-2 text-sm rounded-lg transition-colors",
|
|
33
|
+
"text-[var(--color-muted)] bg-[var(--color-surface-sunken)]",
|
|
34
|
+
"border border-[var(--color-border)]",
|
|
35
|
+
"hover:border-[var(--color-border-muted)] hover:text-[var(--color-foreground)]",
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
aria-label="Search documentation"
|
|
39
|
+
>
|
|
40
|
+
<Search className="w-4 h-4 flex-shrink-0" aria-hidden="true" />
|
|
41
|
+
<span className="flex-1 text-left">Search...</span>
|
|
42
|
+
<kbd
|
|
43
|
+
className={cn(
|
|
44
|
+
"hidden sm:inline-flex items-center gap-0.5 px-1.5 py-0.5",
|
|
45
|
+
"text-xs font-medium text-[var(--color-muted-foreground)]",
|
|
46
|
+
"bg-[var(--color-surface)] border border-[var(--color-border)] rounded",
|
|
47
|
+
// Hide until we know the platform to prevent hydration mismatch flash
|
|
48
|
+
isMac === null && "invisible",
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<span className="text-xs">{isMac === false ? "Ctrl" : "⌘"}</span>K
|
|
52
|
+
</kbd>
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default SidebarSearchTrigger;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface StepsProps {
|
|
2
|
+
title?: string;
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function Steps({ title, children }: StepsProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="steps my-8 not-prose">
|
|
9
|
+
{title && (
|
|
10
|
+
<h4 className="steps-title text-lg font-semibold text-[var(--color-foreground)] mb-4">
|
|
11
|
+
{title}
|
|
12
|
+
</h4>
|
|
13
|
+
)}
|
|
14
|
+
<div className="steps-list relative pl-12">
|
|
15
|
+
<div
|
|
16
|
+
className="steps-line absolute left-4 top-4 bottom-4 w-0.5 bg-[var(--color-border)]"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
/>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default Steps;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Sun, Moon } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
interface ThemeToggleProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ThemeToggle({ className }: ThemeToggleProps) {
|
|
10
|
+
const [isDark, setIsDark] = useState(false);
|
|
11
|
+
const [mounted, setMounted] = useState(false);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setMounted(true);
|
|
15
|
+
// Check initial theme
|
|
16
|
+
const isDarkMode = document.documentElement.classList.contains("dark");
|
|
17
|
+
setIsDark(isDarkMode);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
const toggleTheme = () => {
|
|
21
|
+
const newIsDark = !isDark;
|
|
22
|
+
setIsDark(newIsDark);
|
|
23
|
+
|
|
24
|
+
if (newIsDark) {
|
|
25
|
+
document.documentElement.classList.add("dark");
|
|
26
|
+
localStorage.setItem("theme", "dark");
|
|
27
|
+
} else {
|
|
28
|
+
document.documentElement.classList.remove("dark");
|
|
29
|
+
localStorage.setItem("theme", "light");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Prevent hydration mismatch by showing a placeholder until mounted
|
|
34
|
+
if (!mounted) {
|
|
35
|
+
return (
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
className={cn(
|
|
39
|
+
"flex items-center justify-center w-9 h-9 rounded-lg transition-colors",
|
|
40
|
+
"text-[var(--color-muted)] hover:text-[var(--color-foreground)]",
|
|
41
|
+
"hover:bg-[var(--color-surface-sunken)]",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
aria-label="Toggle theme"
|
|
45
|
+
>
|
|
46
|
+
<span className="w-5 h-5" />
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={toggleTheme}
|
|
55
|
+
className={cn(
|
|
56
|
+
"flex items-center justify-center w-9 h-9 rounded-lg transition-colors",
|
|
57
|
+
"text-[var(--color-muted)] hover:text-[var(--color-foreground)]",
|
|
58
|
+
"hover:bg-[var(--color-surface-sunken)]",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
|
62
|
+
>
|
|
63
|
+
{isDark ? (
|
|
64
|
+
<Sun className="w-5 h-5" />
|
|
65
|
+
) : (
|
|
66
|
+
<Moon className="w-5 h-5" />
|
|
67
|
+
)}
|
|
68
|
+
</button>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default ThemeToggle;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React components for @opendocs/components
|
|
3
|
+
*
|
|
4
|
+
* These are used when importing components in MDX snippets:
|
|
5
|
+
* import { Callout, Card } from '@opendocs/components'
|
|
6
|
+
*
|
|
7
|
+
* Note: The main docs pages use Astro components (passed via components prop).
|
|
8
|
+
* These React versions are specifically for snippet imports.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { Callout } from "./Callout";
|
|
12
|
+
export { Card } from "./Card";
|
|
13
|
+
export { CardGroup } from "./CardGroup";
|
|
14
|
+
export { Steps } from "./Steps";
|