@barodoc/theme-docs 3.0.0 → 6.0.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/package.json +6 -2
- package/src/components/Breadcrumb.astro +8 -8
- package/src/components/CodeCopy.astro +14 -13
- package/src/components/Contributors.astro +71 -0
- package/src/components/DocHeader.tsx +24 -24
- package/src/components/DocsSidebar.tsx +11 -15
- package/src/components/KeyboardShortcuts.astro +108 -0
- package/src/components/LanguageSwitcher.astro +3 -3
- package/src/components/MobileNavSheet.tsx +1 -1
- package/src/components/Search.tsx +10 -10
- package/src/components/SearchDialog.tsx +8 -8
- package/src/components/TableOfContents.astro +5 -5
- package/src/components/ThemeToggle.tsx +4 -4
- package/src/components/VersionSwitcher.tsx +79 -0
- package/src/components/api/ApiEndpoint.astro +5 -5
- package/src/components/api/ApiParam.astro +4 -4
- package/src/components/api/ApiParams.astro +3 -3
- package/src/components/api/ApiResponse.astro +2 -2
- package/src/components/index.ts +5 -0
- package/src/components/mdx/Accordion.tsx +6 -6
- package/src/components/mdx/ApiPlayground.tsx +200 -0
- package/src/components/mdx/Badge.tsx +1 -1
- package/src/components/mdx/Callout.astro +20 -20
- package/src/components/mdx/Card.astro +5 -5
- package/src/components/mdx/CodeGroup.astro +23 -20
- package/src/components/mdx/CodeGroup.tsx +6 -6
- package/src/components/mdx/DocAccordion.tsx +5 -5
- package/src/components/mdx/DocCard.tsx +2 -2
- package/src/components/mdx/DocTabs.tsx +3 -3
- package/src/components/mdx/Expandable.tsx +11 -11
- package/src/components/mdx/FileTree.tsx +6 -6
- package/src/components/mdx/Frame.tsx +2 -2
- package/src/components/mdx/ImageZoom.tsx +35 -0
- package/src/components/mdx/Mermaid.tsx +3 -3
- package/src/components/mdx/ParamField.tsx +7 -7
- package/src/components/mdx/ResponseField.tsx +6 -6
- package/src/components/mdx/Step.astro +2 -2
- package/src/components/mdx/Steps.astro +3 -3
- package/src/components/mdx/Steps.tsx +10 -19
- package/src/components/mdx/Tabs.tsx +5 -8
- package/src/components/mdx/Tooltip.tsx +20 -19
- package/src/components/mdx/Video.tsx +71 -0
- package/src/components/ui/accordion.tsx +2 -2
- package/src/components/ui/alert.tsx +1 -1
- package/src/components/ui/button.tsx +3 -3
- package/src/components/ui/card.tsx +2 -2
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/ui/scroll-area.tsx +1 -1
- package/src/components/ui/separator.tsx +1 -1
- package/src/components/ui/sheet.tsx +3 -3
- package/src/components/ui/tabs.tsx +2 -2
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/index.ts +33 -1
- package/src/layouts/BaseLayout.astro +10 -1
- package/src/layouts/BlogLayout.astro +93 -0
- package/src/layouts/DocsLayout.astro +72 -23
- package/src/pages/404.astro +5 -5
- package/src/pages/blog/[...slug].astro +39 -0
- package/src/pages/blog/index.astro +92 -0
- package/src/pages/changelog/index.astro +72 -0
- package/src/pages/docs/[...slug].astro +6 -2
- package/src/pages/index.astro +21 -21
- package/src/styles/global.css +1041 -166
|
@@ -43,19 +43,19 @@ export function SearchDialog({ className }: SearchDialogProps) {
|
|
|
43
43
|
return (
|
|
44
44
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
45
45
|
<DialogContent className={cn("sm:max-w-2xl p-0 gap-0", className)}>
|
|
46
|
-
<DialogHeader className="px-4 py-3 border-b border-[var(--
|
|
46
|
+
<DialogHeader className="px-4 py-3 border-b border-[var(--bd-border)]">
|
|
47
47
|
<DialogTitle className="sr-only">Search documentation</DialogTitle>
|
|
48
48
|
<div className="flex items-center gap-3">
|
|
49
|
-
<SearchIcon className="h-5 w-5 text-[var(--
|
|
49
|
+
<SearchIcon className="h-5 w-5 text-[var(--bd-text-muted)]" />
|
|
50
50
|
<input
|
|
51
51
|
type="text"
|
|
52
52
|
placeholder="Search documentation..."
|
|
53
53
|
value={query}
|
|
54
54
|
onChange={(e) => setQuery(e.target.value)}
|
|
55
|
-
className="flex-1 bg-transparent text-base outline-none placeholder:text-[var(--
|
|
55
|
+
className="flex-1 bg-transparent text-base outline-none placeholder:text-[var(--bd-text-muted)]"
|
|
56
56
|
autoFocus
|
|
57
57
|
/>
|
|
58
|
-
<kbd className="hidden sm:inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-[var(--
|
|
58
|
+
<kbd className="hidden sm:inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-[var(--bd-bg-subtle)] border border-[var(--bd-border)] rounded text-[var(--bd-text-muted)]">
|
|
59
59
|
Esc
|
|
60
60
|
</kbd>
|
|
61
61
|
</div>
|
|
@@ -65,12 +65,12 @@ export function SearchDialog({ className }: SearchDialogProps) {
|
|
|
65
65
|
<div id="pagefind-results" className="pagefind-ui" />
|
|
66
66
|
) : (
|
|
67
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(--
|
|
69
|
-
<p className="text-sm text-[var(--
|
|
68
|
+
<SearchIcon className="h-12 w-12 text-[var(--bd-text-muted)] mb-4" />
|
|
69
|
+
<p className="text-sm text-[var(--bd-text-secondary)]">
|
|
70
70
|
Start typing to search the documentation
|
|
71
71
|
</p>
|
|
72
|
-
<p className="text-xs text-[var(--
|
|
73
|
-
Press <kbd className="px-1.5 py-0.5 bg-[var(--
|
|
72
|
+
<p className="text-xs text-[var(--bd-text-muted)] mt-2">
|
|
73
|
+
Press <kbd className="px-1.5 py-0.5 bg-[var(--bd-bg-subtle)] border border-[var(--bd-border)] rounded text-xs">⌘K</kbd> to open search
|
|
74
74
|
</p>
|
|
75
75
|
</div>
|
|
76
76
|
)}
|
|
@@ -17,19 +17,19 @@ const filteredHeadings = headings.filter(h => h.depth >= 2 && h.depth <= 3);
|
|
|
17
17
|
|
|
18
18
|
{filteredHeadings.length > 0 && (
|
|
19
19
|
<div class="toc-container">
|
|
20
|
-
<h4 class="text-
|
|
20
|
+
<h4 class="text-[11px] font-semibold uppercase tracking-widest text-[var(--bd-text-muted)] mb-3">
|
|
21
21
|
On this page
|
|
22
22
|
</h4>
|
|
23
23
|
<nav class="relative">
|
|
24
|
-
<div class="absolute left-0 top-0 bottom-0 w-px bg-[var(--
|
|
25
|
-
<ul class="space-y-
|
|
24
|
+
<div class="absolute left-0 top-0 bottom-0 w-px bg-[var(--bd-border)]"></div>
|
|
25
|
+
<ul class="space-y-0.5">
|
|
26
26
|
{filteredHeadings.map((heading) => (
|
|
27
27
|
<li class="relative">
|
|
28
28
|
<a
|
|
29
29
|
href={`#${heading.slug}`}
|
|
30
30
|
class:list={[
|
|
31
|
-
"toc-link block text-
|
|
32
|
-
heading.depth === 2 ? "py-1.5
|
|
31
|
+
"toc-link block text-[var(--bd-text-muted)] hover:text-[var(--bd-text)] transition-all duration-150",
|
|
32
|
+
heading.depth === 2 ? "py-1 pl-3.5 text-[13px]" : "py-0.5 pl-6 text-xs"
|
|
33
33
|
]}
|
|
34
34
|
data-heading={heading.slug}
|
|
35
35
|
>
|
|
@@ -33,7 +33,7 @@ export function ThemeToggle() {
|
|
|
33
33
|
if (!mounted) {
|
|
34
34
|
return (
|
|
35
35
|
<button
|
|
36
|
-
className="p-2 rounded-lg hover:bg-
|
|
36
|
+
className="p-2 rounded-lg hover:bg-[var(--bd-bg-hover)] transition-colors"
|
|
37
37
|
aria-label="Toggle theme"
|
|
38
38
|
>
|
|
39
39
|
<div className="w-5 h-5" />
|
|
@@ -44,13 +44,13 @@ export function ThemeToggle() {
|
|
|
44
44
|
return (
|
|
45
45
|
<button
|
|
46
46
|
onClick={toggleTheme}
|
|
47
|
-
className="p-2 rounded-lg hover:bg-
|
|
47
|
+
className="p-2 rounded-lg hover:bg-[var(--bd-bg-hover)] transition-colors"
|
|
48
48
|
aria-label={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
|
|
49
49
|
>
|
|
50
50
|
{theme === "light" ? (
|
|
51
|
-
<Moon className="w-5 h-5 text-
|
|
51
|
+
<Moon className="w-5 h-5 text-[var(--bd-text-secondary)]" />
|
|
52
52
|
) : (
|
|
53
|
-
<Sun className="w-5 h-5 text-
|
|
53
|
+
<Sun className="w-5 h-5 text-[var(--bd-text-secondary)]" />
|
|
54
54
|
)}
|
|
55
55
|
</button>
|
|
56
56
|
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
3
|
+
|
|
4
|
+
interface VersionConfig {
|
|
5
|
+
label: string;
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface VersionSwitcherProps {
|
|
10
|
+
versions: VersionConfig[];
|
|
11
|
+
currentPath: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function VersionSwitcher({ versions, currentPath }: VersionSwitcherProps) {
|
|
15
|
+
if (!versions || versions.length <= 1) return null;
|
|
16
|
+
|
|
17
|
+
const current = versions.find((v) => currentPath.includes(`/docs/${v.path}/`)) || versions[0];
|
|
18
|
+
|
|
19
|
+
function switchVersion(target: VersionConfig) {
|
|
20
|
+
const regex = /\/docs\/([^/]+)\//;
|
|
21
|
+
const match = currentPath.match(regex);
|
|
22
|
+
if (match) {
|
|
23
|
+
const newPath = currentPath.replace(`/docs/${match[1]}/`, `/docs/${target.path}/`);
|
|
24
|
+
window.location.href = newPath;
|
|
25
|
+
} else {
|
|
26
|
+
window.location.href = `/docs/${target.path}/`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<DropdownMenu.Root>
|
|
32
|
+
<DropdownMenu.Trigger asChild>
|
|
33
|
+
<button className="bd-version-trigger">
|
|
34
|
+
{current.label}
|
|
35
|
+
<svg
|
|
36
|
+
width="12"
|
|
37
|
+
height="12"
|
|
38
|
+
viewBox="0 0 24 24"
|
|
39
|
+
fill="none"
|
|
40
|
+
stroke="currentColor"
|
|
41
|
+
strokeWidth="2"
|
|
42
|
+
strokeLinecap="round"
|
|
43
|
+
strokeLinejoin="round"
|
|
44
|
+
>
|
|
45
|
+
<path d="m6 9 6 6 6-6" />
|
|
46
|
+
</svg>
|
|
47
|
+
</button>
|
|
48
|
+
</DropdownMenu.Trigger>
|
|
49
|
+
|
|
50
|
+
<DropdownMenu.Portal>
|
|
51
|
+
<DropdownMenu.Content className="bd-version-menu" sideOffset={4} align="start">
|
|
52
|
+
{versions.map((v) => (
|
|
53
|
+
<DropdownMenu.Item
|
|
54
|
+
key={v.path}
|
|
55
|
+
className={`bd-version-item ${v.path === current.path ? "bd-version-active" : ""}`}
|
|
56
|
+
onSelect={() => switchVersion(v)}
|
|
57
|
+
>
|
|
58
|
+
{v.label}
|
|
59
|
+
{v.path === current.path && (
|
|
60
|
+
<svg
|
|
61
|
+
width="14"
|
|
62
|
+
height="14"
|
|
63
|
+
viewBox="0 0 24 24"
|
|
64
|
+
fill="none"
|
|
65
|
+
stroke="currentColor"
|
|
66
|
+
strokeWidth="2.5"
|
|
67
|
+
strokeLinecap="round"
|
|
68
|
+
strokeLinejoin="round"
|
|
69
|
+
>
|
|
70
|
+
<polyline points="20 6 9 17 4 12" />
|
|
71
|
+
</svg>
|
|
72
|
+
)}
|
|
73
|
+
</DropdownMenu.Item>
|
|
74
|
+
))}
|
|
75
|
+
</DropdownMenu.Content>
|
|
76
|
+
</DropdownMenu.Portal>
|
|
77
|
+
</DropdownMenu.Root>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -16,17 +16,17 @@ const methodColors = {
|
|
|
16
16
|
};
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
<div class="not-prose my-6 rounded-lg border border-[var(--
|
|
20
|
-
<div class="flex items-center gap-3 px-4 py-3 bg-[var(--
|
|
19
|
+
<div class="not-prose my-6 rounded-lg border border-[var(--bd-border)] overflow-hidden">
|
|
20
|
+
<div class="flex items-center gap-3 px-4 py-3 bg-[var(--bd-bg-subtle)] border-b border-[var(--bd-border)]">
|
|
21
21
|
<span class:list={["px-2 py-1 text-xs font-bold rounded uppercase", methodColors[method]]}>
|
|
22
22
|
{method}
|
|
23
23
|
</span>
|
|
24
|
-
<code class="text-sm font-mono text-[var(--
|
|
24
|
+
<code class="text-sm font-mono text-[var(--bd-text)]">{path}</code>
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
{description && (
|
|
28
|
-
<div class="px-4 py-3 border-b border-[var(--
|
|
29
|
-
<p class="text-sm text-[var(--
|
|
28
|
+
<div class="px-4 py-3 border-b border-[var(--bd-border)]">
|
|
29
|
+
<p class="text-sm text-[var(--bd-text-secondary)]">{description}</p>
|
|
30
30
|
</div>
|
|
31
31
|
)}
|
|
32
32
|
|
|
@@ -9,10 +9,10 @@ interface Props {
|
|
|
9
9
|
const { name, type, required = false, description } = Astro.props;
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
<div class="flex flex-col gap-1 py-3 border-b border-[var(--
|
|
12
|
+
<div class="flex flex-col gap-1 py-3 border-b border-[var(--bd-border)] last:border-b-0">
|
|
13
13
|
<div class="flex items-center gap-2">
|
|
14
|
-
<code class="text-sm font-semibold text-[var(--
|
|
15
|
-
<span class="text-xs text-[var(--
|
|
14
|
+
<code class="text-sm font-semibold text-[var(--bd-text)]">{name}</code>
|
|
15
|
+
<span class="text-xs text-[var(--bd-text-secondary)]">{type}</span>
|
|
16
16
|
{required && (
|
|
17
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
18
|
required
|
|
@@ -20,7 +20,7 @@ const { name, type, required = false, description } = Astro.props;
|
|
|
20
20
|
)}
|
|
21
21
|
</div>
|
|
22
22
|
{description && (
|
|
23
|
-
<p class="text-sm text-[var(--
|
|
23
|
+
<p class="text-sm text-[var(--bd-text-secondary)]">{description}</p>
|
|
24
24
|
)}
|
|
25
25
|
<slot />
|
|
26
26
|
</div>
|
|
@@ -7,9 +7,9 @@ const { title = "Parameters" } = Astro.props;
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
<div class="my-4 px-2">
|
|
10
|
-
<h4 class="text-sm font-semibold text-[var(--
|
|
11
|
-
<div class="border border-[var(--
|
|
12
|
-
<div class="divide-y divide-[var(--
|
|
10
|
+
<h4 class="text-sm font-semibold text-[var(--bd-text)] mb-2">{title}</h4>
|
|
11
|
+
<div class="border border-[var(--bd-border)] rounded-lg overflow-hidden">
|
|
12
|
+
<div class="divide-y divide-[var(--bd-border)] px-4">
|
|
13
13
|
<slot />
|
|
14
14
|
</div>
|
|
15
15
|
</div>
|
|
@@ -26,10 +26,10 @@ const statusColor = statusColors[status] || "bg-gray-100 dark:bg-gray-900/30 tex
|
|
|
26
26
|
{status}
|
|
27
27
|
</span>
|
|
28
28
|
{description && (
|
|
29
|
-
<span class="text-sm text-[var(--
|
|
29
|
+
<span class="text-sm text-[var(--bd-text-secondary)]">{description}</span>
|
|
30
30
|
)}
|
|
31
31
|
</div>
|
|
32
|
-
<div class="border border-[var(--
|
|
32
|
+
<div class="border border-[var(--bd-border)] rounded-lg overflow-hidden px-2">
|
|
33
33
|
<slot />
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
package/src/components/index.ts
CHANGED
|
@@ -24,6 +24,11 @@ export { Expandable, ExpandableList, ExpandableItem } from "./mdx/Expandable.tsx
|
|
|
24
24
|
export { Icon, CheckIcon, XIcon, InfoIcon, WarningIcon } from "./mdx/Icon.tsx";
|
|
25
25
|
export { Steps, Step } from "./mdx/Steps.tsx";
|
|
26
26
|
export { Mermaid } from "./mdx/Mermaid.tsx";
|
|
27
|
+
export { ImageZoom } from "./mdx/ImageZoom.tsx";
|
|
28
|
+
export { Video } from "./mdx/Video.tsx";
|
|
29
|
+
export { ApiPlayground } from "./mdx/ApiPlayground.tsx";
|
|
30
|
+
|
|
31
|
+
export { VersionSwitcher } from "./VersionSwitcher.tsx";
|
|
27
32
|
|
|
28
33
|
// Legacy exports for backwards compatibility
|
|
29
34
|
export { Tabs, Tab } from "./mdx/Tabs.tsx";
|
|
@@ -11,21 +11,21 @@ export function Accordion({ title, icon, defaultOpen = false, children }: Accord
|
|
|
11
11
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
|
-
<div className="accordion-item border border-[var(--
|
|
14
|
+
<div className="accordion-item border border-[var(--bd-border)] rounded-xl overflow-hidden bg-[var(--bd-bg)] my-3">
|
|
15
15
|
<button
|
|
16
16
|
type="button"
|
|
17
17
|
onClick={() => setIsOpen(!isOpen)}
|
|
18
|
-
className="w-full flex items-center gap-3 px-5 py-4 text-left hover:bg-[var(--
|
|
18
|
+
className="w-full flex items-center gap-3 px-5 py-4 text-left hover:bg-[var(--bd-bg-subtle)] transition-colors"
|
|
19
19
|
aria-expanded={isOpen}
|
|
20
20
|
>
|
|
21
21
|
{icon && (
|
|
22
|
-
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-[var(--
|
|
22
|
+
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-[var(--bd-bg-subtle)] text-lg shrink-0">
|
|
23
23
|
{icon}
|
|
24
24
|
</span>
|
|
25
25
|
)}
|
|
26
|
-
<span className="flex-1 font-medium text-[var(--
|
|
26
|
+
<span className="flex-1 font-medium text-[var(--bd-text)]">{title}</span>
|
|
27
27
|
<svg
|
|
28
|
-
className={`w-5 h-5 text-[var(--
|
|
28
|
+
className={`w-5 h-5 text-[var(--bd-text-secondary)] transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
|
|
29
29
|
fill="none"
|
|
30
30
|
stroke="currentColor"
|
|
31
31
|
viewBox="0 0 24 24"
|
|
@@ -38,7 +38,7 @@ export function Accordion({ title, icon, defaultOpen = false, children }: Accord
|
|
|
38
38
|
isOpen ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
|
|
39
39
|
}`}
|
|
40
40
|
>
|
|
41
|
-
<div className="px-5 pb-5 pt-1 text-sm text-[var(--
|
|
41
|
+
<div className="px-5 pb-5 pt-1 text-sm text-[var(--bd-text-secondary)] leading-relaxed">
|
|
42
42
|
{children}
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
|
|
4
|
+
interface ParamDef {
|
|
5
|
+
name: string;
|
|
6
|
+
in: "query" | "path" | "header" | "body";
|
|
7
|
+
type?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ApiPlaygroundProps {
|
|
14
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
15
|
+
url: string;
|
|
16
|
+
params?: ParamDef[];
|
|
17
|
+
body?: string;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const methodColors: Record<string, string> = {
|
|
23
|
+
GET: "bg-green-100 text-green-800 dark:bg-green-950 dark:text-green-300",
|
|
24
|
+
POST: "bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300",
|
|
25
|
+
PUT: "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300",
|
|
26
|
+
PATCH: "bg-orange-100 text-orange-800 dark:bg-orange-950 dark:text-orange-300",
|
|
27
|
+
DELETE: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function ApiPlayground({
|
|
31
|
+
method = "GET",
|
|
32
|
+
url,
|
|
33
|
+
params = [],
|
|
34
|
+
body: initialBody,
|
|
35
|
+
headers: initialHeaders = {},
|
|
36
|
+
className,
|
|
37
|
+
}: ApiPlaygroundProps) {
|
|
38
|
+
const [paramValues, setParamValues] = React.useState<Record<string, string>>(() => {
|
|
39
|
+
const vals: Record<string, string> = {};
|
|
40
|
+
for (const p of params) {
|
|
41
|
+
vals[p.name] = p.defaultValue || "";
|
|
42
|
+
}
|
|
43
|
+
return vals;
|
|
44
|
+
});
|
|
45
|
+
const [bodyValue, setBodyValue] = React.useState(initialBody || "");
|
|
46
|
+
const [response, setResponse] = React.useState<{
|
|
47
|
+
status: number;
|
|
48
|
+
statusText: string;
|
|
49
|
+
body: string;
|
|
50
|
+
time: number;
|
|
51
|
+
} | null>(null);
|
|
52
|
+
const [loading, setLoading] = React.useState(false);
|
|
53
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
54
|
+
|
|
55
|
+
function buildUrl(): string {
|
|
56
|
+
let finalUrl = url;
|
|
57
|
+
const queryParams = new URLSearchParams();
|
|
58
|
+
|
|
59
|
+
for (const p of params) {
|
|
60
|
+
const val = paramValues[p.name] || "";
|
|
61
|
+
if (p.in === "path") {
|
|
62
|
+
finalUrl = finalUrl.replace(`{${p.name}}`, encodeURIComponent(val));
|
|
63
|
+
} else if (p.in === "query" && val) {
|
|
64
|
+
queryParams.set(p.name, val);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const qs = queryParams.toString();
|
|
69
|
+
return qs ? `${finalUrl}?${qs}` : finalUrl;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function sendRequest() {
|
|
73
|
+
setLoading(true);
|
|
74
|
+
setError(null);
|
|
75
|
+
setResponse(null);
|
|
76
|
+
|
|
77
|
+
const start = performance.now();
|
|
78
|
+
const reqUrl = buildUrl();
|
|
79
|
+
|
|
80
|
+
const headers: Record<string, string> = { ...initialHeaders };
|
|
81
|
+
for (const p of params) {
|
|
82
|
+
if (p.in === "header" && paramValues[p.name]) {
|
|
83
|
+
headers[p.name] = paramValues[p.name];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const hasBody = ["POST", "PUT", "PATCH"].includes(method) && bodyValue;
|
|
88
|
+
if (hasBody && !headers["Content-Type"]) {
|
|
89
|
+
headers["Content-Type"] = "application/json";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(reqUrl, {
|
|
94
|
+
method,
|
|
95
|
+
headers,
|
|
96
|
+
body: hasBody ? bodyValue : undefined,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const elapsed = Math.round(performance.now() - start);
|
|
100
|
+
let text: string;
|
|
101
|
+
try {
|
|
102
|
+
const json = await res.json();
|
|
103
|
+
text = JSON.stringify(json, null, 2);
|
|
104
|
+
} catch {
|
|
105
|
+
text = await res.text();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setResponse({ status: res.status, statusText: res.statusText, body: text, time: elapsed });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
setError(err instanceof Error ? err.message : "Request failed");
|
|
111
|
+
} finally {
|
|
112
|
+
setLoading(false);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className={cn("bd-playground", className)}>
|
|
118
|
+
{/* Header */}
|
|
119
|
+
<div className="bd-playground-header">
|
|
120
|
+
<span className={cn("bd-playground-method", methodColors[method])}>
|
|
121
|
+
{method}
|
|
122
|
+
</span>
|
|
123
|
+
<code className="bd-playground-url">{url}</code>
|
|
124
|
+
<button
|
|
125
|
+
className="bd-playground-send"
|
|
126
|
+
onClick={sendRequest}
|
|
127
|
+
disabled={loading}
|
|
128
|
+
>
|
|
129
|
+
{loading ? "Sending..." : "Send"}
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Parameters */}
|
|
134
|
+
{params.length > 0 && (
|
|
135
|
+
<div className="bd-playground-params">
|
|
136
|
+
{params.map((p) => (
|
|
137
|
+
<div key={p.name} className="bd-playground-param">
|
|
138
|
+
<label className="bd-playground-label">
|
|
139
|
+
<span>{p.name}</span>
|
|
140
|
+
<span className="bd-playground-param-meta">
|
|
141
|
+
{p.in}
|
|
142
|
+
{p.required && <span className="bd-playground-required">*</span>}
|
|
143
|
+
</span>
|
|
144
|
+
</label>
|
|
145
|
+
<input
|
|
146
|
+
type="text"
|
|
147
|
+
className="bd-playground-input"
|
|
148
|
+
value={paramValues[p.name] || ""}
|
|
149
|
+
placeholder={p.description || p.type || ""}
|
|
150
|
+
onChange={(e) =>
|
|
151
|
+
setParamValues((prev) => ({ ...prev, [p.name]: e.target.value }))
|
|
152
|
+
}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{/* Body */}
|
|
160
|
+
{["POST", "PUT", "PATCH"].includes(method) && (
|
|
161
|
+
<div className="bd-playground-body-section">
|
|
162
|
+
<label className="bd-playground-label">Request Body</label>
|
|
163
|
+
<textarea
|
|
164
|
+
className="bd-playground-textarea"
|
|
165
|
+
value={bodyValue}
|
|
166
|
+
onChange={(e) => setBodyValue(e.target.value)}
|
|
167
|
+
rows={6}
|
|
168
|
+
placeholder='{ "key": "value" }'
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{/* Response */}
|
|
174
|
+
{(response || error) && (
|
|
175
|
+
<div className="bd-playground-response">
|
|
176
|
+
{error ? (
|
|
177
|
+
<div className="bd-playground-error">{error}</div>
|
|
178
|
+
) : response ? (
|
|
179
|
+
<>
|
|
180
|
+
<div className="bd-playground-response-header">
|
|
181
|
+
<span
|
|
182
|
+
className={cn(
|
|
183
|
+
"bd-playground-status",
|
|
184
|
+
response.status < 300 ? "bd-status-ok" : "bd-status-err"
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
{response.status} {response.statusText}
|
|
188
|
+
</span>
|
|
189
|
+
<span className="bd-playground-time">{response.time}ms</span>
|
|
190
|
+
</div>
|
|
191
|
+
<pre className="bd-playground-pre">
|
|
192
|
+
<code>{response.body}</code>
|
|
193
|
+
</pre>
|
|
194
|
+
</>
|
|
195
|
+
) : null}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -13,7 +13,7 @@ const badgeVariants = cva(
|
|
|
13
13
|
warning: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300",
|
|
14
14
|
error: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",
|
|
15
15
|
info: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300",
|
|
16
|
-
outline: "border border-[var(--
|
|
16
|
+
outline: "border border-[var(--bd-border)] text-[var(--bd-text-secondary)]",
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
defaultVariants: {
|
|
@@ -8,34 +8,34 @@ const { type = "info", title } = Astro.props;
|
|
|
8
8
|
|
|
9
9
|
const styles = {
|
|
10
10
|
info: {
|
|
11
|
-
container: "bg-blue-50/70 dark:bg-blue-
|
|
12
|
-
iconBg: "bg-blue-100 dark:bg-blue-
|
|
11
|
+
container: "bg-blue-50/70 dark:bg-blue-500/10 border-l-blue-500",
|
|
12
|
+
iconBg: "bg-blue-100 dark:bg-blue-500/20",
|
|
13
13
|
icon: "text-blue-600 dark:text-blue-400",
|
|
14
|
-
title: "text-blue-900 dark:text-blue-
|
|
14
|
+
title: "text-blue-900 dark:text-blue-200",
|
|
15
15
|
},
|
|
16
16
|
note: {
|
|
17
|
-
container: "bg-gray-50/70 dark:bg-gray-
|
|
18
|
-
iconBg: "bg-gray-100 dark:bg-gray-
|
|
17
|
+
container: "bg-gray-50/70 dark:bg-gray-500/10 border-l-gray-400",
|
|
18
|
+
iconBg: "bg-gray-100 dark:bg-gray-500/20",
|
|
19
19
|
icon: "text-gray-600 dark:text-gray-400",
|
|
20
|
-
title: "text-gray-900 dark:text-gray-
|
|
20
|
+
title: "text-gray-900 dark:text-gray-200",
|
|
21
21
|
},
|
|
22
22
|
warning: {
|
|
23
|
-
container: "bg-orange-50/70 dark:bg-orange-
|
|
24
|
-
iconBg: "bg-orange-100 dark:bg-orange-
|
|
23
|
+
container: "bg-orange-50/70 dark:bg-orange-500/10 border-l-orange-500",
|
|
24
|
+
iconBg: "bg-orange-100 dark:bg-orange-500/20",
|
|
25
25
|
icon: "text-orange-600 dark:text-orange-400",
|
|
26
|
-
title: "text-orange-900 dark:text-orange-
|
|
26
|
+
title: "text-orange-900 dark:text-orange-200",
|
|
27
27
|
},
|
|
28
28
|
tip: {
|
|
29
|
-
container: "bg-green-50/70 dark:bg-green-
|
|
30
|
-
iconBg: "bg-green-100 dark:bg-green-
|
|
29
|
+
container: "bg-green-50/70 dark:bg-green-500/10 border-l-green-500",
|
|
30
|
+
iconBg: "bg-green-100 dark:bg-green-500/20",
|
|
31
31
|
icon: "text-green-600 dark:text-green-400",
|
|
32
|
-
title: "text-green-900 dark:text-green-
|
|
32
|
+
title: "text-green-900 dark:text-green-200",
|
|
33
33
|
},
|
|
34
34
|
danger: {
|
|
35
|
-
container: "bg-red-50/70 dark:bg-red-
|
|
36
|
-
iconBg: "bg-red-100 dark:bg-red-
|
|
35
|
+
container: "bg-red-50/70 dark:bg-red-500/10 border-l-red-500",
|
|
36
|
+
iconBg: "bg-red-100 dark:bg-red-500/20",
|
|
37
37
|
icon: "text-red-600 dark:text-red-400",
|
|
38
|
-
title: "text-red-900 dark:text-red-
|
|
38
|
+
title: "text-red-900 dark:text-red-200",
|
|
39
39
|
},
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -60,17 +60,17 @@ const icon = icons[type];
|
|
|
60
60
|
const displayTitle = title || defaultTitles[type];
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
-
<div class:list={["not-prose my-
|
|
64
|
-
<div class="
|
|
63
|
+
<div class:list={["not-prose my-5 rounded-lg border-l-4 overflow-hidden", style.container]}>
|
|
64
|
+
<div class="px-4 py-3.5">
|
|
65
65
|
<div class="flex items-start gap-3">
|
|
66
|
-
<div class:list={["shrink-0 flex items-center justify-center w-6 h-6 rounded", style.iconBg]}>
|
|
66
|
+
<div class:list={["shrink-0 flex items-center justify-center w-6 h-6 rounded-md", style.iconBg]}>
|
|
67
67
|
<div class:list={[style.icon]} set:html={icon} />
|
|
68
68
|
</div>
|
|
69
69
|
<div class="flex-1 min-w-0">
|
|
70
70
|
{displayTitle && (
|
|
71
|
-
<p class:list={["font-semibold text-
|
|
71
|
+
<p class:list={["font-semibold text-[13px] mb-1", style.title]}>{displayTitle}</p>
|
|
72
72
|
)}
|
|
73
|
-
<div class="text-
|
|
73
|
+
<div class="text-[13px] text-zinc-700 dark:text-zinc-300 leading-relaxed [&>p]:m-0 [&>p+p]:mt-1.5">
|
|
74
74
|
<slot />
|
|
75
75
|
</div>
|
|
76
76
|
</div>
|
|
@@ -13,27 +13,27 @@ const Tag = href ? "a" : "div";
|
|
|
13
13
|
<Tag
|
|
14
14
|
href={href}
|
|
15
15
|
class:list={[
|
|
16
|
-
"card-component not-prose group block p-4 rounded-lg border border-[var(--
|
|
16
|
+
"card-component not-prose group block p-4 rounded-lg border border-[var(--bd-border)] bg-[var(--bd-bg)] no-underline",
|
|
17
17
|
href && "cursor-pointer hover:border-primary-300 dark:hover:border-primary-600 hover:shadow-md transition-all duration-200"
|
|
18
18
|
]}
|
|
19
19
|
style={href ? "cursor: pointer;" : ""}
|
|
20
20
|
>
|
|
21
21
|
<div class="flex flex-col gap-3">
|
|
22
22
|
{icon && (
|
|
23
|
-
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-[var(--
|
|
23
|
+
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-[var(--bd-bg-subtle)] group-hover:bg-primary-50 dark:group-hover:bg-primary-950/50 transition-colors">
|
|
24
24
|
<span class="text-xl">{icon}</span>
|
|
25
25
|
</div>
|
|
26
26
|
)}
|
|
27
27
|
<div class="flex-1 min-w-0">
|
|
28
28
|
<div class="flex items-center gap-2">
|
|
29
|
-
<h3 class="font-semibold text-[var(--
|
|
29
|
+
<h3 class="font-semibold text-[var(--bd-text)] group-hover:text-primary-700 dark:group-hover:text-primary-300 transition-colors">{title}</h3>
|
|
30
30
|
{href && (
|
|
31
|
-
<svg class="w-4 h-4 text-[var(--
|
|
31
|
+
<svg class="w-4 h-4 text-[var(--bd-text-muted)] opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
32
32
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
33
33
|
</svg>
|
|
34
34
|
)}
|
|
35
35
|
</div>
|
|
36
|
-
<div class="mt-1 text-sm text-[var(--
|
|
36
|
+
<div class="mt-1 text-sm text-[var(--bd-text-secondary)] group-hover:text-[var(--bd-text)] leading-relaxed transition-colors">
|
|
37
37
|
<slot />
|
|
38
38
|
</div>
|
|
39
39
|
</div>
|