@barodoc/theme-docs 1.0.0 → 2.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 +2 -2
- package/src/components/Banner.astro +56 -0
- package/src/components/Breadcrumb.astro +32 -0
- package/src/components/CodeCopy.astro +40 -0
- package/src/components/mdx/CodeGroup.astro +35 -23
- package/src/components/mdx/CodeGroup.tsx +76 -10
- package/src/components/mdx/CodeItem.astro +13 -0
- package/src/index.ts +60 -3
- package/src/layouts/BaseLayout.astro +24 -1
- package/src/layouts/DocsLayout.astro +78 -5
- package/src/pages/docs/[...slug].astro +25 -0
- package/src/styles/global.css +44 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barodoc/theme-docs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Documentation theme for Barodoc",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"lucide-react": "^0.563.0",
|
|
31
31
|
"mermaid": "^11.12.2",
|
|
32
32
|
"tailwind-merge": "^3.4.0",
|
|
33
|
-
"@barodoc/core": "
|
|
33
|
+
"@barodoc/core": "2.0.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"astro": "^5.0.0",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from "virtual:barodoc/config";
|
|
3
|
+
|
|
4
|
+
const announcement = config.announcement;
|
|
5
|
+
const show = !!announcement?.text;
|
|
6
|
+
const dismissible = announcement?.dismissible !== false;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
{show && (
|
|
10
|
+
<div
|
|
11
|
+
id="site-banner"
|
|
12
|
+
class="banner-bar bg-primary-600 text-white text-sm text-center py-2 px-4 flex items-center justify-center gap-2 relative"
|
|
13
|
+
>
|
|
14
|
+
{announcement.link ? (
|
|
15
|
+
<a href={announcement.link} class="hover:underline font-medium">
|
|
16
|
+
{announcement.text}
|
|
17
|
+
<svg class="inline w-3.5 h-3.5 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
19
|
+
</svg>
|
|
20
|
+
</a>
|
|
21
|
+
) : (
|
|
22
|
+
<span class="font-medium">{announcement.text}</span>
|
|
23
|
+
)}
|
|
24
|
+
{dismissible && (
|
|
25
|
+
<button
|
|
26
|
+
id="banner-dismiss"
|
|
27
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-white/20 transition-colors"
|
|
28
|
+
aria-label="Dismiss banner"
|
|
29
|
+
>
|
|
30
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
31
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
32
|
+
</svg>
|
|
33
|
+
</button>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
<script>
|
|
37
|
+
function initBanner() {
|
|
38
|
+
const banner = document.getElementById('site-banner');
|
|
39
|
+
const dismiss = document.getElementById('banner-dismiss');
|
|
40
|
+
if (!banner) return;
|
|
41
|
+
const key = 'barodoc-banner-dismissed';
|
|
42
|
+
if (localStorage.getItem(key) === banner.textContent?.trim()) {
|
|
43
|
+
banner.style.display = 'none';
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (dismiss) {
|
|
47
|
+
dismiss.addEventListener('click', () => {
|
|
48
|
+
banner.style.display = 'none';
|
|
49
|
+
localStorage.setItem(key, banner.textContent?.trim() || '');
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
initBanner();
|
|
54
|
+
document.addEventListener('astro:page-load', initBanner);
|
|
55
|
+
</script>
|
|
56
|
+
)}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface BreadcrumbItem {
|
|
3
|
+
label: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
items: BreadcrumbItem[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { items } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<nav aria-label="Breadcrumb" class="mb-3">
|
|
15
|
+
<ol class="flex flex-wrap items-center gap-1 text-xs text-[var(--color-text-muted)]">
|
|
16
|
+
<li class="flex items-center gap-1">
|
|
17
|
+
<a href="/docs" class="hover:text-[var(--color-text-secondary)] transition-colors">Docs</a>
|
|
18
|
+
</li>
|
|
19
|
+
{items.map((item, i) => (
|
|
20
|
+
<li class="flex items-center gap-1">
|
|
21
|
+
<svg class="w-3 h-3 text-[var(--color-text-muted)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
22
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
23
|
+
</svg>
|
|
24
|
+
{item.href && i < items.length - 1 ? (
|
|
25
|
+
<a href={item.href} class="hover:text-[var(--color-text-secondary)] transition-colors">{item.label}</a>
|
|
26
|
+
) : (
|
|
27
|
+
<span class="text-[var(--color-text-secondary)]">{item.label}</span>
|
|
28
|
+
)}
|
|
29
|
+
</li>
|
|
30
|
+
))}
|
|
31
|
+
</ol>
|
|
32
|
+
</nav>
|
|
@@ -99,6 +99,46 @@
|
|
|
99
99
|
border-radius: 0 !important;
|
|
100
100
|
background: transparent !important;
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
/* Inside CodeGroup: no extra border/background, add padding so code has internal spacing */
|
|
104
|
+
.code-group .code-block-wrapper {
|
|
105
|
+
margin: 0;
|
|
106
|
+
border: none;
|
|
107
|
+
border-radius: 0;
|
|
108
|
+
background: transparent;
|
|
109
|
+
/* padding: 1rem 1.25rem; */
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.code-group .code-block-wrapper pre {
|
|
113
|
+
padding: 0 !important;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* pre not wrapped by CodeCopy (direct child of code-group-content) */
|
|
117
|
+
.code-group .code-group-content > pre {
|
|
118
|
+
padding: 1rem 1.25rem !important;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* CodeItem: one code block per tab; only the code area is visible, no extra wrapper space */
|
|
122
|
+
.code-group .code-item {
|
|
123
|
+
margin: 0;
|
|
124
|
+
padding: 0;
|
|
125
|
+
display: block;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.code-group .code-item:not(:first-child) {
|
|
129
|
+
display: none; /* toggled by CodeGroup script */
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* .code-group .code-item .code-block-wrapper,
|
|
133
|
+
.code-group .code-item > pre {
|
|
134
|
+
margin: 0 !important;
|
|
135
|
+
border: none !important;
|
|
136
|
+
padding: 0.25rem 0rem !important;
|
|
137
|
+
} */
|
|
138
|
+
|
|
139
|
+
/* .code-group .code-item .code-block-wrapper pre {
|
|
140
|
+
padding: 0 !important;
|
|
141
|
+
} */
|
|
102
142
|
|
|
103
143
|
.code-block-header {
|
|
104
144
|
display: flex;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
// CodeGroup component for tabbed code blocks
|
|
2
|
+
// CodeGroup component for tabbed code blocks.
|
|
3
|
+
// When using CodeItem children, each CodeItem's title is used for the tab label.
|
|
4
|
+
// Optional titles array is only used when not using CodeItem (direct pre children).
|
|
3
5
|
interface Props {
|
|
4
6
|
titles?: string[];
|
|
5
7
|
}
|
|
@@ -7,9 +9,9 @@ interface Props {
|
|
|
7
9
|
const { titles = [] } = Astro.props;
|
|
8
10
|
---
|
|
9
11
|
|
|
10
|
-
<div class="not-prose my-4
|
|
11
|
-
<div class="code-group-tabs flex border-b border-[var(--color-border)] bg-[var(--color-bg-
|
|
12
|
-
<div class="code-group-content">
|
|
12
|
+
<div class="code-group not-prose my-4 rounded-lg border border-[var(--color-border)] overflow-hidden" data-titles={JSON.stringify(titles)}>
|
|
13
|
+
<div class="code-group-tabs flex flex-wrap gap-0 border-b border-[var(--color-border)] bg-[var(--color-bg-tertiary)]"></div>
|
|
14
|
+
<div class="code-group-content bg-[var(--color-bg-secondary)]">
|
|
13
15
|
<slot />
|
|
14
16
|
</div>
|
|
15
17
|
</div>
|
|
@@ -19,40 +21,50 @@ const { titles = [] } = Astro.props;
|
|
|
19
21
|
document.querySelectorAll('.code-group').forEach((group) => {
|
|
20
22
|
const tabsContainer = group.querySelector('.code-group-tabs');
|
|
21
23
|
const contentContainer = group.querySelector('.code-group-content');
|
|
22
|
-
const
|
|
23
|
-
const codeBlocks = contentContainer?.querySelectorAll('pre');
|
|
24
|
+
const fallbackTitles = JSON.parse(group.getAttribute('data-titles') || '[]');
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
const codeItems = contentContainer?.querySelectorAll(':scope > .code-item');
|
|
27
|
+
const directPres = contentContainer?.querySelectorAll(':scope > pre');
|
|
26
28
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
+
// When CodeItem children exist: use them for tabs (titles from data-title), one tab per .code-item
|
|
30
|
+
const useCodeItems = (codeItems?.length ?? 0) > 0;
|
|
31
|
+
const tabPanels = useCodeItems
|
|
32
|
+
? Array.from(codeItems!)
|
|
33
|
+
: Array.from(directPres || []);
|
|
34
|
+
const titles = useCodeItems && codeItems?.length
|
|
35
|
+
? Array.from(codeItems!).map((el) => (el as HTMLElement).getAttribute('data-title') ?? '')
|
|
36
|
+
: fallbackTitles;
|
|
37
|
+
const numTabs = tabPanels.length;
|
|
38
|
+
|
|
39
|
+
if (!tabsContainer || !contentContainer || numTabs === 0) return;
|
|
40
|
+
|
|
41
|
+
// Create tabs from titles; show/hide tabPanels
|
|
42
|
+
tabPanels.forEach((_panel, index) => {
|
|
29
43
|
const tab = document.createElement('button');
|
|
30
|
-
tab.className = 'px-4 py-2 text-sm font-medium transition-colors text-[var(--color-text-
|
|
31
|
-
tab.textContent = titles[index] || `Tab ${index + 1}`;
|
|
44
|
+
tab.className = 'shrink-0 px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap text-[var(--color-text-muted)] hover:text-[var(--color-text)]';
|
|
45
|
+
tab.textContent = titles[index]?.trim() || `Tab ${index + 1}`;
|
|
46
|
+
const panel = tabPanels[index] as HTMLElement;
|
|
32
47
|
tab.addEventListener('click', () => {
|
|
33
|
-
// Update tabs
|
|
34
48
|
tabsContainer.querySelectorAll('button').forEach((t, i) => {
|
|
35
49
|
if (i === index) {
|
|
36
|
-
t.classList.add('border-b-2', 'border-primary-
|
|
37
|
-
t.classList.remove('text-[var(--color-text-
|
|
50
|
+
t.classList.add('border-b-2', 'border-primary-500', 'text-[var(--color-text)]', '-mb-px');
|
|
51
|
+
t.classList.remove('text-[var(--color-text-muted)]');
|
|
38
52
|
} else {
|
|
39
|
-
t.classList.remove('border-b-2', 'border-primary-
|
|
40
|
-
t.classList.add('text-[var(--color-text-
|
|
53
|
+
t.classList.remove('border-b-2', 'border-primary-500', 'text-[var(--color-text)]', '-mb-px');
|
|
54
|
+
t.classList.add('text-[var(--color-text-muted)]');
|
|
41
55
|
}
|
|
42
56
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
(b as HTMLElement).style.display = i === index ? 'block' : 'none';
|
|
57
|
+
tabPanels.forEach((p, i) => {
|
|
58
|
+
(p as HTMLElement).style.display = i === index ? 'block' : 'none';
|
|
46
59
|
});
|
|
47
60
|
});
|
|
48
61
|
tabsContainer.appendChild(tab);
|
|
49
62
|
|
|
50
|
-
// Hide all but first
|
|
51
63
|
if (index === 0) {
|
|
52
|
-
tab.classList.add('border-b-2', 'border-primary-
|
|
53
|
-
tab.classList.remove('text-[var(--color-text-
|
|
64
|
+
tab.classList.add('border-b-2', 'border-primary-500', 'text-[var(--color-text)]', '-mb-px');
|
|
65
|
+
tab.classList.remove('text-[var(--color-text-muted)]');
|
|
54
66
|
} else {
|
|
55
|
-
|
|
67
|
+
panel.style.display = 'none';
|
|
56
68
|
}
|
|
57
69
|
});
|
|
58
70
|
});
|
|
@@ -5,25 +5,91 @@ interface CodeGroupProps {
|
|
|
5
5
|
titles?: string[];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
function isCodeBlockLike(el: React.ReactElement): boolean {
|
|
9
|
+
if (el.type === "pre") return true;
|
|
10
|
+
const className =
|
|
11
|
+
typeof el.props?.className === "string" ? el.props.className : "";
|
|
12
|
+
if (
|
|
13
|
+
className.includes("language-") ||
|
|
14
|
+
className.includes("astro-code") ||
|
|
15
|
+
className.includes("code-block")
|
|
16
|
+
)
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectCodeBlocks(node: React.ReactNode): React.ReactElement[] {
|
|
22
|
+
const result: React.ReactElement[] = [];
|
|
23
|
+
React.Children.forEach(node, (child) => {
|
|
24
|
+
if (!React.isValidElement(child)) return;
|
|
25
|
+
if (child.type === React.Fragment && child.props?.children != null) {
|
|
26
|
+
result.push(...collectCodeBlocks(child.props.children));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (child.type === "pre" || isCodeBlockLike(child)) {
|
|
30
|
+
result.push(child);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (child.props?.children != null) {
|
|
34
|
+
result.push(...collectCodeBlocks(child.props.children));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
8
40
|
export function CodeGroup({ children, titles = [] }: CodeGroupProps) {
|
|
9
41
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
|
|
43
|
+
const codeBlocks = React.useMemo(() => {
|
|
44
|
+
let blocks = collectCodeBlocks(children);
|
|
45
|
+
if (blocks.length > 0) return blocks;
|
|
46
|
+
const direct = React.Children.toArray(children).filter(
|
|
47
|
+
(c): c is React.ReactElement => React.isValidElement(c) && c.type != null
|
|
48
|
+
);
|
|
49
|
+
if (direct.length > 1) return direct;
|
|
50
|
+
if (
|
|
51
|
+
direct.length === 1 &&
|
|
52
|
+
direct[0].props?.children != null
|
|
53
|
+
) {
|
|
54
|
+
const inner = React.Children.toArray(direct[0].props.children).filter(
|
|
55
|
+
(c): c is React.ReactElement =>
|
|
56
|
+
React.isValidElement(c) && c.type != null
|
|
57
|
+
);
|
|
58
|
+
if (inner.length > 0) return inner;
|
|
59
|
+
}
|
|
60
|
+
return direct;
|
|
61
|
+
}, [children]);
|
|
62
|
+
|
|
63
|
+
const tabTitles =
|
|
64
|
+
titles.length >= codeBlocks.length
|
|
65
|
+
? titles.slice(0, codeBlocks.length)
|
|
66
|
+
: [
|
|
67
|
+
...titles,
|
|
68
|
+
...codeBlocks
|
|
69
|
+
.slice(titles.length)
|
|
70
|
+
.map((_, i) => `Tab ${titles.length + i + 1}`),
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
if (codeBlocks.length === 0) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="code-group not-prose my-4 rounded-lg border border-[var(--color-border)] overflow-hidden p-4 text-[var(--color-text-muted)] text-sm">
|
|
76
|
+
No code blocks found inside CodeGroup.
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
14
81
|
return (
|
|
15
|
-
<div className="not-prose my-4 rounded-lg border border-[var(--color-border)] overflow-hidden">
|
|
82
|
+
<div className="code-group not-prose my-4 rounded-lg border border-[var(--color-border)] overflow-hidden">
|
|
16
83
|
{/* Tabs */}
|
|
17
|
-
<div className="flex bg-[var(--color-bg-tertiary)] border-b border-[var(--color-border)]">
|
|
18
|
-
{
|
|
84
|
+
<div className="flex flex-wrap gap-0 bg-[var(--color-bg-tertiary)] border-b border-[var(--color-border)]">
|
|
85
|
+
{tabTitles.map((title, index) => {
|
|
19
86
|
const isActive = activeIndex === index;
|
|
20
|
-
const title = titles[index] || `Tab ${index + 1}`;
|
|
21
87
|
return (
|
|
22
88
|
<button
|
|
23
89
|
key={index}
|
|
24
90
|
type="button"
|
|
25
91
|
onClick={() => setActiveIndex(index)}
|
|
26
|
-
className={`px-4 py-2 text-sm font-medium transition-colors ${
|
|
92
|
+
className={`shrink-0 px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
|
|
27
93
|
isActive
|
|
28
94
|
? "bg-[var(--color-bg-secondary)] text-[var(--color-text)] border-b-2 border-primary-500 -mb-px"
|
|
29
95
|
: "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
|
@@ -34,7 +100,7 @@ export function CodeGroup({ children, titles = [] }: CodeGroupProps) {
|
|
|
34
100
|
);
|
|
35
101
|
})}
|
|
36
102
|
</div>
|
|
37
|
-
|
|
103
|
+
|
|
38
104
|
{/* Content */}
|
|
39
105
|
<div className="bg-[var(--color-bg-secondary)]">
|
|
40
106
|
{codeBlocks.map((block, index) => (
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Wraps a single code block inside CodeGroup. Use one CodeItem per tab.
|
|
3
|
+
// Put one fenced code block (```lang ... ```) inside; MDX/Shiki handle syntax highlighting.
|
|
4
|
+
interface Props {
|
|
5
|
+
title?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { title = "" } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="code-item" data-title={title}>
|
|
12
|
+
<slot />
|
|
13
|
+
</div>
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
|
-
import type { ThemeExport } from "@barodoc/core";
|
|
2
|
+
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";
|
|
@@ -8,7 +8,60 @@ export interface DocsThemeOptions {
|
|
|
8
8
|
customCss?: string[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/** HAST node with optional children and properties */
|
|
12
|
+
interface HastNode {
|
|
13
|
+
type: string;
|
|
14
|
+
children?: HastNode[];
|
|
15
|
+
properties?: Record<string, unknown>;
|
|
16
|
+
tagName?: string;
|
|
17
|
+
value?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getTextContent(node: HastNode): string {
|
|
21
|
+
if (node.type === "text") return node.value ?? "";
|
|
22
|
+
if (node.children?.length) return node.children.map(getTextContent).join("");
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isEmptyLine(node: HastNode): boolean {
|
|
27
|
+
if (node.type !== "element" || node.tagName !== "span") return false;
|
|
28
|
+
const cls = node.properties?.className;
|
|
29
|
+
const isLine =
|
|
30
|
+
Array.isArray(cls) && cls.some((c) => c === "line" || (typeof c === "string" && c.includes("line")));
|
|
31
|
+
if (!isLine) return false;
|
|
32
|
+
return /^\s*$/.test(getTextContent(node));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Removes leading and trailing empty span.line so only the code area is visible. */
|
|
36
|
+
function createTrimEmptyLinesTransformer() {
|
|
37
|
+
return {
|
|
38
|
+
name: "barodoc-trim-empty-lines",
|
|
39
|
+
code(node: HastNode) {
|
|
40
|
+
const lines = node.children;
|
|
41
|
+
if (!lines?.length) return;
|
|
42
|
+
let start = 0;
|
|
43
|
+
let end = lines.length;
|
|
44
|
+
while (start < end && isEmptyLine(lines[start])) start++;
|
|
45
|
+
while (end > start && isEmptyLine(lines[end - 1])) end--;
|
|
46
|
+
node.children = lines.slice(start, end);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createLineNumbersTransformer() {
|
|
52
|
+
return {
|
|
53
|
+
name: "barodoc-line-numbers",
|
|
54
|
+
pre(node: { properties?: Record<string, unknown> }) {
|
|
55
|
+
(this as { addClassToHast: (node: unknown, cls: string) => void }).addClassToHast(node, "line-numbers");
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createThemeIntegration(
|
|
61
|
+
config: ResolvedBarodocConfig,
|
|
62
|
+
options?: DocsThemeOptions
|
|
63
|
+
): AstroIntegration {
|
|
64
|
+
const lineNumbers = config?.lineNumbers === true;
|
|
12
65
|
return {
|
|
13
66
|
name: "@barodoc/theme-docs",
|
|
14
67
|
hooks: {
|
|
@@ -41,6 +94,10 @@ function createThemeIntegration(options?: DocsThemeOptions): AstroIntegration {
|
|
|
41
94
|
light: "github-light",
|
|
42
95
|
dark: "github-dark",
|
|
43
96
|
},
|
|
97
|
+
transformers: [
|
|
98
|
+
createTrimEmptyLinesTransformer(),
|
|
99
|
+
...(lineNumbers ? [createLineNumbersTransformer()] : []),
|
|
100
|
+
],
|
|
44
101
|
},
|
|
45
102
|
},
|
|
46
103
|
});
|
|
@@ -54,7 +111,7 @@ function createThemeIntegration(options?: DocsThemeOptions): AstroIntegration {
|
|
|
54
111
|
export default function docsTheme(options?: DocsThemeOptions): ThemeExport {
|
|
55
112
|
return {
|
|
56
113
|
name: "@barodoc/theme-docs",
|
|
57
|
-
integration: () => createThemeIntegration(options),
|
|
114
|
+
integration: (config) => createThemeIntegration(config, options),
|
|
58
115
|
styles: options?.customCss || [],
|
|
59
116
|
};
|
|
60
117
|
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
import ThemeScript from "../components/ThemeScript.astro";
|
|
3
3
|
import { SearchDialog } from "../components/SearchDialog";
|
|
4
|
+
import config from "virtual:barodoc/config";
|
|
4
5
|
import "@barodoc/theme-docs/styles/global.css";
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
title: string;
|
|
8
9
|
description?: string;
|
|
10
|
+
ogImage?: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
const { title, description = "Documentation" } = Astro.props;
|
|
13
|
+
const { title, description = "Documentation", ogImage } = Astro.props;
|
|
14
|
+
const siteName = config.name;
|
|
15
|
+
const canonicalUrl = config.site ? new URL(Astro.url.pathname, config.site).href : Astro.url.href;
|
|
16
|
+
const ogImageUrl = ogImage
|
|
17
|
+
? (config.site ? new URL(ogImage, config.site).href : ogImage)
|
|
18
|
+
: undefined;
|
|
12
19
|
---
|
|
13
20
|
|
|
14
21
|
<!doctype html>
|
|
@@ -18,6 +25,22 @@ const { title, description = "Documentation" } = Astro.props;
|
|
|
18
25
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
19
26
|
<meta name="description" content={description} />
|
|
20
27
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
28
|
+
<link rel="canonical" href={canonicalUrl} />
|
|
29
|
+
|
|
30
|
+
<!-- Open Graph -->
|
|
31
|
+
<meta property="og:type" content="article" />
|
|
32
|
+
<meta property="og:title" content={title} />
|
|
33
|
+
<meta property="og:description" content={description} />
|
|
34
|
+
<meta property="og:site_name" content={siteName} />
|
|
35
|
+
<meta property="og:url" content={canonicalUrl} />
|
|
36
|
+
{ogImageUrl && <meta property="og:image" content={ogImageUrl} />}
|
|
37
|
+
|
|
38
|
+
<!-- Twitter Card -->
|
|
39
|
+
<meta name="twitter:card" content={ogImageUrl ? "summary_large_image" : "summary"} />
|
|
40
|
+
<meta name="twitter:title" content={title} />
|
|
41
|
+
<meta name="twitter:description" content={description} />
|
|
42
|
+
{ogImageUrl && <meta name="twitter:image" content={ogImageUrl} />}
|
|
43
|
+
|
|
21
44
|
<title>{title}</title>
|
|
22
45
|
<ThemeScript />
|
|
23
46
|
</head>
|
|
@@ -5,31 +5,46 @@ import Sidebar from "../components/Sidebar.astro";
|
|
|
5
5
|
import TableOfContents from "../components/TableOfContents.astro";
|
|
6
6
|
import MobileNav from "../components/MobileNav.astro";
|
|
7
7
|
import CodeCopy from "../components/CodeCopy.astro";
|
|
8
|
+
import Breadcrumb from "../components/Breadcrumb.astro";
|
|
9
|
+
import Banner from "../components/Banner.astro";
|
|
8
10
|
import { defaultLocale } from "virtual:barodoc/i18n";
|
|
9
11
|
import { getLocaleFromPath } from "@barodoc/core";
|
|
12
|
+
import config from "virtual:barodoc/config";
|
|
10
13
|
|
|
11
14
|
interface PageLink {
|
|
12
15
|
title: string;
|
|
13
16
|
href: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
interface BreadcrumbItem {
|
|
20
|
+
label: string;
|
|
21
|
+
href?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
interface Props {
|
|
17
25
|
title: string;
|
|
18
26
|
description?: string;
|
|
19
27
|
headings?: { depth: number; slug: string; text: string }[];
|
|
20
28
|
prevPage?: PageLink | null;
|
|
21
29
|
nextPage?: PageLink | null;
|
|
30
|
+
editUrl?: string | null;
|
|
31
|
+
lastUpdated?: Date | null;
|
|
32
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
const { title, description, headings = [], prevPage, nextPage } = Astro.props;
|
|
35
|
+
const { title, description, headings = [], prevPage, nextPage, editUrl, lastUpdated, breadcrumbs = [] } = Astro.props;
|
|
25
36
|
const currentPath = Astro.url.pathname;
|
|
26
37
|
|
|
27
38
|
// Get locale from path
|
|
28
39
|
const i18nConfig = { defaultLocale, locales: [defaultLocale] };
|
|
29
40
|
const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
|
|
41
|
+
|
|
42
|
+
const hasFeedback = config.feedback?.enabled === true;
|
|
43
|
+
const feedbackEndpoint = config.feedback?.endpoint;
|
|
30
44
|
---
|
|
31
45
|
|
|
32
46
|
<BaseLayout title={title} description={description}>
|
|
47
|
+
<Banner />
|
|
33
48
|
<Header currentLocale={currentLocale} currentPath={currentPath} />
|
|
34
49
|
|
|
35
50
|
<!-- Centered container for docs layout -->
|
|
@@ -45,6 +60,7 @@ const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
|
|
|
45
60
|
<!-- Main Content: no min-width on mobile to avoid horizontal scroll -->
|
|
46
61
|
<main class="flex-1 min-w-0 lg:min-w-[650px] max-w-[720px]">
|
|
47
62
|
<div class="px-4 py-6 sm:px-6 sm:py-8 lg:px-8 lg:py-8">
|
|
63
|
+
{breadcrumbs.length > 0 && <Breadcrumb items={breadcrumbs} />}
|
|
48
64
|
<article class="prose prose-gray dark:prose-invert max-w-none min-w-0 overflow-x-auto">
|
|
49
65
|
<slot />
|
|
50
66
|
</article>
|
|
@@ -94,10 +110,41 @@ const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
|
|
|
94
110
|
)}
|
|
95
111
|
|
|
96
112
|
<!-- Page footer -->
|
|
97
|
-
<footer class="mt-6 pt-4 border-t border-[var(--color-border-light)]">
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
<footer class="mt-6 pt-4 border-t border-[var(--color-border-light)] flex flex-col gap-3">
|
|
114
|
+
<div class="flex flex-wrap items-center justify-between gap-2 text-sm text-[var(--color-text-muted)]">
|
|
115
|
+
{lastUpdated && (
|
|
116
|
+
<span>
|
|
117
|
+
Last updated: <time datetime={lastUpdated.toISOString()}>
|
|
118
|
+
{lastUpdated.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })}
|
|
119
|
+
</time>
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
{editUrl && (
|
|
123
|
+
<a
|
|
124
|
+
href={editUrl}
|
|
125
|
+
target="_blank"
|
|
126
|
+
rel="noopener noreferrer"
|
|
127
|
+
class="inline-flex items-center gap-1 text-primary-600 dark:text-primary-400 hover:underline"
|
|
128
|
+
>
|
|
129
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
130
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
131
|
+
</svg>
|
|
132
|
+
Edit this page
|
|
133
|
+
</a>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
{hasFeedback && (
|
|
137
|
+
<div class="doc-feedback flex items-center gap-3 text-sm text-[var(--color-text-muted)]" data-endpoint={feedbackEndpoint}>
|
|
138
|
+
<span>Was this page helpful?</span>
|
|
139
|
+
<button type="button" class="feedback-btn inline-flex items-center gap-1 px-2 py-1 rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)] transition-colors" data-value="yes" aria-label="Yes">
|
|
140
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 9V5a3 3 0 00-3-3l-4 9v11h11.28a2 2 0 002-1.7l1.38-9a2 2 0 00-2-2.3H14z" /></svg>
|
|
141
|
+
</button>
|
|
142
|
+
<button type="button" class="feedback-btn inline-flex items-center gap-1 px-2 py-1 rounded border border-[var(--color-border)] hover:bg-[var(--color-bg-secondary)] transition-colors" data-value="no" aria-label="No">
|
|
143
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 15v3.586a1 1 0 01-.293.707l-2 2A1 1 0 016 20.586V15m4 0h5.17a2 2 0 001.98-1.742l1.08-7.56A1 1 0 0017.242 4.5H10m0 10.5V4.5m0 0H7.5A2.5 2.5 0 005 7v3" /></svg>
|
|
144
|
+
</button>
|
|
145
|
+
<span class="feedback-thanks hidden text-green-600 dark:text-green-400">Thanks for your feedback!</span>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
101
148
|
</footer>
|
|
102
149
|
</div>
|
|
103
150
|
</main>
|
|
@@ -118,4 +165,30 @@ const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
|
|
|
118
165
|
|
|
119
166
|
<!-- Code copy functionality -->
|
|
120
167
|
<CodeCopy />
|
|
168
|
+
|
|
169
|
+
{hasFeedback && (
|
|
170
|
+
<script>
|
|
171
|
+
function initFeedback() {
|
|
172
|
+
document.querySelectorAll('.doc-feedback').forEach((el) => {
|
|
173
|
+
if (el.hasAttribute('data-init')) return;
|
|
174
|
+
el.setAttribute('data-init', 'true');
|
|
175
|
+
const endpoint = el.getAttribute('data-endpoint');
|
|
176
|
+
const thanks = el.querySelector('.feedback-thanks');
|
|
177
|
+
el.querySelectorAll('.feedback-btn').forEach((btn) => {
|
|
178
|
+
btn.addEventListener('click', async () => {
|
|
179
|
+
const value = btn.getAttribute('data-value');
|
|
180
|
+
const page = window.location.pathname;
|
|
181
|
+
el.querySelectorAll('.feedback-btn').forEach((b: any) => b.disabled = true);
|
|
182
|
+
if (thanks) thanks.classList.remove('hidden');
|
|
183
|
+
if (endpoint) {
|
|
184
|
+
try { await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ page, value }) }); } catch {}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
initFeedback();
|
|
191
|
+
document.addEventListener('astro:page-load', initFeedback);
|
|
192
|
+
</script>
|
|
193
|
+
)}
|
|
121
194
|
</BaseLayout>
|
|
@@ -89,6 +89,28 @@ const nextPage = currentIndex < allPages.length - 1
|
|
|
89
89
|
: null;
|
|
90
90
|
|
|
91
91
|
const category = findCategory(cleanSlug);
|
|
92
|
+
|
|
93
|
+
// Edit link
|
|
94
|
+
const editBaseUrl = config.editLink?.baseUrl;
|
|
95
|
+
const editUrl = editBaseUrl
|
|
96
|
+
? `${editBaseUrl.replace(/\/$/, "")}/${doc.id}`
|
|
97
|
+
: null;
|
|
98
|
+
|
|
99
|
+
// Last updated: use file modification if available
|
|
100
|
+
let lastUpdated: Date | null = null;
|
|
101
|
+
if (config.lastUpdated) {
|
|
102
|
+
const mod = (doc.data as Record<string, unknown>).lastUpdated;
|
|
103
|
+
if (mod instanceof Date) {
|
|
104
|
+
lastUpdated = mod;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Breadcrumbs: [Group, Page Title]
|
|
109
|
+
const breadcrumbs: { label: string; href?: string }[] = [];
|
|
110
|
+
if (category) {
|
|
111
|
+
breadcrumbs.push({ label: category });
|
|
112
|
+
}
|
|
113
|
+
breadcrumbs.push({ label: doc.data.title });
|
|
92
114
|
---
|
|
93
115
|
|
|
94
116
|
<DocsLayout
|
|
@@ -97,6 +119,9 @@ const category = findCategory(cleanSlug);
|
|
|
97
119
|
headings={headings}
|
|
98
120
|
prevPage={prevPage}
|
|
99
121
|
nextPage={nextPage}
|
|
122
|
+
editUrl={editUrl}
|
|
123
|
+
lastUpdated={lastUpdated}
|
|
124
|
+
breadcrumbs={breadcrumbs}
|
|
100
125
|
>
|
|
101
126
|
{category && (
|
|
102
127
|
<p class="text-xs font-medium uppercase tracking-wider text-primary-600 dark:text-primary-400 mb-2">
|
package/src/styles/global.css
CHANGED
|
@@ -77,29 +77,29 @@
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/* Prose styling for MDX content -
|
|
80
|
+
/* Prose styling for MDX content - */
|
|
81
81
|
.prose {
|
|
82
82
|
--tw-prose-body: var(--color-text);
|
|
83
83
|
--tw-prose-headings: var(--color-text);
|
|
84
84
|
--tw-prose-links: var(--color-primary-600);
|
|
85
85
|
--tw-prose-code: var(--color-text);
|
|
86
86
|
--tw-prose-pre-bg: var(--color-bg-secondary);
|
|
87
|
-
font-size: 0.875rem !important;
|
|
88
|
-
line-height: 1.6 !important;
|
|
87
|
+
/* font-size: 0.875rem !important; */
|
|
88
|
+
/* line-height: 1.6 !important; */
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
.dark .prose {
|
|
92
92
|
--tw-prose-links: var(--color-primary-400);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
/* Headings -
|
|
95
|
+
/* Headings - */
|
|
96
96
|
.prose :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
|
|
97
97
|
font-size: 1.75rem !important;
|
|
98
98
|
font-weight: 700 !important;
|
|
99
99
|
letter-spacing: -0.025em !important;
|
|
100
100
|
margin-top: 0 !important;
|
|
101
101
|
margin-bottom: 0.375rem !important;
|
|
102
|
-
line-height: 1.25 !important;
|
|
102
|
+
/* line-height: 1.25 !important; */
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
.prose :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
|
|
@@ -143,14 +143,14 @@
|
|
|
143
143
|
text-underline-offset: 2px !important;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
/* Code block styling -
|
|
146
|
+
/* Code block styling - */
|
|
147
147
|
.prose :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
|
|
148
148
|
background-color: var(--color-bg) !important;
|
|
149
149
|
border: 1px solid var(--color-border) !important;
|
|
150
150
|
border-radius: 0.5rem !important;
|
|
151
151
|
padding: 0.75rem 1rem !important;
|
|
152
152
|
font-size: 0.8125rem !important;
|
|
153
|
-
line-height: 1.
|
|
153
|
+
line-height: 1.45 !important;
|
|
154
154
|
overflow-x: auto !important;
|
|
155
155
|
margin: 0.75rem 0 !important;
|
|
156
156
|
}
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
color: var(--color-text-muted) !important;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
/* Blockquotes -
|
|
188
|
+
/* Blockquotes - */
|
|
189
189
|
.prose :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
|
|
190
190
|
border-left: 3px solid var(--color-primary-500) !important;
|
|
191
191
|
background-color: var(--color-bg-secondary) !important;
|
|
@@ -241,6 +241,20 @@
|
|
|
241
241
|
.astro-code {
|
|
242
242
|
background-color: var(--color-bg) !important;
|
|
243
243
|
border-radius: 0.5rem;
|
|
244
|
+
line-height: 1.45;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.astro-code code {
|
|
248
|
+
display: flex;
|
|
249
|
+
flex-direction: column;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Tighter line spacing: Shiki outputs each line as span.line */
|
|
253
|
+
.astro-code span.line {
|
|
254
|
+
display: block;
|
|
255
|
+
line-height: 1.45;
|
|
256
|
+
margin: 0;
|
|
257
|
+
padding: 0;
|
|
244
258
|
}
|
|
245
259
|
|
|
246
260
|
.dark .astro-code,
|
|
@@ -249,6 +263,28 @@
|
|
|
249
263
|
background-color: var(--shiki-dark-bg) !important;
|
|
250
264
|
}
|
|
251
265
|
|
|
266
|
+
/* Line numbers: applied when barodoc.config.json has "lineNumbers": true */
|
|
267
|
+
.astro-code.line-numbers {
|
|
268
|
+
counter-reset: line;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.astro-code.line-numbers span.line {
|
|
272
|
+
display: block;
|
|
273
|
+
counter-increment: line;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.astro-code.line-numbers span.line::before {
|
|
277
|
+
content: counter(line);
|
|
278
|
+
display: inline-block;
|
|
279
|
+
min-width: 2.5rem;
|
|
280
|
+
margin-right: 1rem;
|
|
281
|
+
padding-right: 0.5rem;
|
|
282
|
+
text-align: right;
|
|
283
|
+
color: var(--color-text-muted);
|
|
284
|
+
user-select: none;
|
|
285
|
+
border-right: 1px solid var(--color-border);
|
|
286
|
+
}
|
|
287
|
+
|
|
252
288
|
/* Pagefind search styling */
|
|
253
289
|
.pagefind-ui {
|
|
254
290
|
--pagefind-ui-scale: 1;
|