@barodoc/theme-docs 5.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/Contributors.astro +71 -0
- package/src/components/KeyboardShortcuts.astro +108 -0
- package/src/components/VersionSwitcher.tsx +79 -0
- package/src/components/index.ts +5 -0
- package/src/components/mdx/ApiPlayground.tsx +200 -0
- package/src/components/mdx/ImageZoom.tsx +35 -0
- package/src/components/mdx/Video.tsx +71 -0
- package/src/index.ts +27 -1
- package/src/layouts/BaseLayout.astro +3 -0
- package/src/layouts/BlogLayout.astro +93 -0
- package/src/layouts/DocsLayout.astro +33 -1
- 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 +4 -0
- package/src/styles/global.css +568 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barodoc/theme-docs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Documentation theme for Barodoc",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,9 +28,13 @@
|
|
|
28
28
|
"class-variance-authority": "^0.7.1",
|
|
29
29
|
"clsx": "^2.1.1",
|
|
30
30
|
"lucide-react": "^0.563.0",
|
|
31
|
+
"medium-zoom": "^1.1.0",
|
|
31
32
|
"mermaid": "^11.12.2",
|
|
33
|
+
"reading-time": "^1.5.0",
|
|
34
|
+
"rehype-katex": "^7.0.1",
|
|
35
|
+
"remark-math": "^6.0.0",
|
|
32
36
|
"tailwind-merge": "^3.4.0",
|
|
33
|
-
"@barodoc/core": "
|
|
37
|
+
"@barodoc/core": "6.0.0"
|
|
34
38
|
},
|
|
35
39
|
"peerDependencies": {
|
|
36
40
|
"astro": "^5.0.0",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
filePath?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Contributor {
|
|
9
|
+
name: string;
|
|
10
|
+
email: string;
|
|
11
|
+
avatarUrl: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { filePath } = Astro.props;
|
|
15
|
+
|
|
16
|
+
let contributors: Contributor[] = [];
|
|
17
|
+
|
|
18
|
+
if (filePath) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = execSync(
|
|
21
|
+
`git log --format='%aN|%aE' -- "${filePath}"`,
|
|
22
|
+
{ encoding: "utf-8", timeout: 5000 }
|
|
23
|
+
).trim();
|
|
24
|
+
|
|
25
|
+
if (raw) {
|
|
26
|
+
const seen = new Map<string, Contributor>();
|
|
27
|
+
for (const line of raw.split("\n")) {
|
|
28
|
+
const [name, email] = line.split("|");
|
|
29
|
+
if (!name || seen.has(email)) continue;
|
|
30
|
+
const hash = email.trim().toLowerCase();
|
|
31
|
+
seen.set(email, {
|
|
32
|
+
name: name.trim(),
|
|
33
|
+
email: email.trim(),
|
|
34
|
+
avatarUrl: `https://gravatar.com/avatar/${await computeHash(hash)}?s=64&d=mp`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
contributors = Array.from(seen.values());
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// git not available or file not tracked
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function computeHash(input: string): Promise<string> {
|
|
45
|
+
const encoder = new TextEncoder();
|
|
46
|
+
const data = encoder.encode(input);
|
|
47
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
48
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
49
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
50
|
+
.join("");
|
|
51
|
+
}
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
{contributors.length > 0 && (
|
|
55
|
+
<div class="bd-contributors">
|
|
56
|
+
<span class="bd-contributors-label">Contributors</span>
|
|
57
|
+
<div class="bd-contributors-avatars">
|
|
58
|
+
{contributors.map((c) => (
|
|
59
|
+
<img
|
|
60
|
+
src={c.avatarUrl}
|
|
61
|
+
alt={c.name}
|
|
62
|
+
title={c.name}
|
|
63
|
+
class="bd-contributor-avatar"
|
|
64
|
+
loading="lazy"
|
|
65
|
+
width="28"
|
|
66
|
+
height="28"
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Keyboard shortcuts: Cmd/Ctrl+K → search, ←/→ → prev/next, ? → help modal
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
function initKeyboardShortcuts() {
|
|
7
|
+
if (window.__bdShortcutsInit) return;
|
|
8
|
+
window.__bdShortcutsInit = true;
|
|
9
|
+
|
|
10
|
+
const isMac = navigator.platform.toUpperCase().includes('MAC');
|
|
11
|
+
const modLabel = isMac ? '⌘' : 'Ctrl';
|
|
12
|
+
|
|
13
|
+
function isInputFocused(): boolean {
|
|
14
|
+
const el = document.activeElement;
|
|
15
|
+
if (!el) return false;
|
|
16
|
+
const tag = el.tagName;
|
|
17
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (el as HTMLElement).isContentEditable;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function showShortcutsModal() {
|
|
21
|
+
if (document.querySelector('.bd-shortcuts-overlay')) return;
|
|
22
|
+
|
|
23
|
+
const overlay = document.createElement('div');
|
|
24
|
+
overlay.className = 'bd-shortcuts-overlay';
|
|
25
|
+
overlay.addEventListener('click', (e) => {
|
|
26
|
+
if (e.target === overlay) overlay.remove();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
overlay.innerHTML = `
|
|
30
|
+
<div class="bd-shortcuts-modal">
|
|
31
|
+
<h3>Keyboard Shortcuts</h3>
|
|
32
|
+
<div class="bd-shortcut-row">
|
|
33
|
+
<span class="bd-shortcut-desc">Search</span>
|
|
34
|
+
<span class="bd-shortcut-keys"><kbd class="bd-kbd">${modLabel}</kbd><kbd class="bd-kbd">K</kbd></span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="bd-shortcut-row">
|
|
37
|
+
<span class="bd-shortcut-desc">Previous page</span>
|
|
38
|
+
<span class="bd-shortcut-keys"><kbd class="bd-kbd">←</kbd></span>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="bd-shortcut-row">
|
|
41
|
+
<span class="bd-shortcut-desc">Next page</span>
|
|
42
|
+
<span class="bd-shortcut-keys"><kbd class="bd-kbd">→</kbd></span>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="bd-shortcut-row">
|
|
45
|
+
<span class="bd-shortcut-desc">Show shortcuts</span>
|
|
46
|
+
<span class="bd-shortcut-keys"><kbd class="bd-kbd">?</kbd></span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
document.body.appendChild(overlay);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
55
|
+
// Close modal on Escape
|
|
56
|
+
if (e.key === 'Escape') {
|
|
57
|
+
const modal = document.querySelector('.bd-shortcuts-overlay');
|
|
58
|
+
if (modal) { modal.remove(); return; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Cmd/Ctrl+K → open search
|
|
62
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
const searchBtn = document.querySelector('[data-search-trigger]') as HTMLButtonElement;
|
|
65
|
+
if (searchBtn) searchBtn.click();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (isInputFocused()) return;
|
|
70
|
+
|
|
71
|
+
// ? → show shortcuts
|
|
72
|
+
if (e.key === '?' || (e.shiftKey && e.key === '/')) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
showShortcutsModal();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ← → prev page
|
|
79
|
+
if (e.key === 'ArrowLeft' && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
80
|
+
const prev = document.querySelector('nav[aria-label="Page navigation"] a:first-of-type') as HTMLAnchorElement;
|
|
81
|
+
if (prev) { prev.click(); return; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// → → next page
|
|
85
|
+
if (e.key === 'ArrowRight' && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
86
|
+
const links = document.querySelectorAll('nav[aria-label="Page navigation"] a');
|
|
87
|
+
const next = links[links.length - 1] as HTMLAnchorElement;
|
|
88
|
+
if (next && links.length > 1) { next.click(); return; }
|
|
89
|
+
if (next && links.length === 1) {
|
|
90
|
+
const parent = next.closest('.col-span-1');
|
|
91
|
+
if (parent && parent === parent.parentElement?.lastElementChild) { next.click(); }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
initKeyboardShortcuts();
|
|
98
|
+
document.addEventListener('astro:page-load', () => {
|
|
99
|
+
window.__bdShortcutsInit = false;
|
|
100
|
+
initKeyboardShortcuts();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
declare global {
|
|
104
|
+
interface Window {
|
|
105
|
+
__bdShortcutsInit?: boolean;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
@@ -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
|
+
}
|
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";
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import mediumZoom from "medium-zoom";
|
|
3
|
+
import type { Zoom } from "medium-zoom";
|
|
4
|
+
|
|
5
|
+
interface ImageZoomProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
6
|
+
zoomSrc?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ImageZoom({ zoomSrc, ...props }: ImageZoomProps) {
|
|
10
|
+
const imgRef = React.useRef<HTMLImageElement>(null);
|
|
11
|
+
const zoomRef = React.useRef<Zoom | null>(null);
|
|
12
|
+
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
if (!imgRef.current) return;
|
|
15
|
+
|
|
16
|
+
zoomRef.current = mediumZoom(imgRef.current, {
|
|
17
|
+
margin: 24,
|
|
18
|
+
background: "var(--bd-bg)",
|
|
19
|
+
scrollOffset: 0,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return () => {
|
|
23
|
+
zoomRef.current?.detach();
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<img
|
|
29
|
+
ref={imgRef}
|
|
30
|
+
data-zoom-src={zoomSrc}
|
|
31
|
+
className="bd-image-zoom"
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
|
|
4
|
+
interface VideoProps {
|
|
5
|
+
url: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
caption?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseVideoUrl(url: string): { provider: string; embedUrl: string } | null {
|
|
12
|
+
// YouTube: youtube.com/watch?v=ID, youtu.be/ID, youtube.com/embed/ID
|
|
13
|
+
const ytMatch = url.match(
|
|
14
|
+
/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
|
|
15
|
+
);
|
|
16
|
+
if (ytMatch) {
|
|
17
|
+
return {
|
|
18
|
+
provider: "youtube",
|
|
19
|
+
embedUrl: `https://www.youtube-nocookie.com/embed/${ytMatch[1]}?rel=0`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Vimeo: vimeo.com/ID, player.vimeo.com/video/ID
|
|
24
|
+
const vimeoMatch = url.match(/(?:vimeo\.com\/|player\.vimeo\.com\/video\/)(\d+)/);
|
|
25
|
+
if (vimeoMatch) {
|
|
26
|
+
return {
|
|
27
|
+
provider: "vimeo",
|
|
28
|
+
embedUrl: `https://player.vimeo.com/video/${vimeoMatch[1]}?dnt=1`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Loom: loom.com/share/ID, loom.com/embed/ID
|
|
33
|
+
const loomMatch = url.match(/loom\.com\/(?:share|embed)\/([a-f0-9]+)/);
|
|
34
|
+
if (loomMatch) {
|
|
35
|
+
return {
|
|
36
|
+
provider: "loom",
|
|
37
|
+
embedUrl: `https://www.loom.com/embed/${loomMatch[1]}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function Video({ url, title, caption, className }: VideoProps) {
|
|
45
|
+
const parsed = parseVideoUrl(url);
|
|
46
|
+
|
|
47
|
+
if (!parsed) {
|
|
48
|
+
return (
|
|
49
|
+
<div className={cn("bd-video", className)} style={{ paddingTop: 0, padding: "2rem" }}>
|
|
50
|
+
<p style={{ color: "var(--bd-text-muted)", margin: 0, textAlign: "center" }}>
|
|
51
|
+
Unsupported video URL: {url}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<figure className={className}>
|
|
59
|
+
<div className="bd-video">
|
|
60
|
+
<iframe
|
|
61
|
+
src={parsed.embedUrl}
|
|
62
|
+
title={title || `${parsed.provider} video`}
|
|
63
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
64
|
+
allowFullScreen
|
|
65
|
+
loading="lazy"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
{caption && <figcaption className="bd-video-caption">{caption}</figcaption>}
|
|
69
|
+
</figure>
|
|
70
|
+
);
|
|
71
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { ThemeExport, ResolvedBarodocConfig } from "@barodoc/core";
|
|
|
3
3
|
import mdx from "@astrojs/mdx";
|
|
4
4
|
import react from "@astrojs/react";
|
|
5
5
|
import tailwindcss from "@tailwindcss/vite";
|
|
6
|
+
import remarkMath from "remark-math";
|
|
7
|
+
import rehypeKatex from "rehype-katex";
|
|
6
8
|
|
|
7
9
|
export interface DocsThemeOptions {
|
|
8
10
|
customCss?: string[];
|
|
@@ -79,9 +81,33 @@ function createThemeIntegration(
|
|
|
79
81
|
entrypoint: "@barodoc/theme-docs/pages/docs/[...slug].astro",
|
|
80
82
|
});
|
|
81
83
|
|
|
84
|
+
// Blog routes
|
|
85
|
+
if (config?.blog?.enabled !== false) {
|
|
86
|
+
injectRoute({
|
|
87
|
+
pattern: "/blog",
|
|
88
|
+
entrypoint: "@barodoc/theme-docs/pages/blog/index.astro",
|
|
89
|
+
});
|
|
90
|
+
injectRoute({
|
|
91
|
+
pattern: "/blog/[...slug]",
|
|
92
|
+
entrypoint: "@barodoc/theme-docs/pages/blog/[...slug].astro",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Changelog route
|
|
97
|
+
injectRoute({
|
|
98
|
+
pattern: "/changelog",
|
|
99
|
+
entrypoint: "@barodoc/theme-docs/pages/changelog/index.astro",
|
|
100
|
+
});
|
|
101
|
+
|
|
82
102
|
// Update Astro config with integrations and Vite plugins
|
|
83
103
|
updateConfig({
|
|
84
|
-
integrations: [
|
|
104
|
+
integrations: [
|
|
105
|
+
mdx({
|
|
106
|
+
remarkPlugins: [remarkMath],
|
|
107
|
+
rehypePlugins: [rehypeKatex],
|
|
108
|
+
}),
|
|
109
|
+
react(),
|
|
110
|
+
],
|
|
85
111
|
vite: {
|
|
86
112
|
plugins: [tailwindcss()],
|
|
87
113
|
optimizeDeps: {
|
|
@@ -45,6 +45,9 @@ const themeCSS = config.theme?.colors ? generateThemeCSS(config.theme.colors) :
|
|
|
45
45
|
<meta name="twitter:description" content={description} />
|
|
46
46
|
{ogImageUrl && <meta name="twitter:image" content={ogImageUrl} />}
|
|
47
47
|
|
|
48
|
+
<!-- KaTeX math rendering -->
|
|
49
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css" crossorigin="anonymous" />
|
|
50
|
+
|
|
48
51
|
<title>{title}</title>
|
|
49
52
|
<ThemeScript />
|
|
50
53
|
<ClientRouter />
|