@barodoc/theme-docs 0.0.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/package.json +2 -2
- package/src/components/CodeCopy.astro +40 -0
- package/src/components/DocHeader.tsx +100 -10
- package/src/components/Header.astro +5 -9
- package/src/components/LanguageSwitcher.astro +12 -10
- package/src/components/Sidebar.astro +3 -7
- 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 -14
- package/src/layouts/DocsLayout.astro +7 -7
- package/src/pages/404.astro +32 -0
- package/src/pages/docs/[...slug].astro +3 -6
- package/src/styles/global.css +45 -8
package/LICENSE
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barodoc/theme-docs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
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": "1.0.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"astro": "^5.0.0",
|
|
@@ -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,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { Search, Menu, Moon, Sun, Github } from "lucide-react";
|
|
2
|
+
import { Search, Menu, Moon, Sun, Github, Globe, ChevronDown } from "lucide-react";
|
|
3
3
|
import { Button } from "./ui/button";
|
|
4
4
|
import { Separator } from "./ui/separator";
|
|
5
5
|
import {
|
|
@@ -17,6 +17,30 @@ interface DocHeaderProps {
|
|
|
17
17
|
hasMultipleLocales?: boolean;
|
|
18
18
|
currentLocale?: string;
|
|
19
19
|
localeLabels?: Record<string, string>;
|
|
20
|
+
currentPath?: string;
|
|
21
|
+
locales?: string[];
|
|
22
|
+
defaultLocale?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getLocalizedUrl(
|
|
26
|
+
path: string,
|
|
27
|
+
locale: string,
|
|
28
|
+
defaultLocale: string,
|
|
29
|
+
): string {
|
|
30
|
+
const docsPrefix = "/docs/";
|
|
31
|
+
const koPrefix = "/docs/ko/";
|
|
32
|
+
|
|
33
|
+
if (path.startsWith(koPrefix)) {
|
|
34
|
+
path = path === "/docs/ko" ? "/docs" : docsPrefix + path.slice(koPrefix.length);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (locale === defaultLocale) {
|
|
38
|
+
return path || "/";
|
|
39
|
+
}
|
|
40
|
+
if (path === "/" || !path.startsWith(docsPrefix)) {
|
|
41
|
+
return path === "/" ? "/docs/ko/introduction" : path;
|
|
42
|
+
}
|
|
43
|
+
return docsPrefix + "ko/" + path.slice(docsPrefix.length);
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
export function DocHeader({
|
|
@@ -24,10 +48,25 @@ export function DocHeader({
|
|
|
24
48
|
logo,
|
|
25
49
|
githubUrl,
|
|
26
50
|
hasMultipleLocales,
|
|
27
|
-
currentLocale,
|
|
28
|
-
localeLabels,
|
|
51
|
+
currentLocale = "en",
|
|
52
|
+
localeLabels = {},
|
|
53
|
+
currentPath = "",
|
|
54
|
+
locales = [],
|
|
55
|
+
defaultLocale = "en",
|
|
29
56
|
}: DocHeaderProps) {
|
|
30
57
|
const [theme, setTheme] = React.useState<"light" | "dark">("light");
|
|
58
|
+
const [langOpen, setLangOpen] = React.useState(false);
|
|
59
|
+
const langRef = React.useRef<HTMLDivElement>(null);
|
|
60
|
+
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
const close = (e: MouseEvent) => {
|
|
63
|
+
if (langRef.current && !langRef.current.contains(e.target as Node)) {
|
|
64
|
+
setLangOpen(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
document.addEventListener("click", close);
|
|
68
|
+
return () => document.removeEventListener("click", close);
|
|
69
|
+
}, []);
|
|
31
70
|
|
|
32
71
|
React.useEffect(() => {
|
|
33
72
|
const isDark = document.documentElement.classList.contains("dark");
|
|
@@ -51,21 +90,21 @@ export function DocHeader({
|
|
|
51
90
|
|
|
52
91
|
return (
|
|
53
92
|
<TooltipProvider>
|
|
54
|
-
<header className="sticky top-0 z-50 w-full border-b border-[var(--color-border)] bg-[var(--color-bg)]/95 backdrop-blur-md supports-[backdrop-filter]:bg-[var(--color-bg)]/80">
|
|
55
|
-
<div className="flex h-14 items-center justify-between px-4 max-w-[1120px] mx-auto">
|
|
93
|
+
<header className="sticky top-0 z-50 w-full min-w-0 border-b border-[var(--color-border)] bg-[var(--color-bg)]/95 backdrop-blur-md supports-[backdrop-filter]:bg-[var(--color-bg)]/80">
|
|
94
|
+
<div className="flex h-14 items-center justify-between gap-2 px-3 sm:px-4 max-w-[1120px] mx-auto min-w-0">
|
|
56
95
|
{/* Logo */}
|
|
57
|
-
<div className="flex items-center gap-6">
|
|
96
|
+
<div className="flex items-center gap-6 min-w-0 shrink">
|
|
58
97
|
<a
|
|
59
98
|
href="/"
|
|
60
|
-
className="flex items-center gap-2
|
|
99
|
+
className="flex items-center gap-2 min-w-0 shrink overflow-hidden font-semibold text-[var(--color-text)] hover:opacity-80 transition-opacity"
|
|
61
100
|
>
|
|
62
|
-
{logo && <img src={logo} alt={siteName} className="h-7 w-7" />}
|
|
63
|
-
<span className="text-lg">{siteName}</span>
|
|
101
|
+
{logo && <img src={logo} alt={siteName} className="h-7 w-7 shrink-0" />}
|
|
102
|
+
<span className="text-lg truncate">{siteName}</span>
|
|
64
103
|
</a>
|
|
65
104
|
</div>
|
|
66
105
|
|
|
67
106
|
{/* Right side actions */}
|
|
68
|
-
<div className="flex items-center gap-1">
|
|
107
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
69
108
|
{/* Search button */}
|
|
70
109
|
<Button
|
|
71
110
|
variant="outline"
|
|
@@ -124,6 +163,57 @@ export function DocHeader({
|
|
|
124
163
|
</Tooltip>
|
|
125
164
|
)}
|
|
126
165
|
|
|
166
|
+
{/* Language switcher */}
|
|
167
|
+
{hasMultipleLocales && locales.length > 0 && (
|
|
168
|
+
<>
|
|
169
|
+
<Separator orientation="vertical" className="hidden md:block h-6 mx-2" />
|
|
170
|
+
<div className="relative" ref={langRef}>
|
|
171
|
+
<Tooltip>
|
|
172
|
+
<TooltipTrigger asChild>
|
|
173
|
+
<Button
|
|
174
|
+
variant="ghost"
|
|
175
|
+
className="rounded-xl gap-1 px-2"
|
|
176
|
+
onClick={(e) => {
|
|
177
|
+
e.stopPropagation();
|
|
178
|
+
setLangOpen((o) => !o);
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<Globe className="h-4 w-4" />
|
|
182
|
+
<span className="text-sm hidden sm:inline">
|
|
183
|
+
{localeLabels[currentLocale] ?? currentLocale}
|
|
184
|
+
</span>
|
|
185
|
+
<ChevronDown className="h-3 w-3" />
|
|
186
|
+
<span className="sr-only">Language</span>
|
|
187
|
+
</Button>
|
|
188
|
+
</TooltipTrigger>
|
|
189
|
+
<TooltipContent>Language</TooltipContent>
|
|
190
|
+
</Tooltip>
|
|
191
|
+
{langOpen && (
|
|
192
|
+
<div
|
|
193
|
+
className="absolute right-0 mt-1 py-1 min-w-[8rem] rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] shadow-lg z-50"
|
|
194
|
+
role="menu"
|
|
195
|
+
>
|
|
196
|
+
{locales.map((locale) => (
|
|
197
|
+
<a
|
|
198
|
+
key={locale}
|
|
199
|
+
href={getLocalizedUrl(currentPath, locale, defaultLocale)}
|
|
200
|
+
className={cn(
|
|
201
|
+
"block px-3 py-2 text-sm transition-colors",
|
|
202
|
+
locale === currentLocale
|
|
203
|
+
? "bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300"
|
|
204
|
+
: "text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)]"
|
|
205
|
+
)}
|
|
206
|
+
role="menuitem"
|
|
207
|
+
>
|
|
208
|
+
{localeLabels[locale] ?? locale}
|
|
209
|
+
</a>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
|
|
127
217
|
{/* Theme toggle */}
|
|
128
218
|
<Tooltip>
|
|
129
219
|
<TooltipTrigger asChild>
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { DocHeader } from "./DocHeader";
|
|
3
|
-
import LanguageSwitcher from "./LanguageSwitcher.astro";
|
|
4
3
|
import config from "virtual:barodoc/config";
|
|
5
4
|
import { locales, defaultLocale } from "virtual:barodoc/i18n";
|
|
6
5
|
|
|
7
6
|
interface Props {
|
|
8
7
|
currentLocale?: string;
|
|
8
|
+
currentPath?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const { currentLocale = defaultLocale } = Astro.props;
|
|
11
|
+
const { currentLocale = defaultLocale, currentPath = "" } = Astro.props;
|
|
12
12
|
const hasMultipleLocales = locales.length > 1;
|
|
13
13
|
|
|
14
|
-
// Build locale labels from config
|
|
15
14
|
const localeLabels: Record<string, string> = config.i18n?.labels || {};
|
|
16
15
|
---
|
|
17
16
|
|
|
@@ -23,10 +22,7 @@ const localeLabels: Record<string, string> = config.i18n?.labels || {};
|
|
|
23
22
|
hasMultipleLocales={hasMultipleLocales}
|
|
24
23
|
currentLocale={currentLocale}
|
|
25
24
|
localeLabels={localeLabels}
|
|
25
|
+
currentPath={currentPath}
|
|
26
|
+
locales={locales}
|
|
27
|
+
defaultLocale={defaultLocale}
|
|
26
28
|
/>
|
|
27
|
-
|
|
28
|
-
{hasMultipleLocales && (
|
|
29
|
-
<div class="hidden">
|
|
30
|
-
<LanguageSwitcher currentLocale={currentLocale} />
|
|
31
|
-
</div>
|
|
32
|
-
)}
|
|
@@ -10,20 +10,22 @@ const { currentLocale } = Astro.props;
|
|
|
10
10
|
const currentPath = Astro.url.pathname;
|
|
11
11
|
|
|
12
12
|
function getLocalizedUrl(locale: string): string {
|
|
13
|
-
//
|
|
13
|
+
// URLs: /docs/... (en), /docs/ko/... (ko) — no /en/ or /ko/ prefix
|
|
14
14
|
let path = currentPath;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const docsPrefix = "/docs/";
|
|
16
|
+
const koPrefix = "/docs/ko/";
|
|
17
|
+
|
|
18
|
+
if (path.startsWith(koPrefix)) {
|
|
19
|
+
path = path === koPrefix.slice(0, -1) ? "/docs" : docsPrefix + path.slice(koPrefix.length);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
// Add new locale
|
|
21
|
+
|
|
23
22
|
if (locale === defaultLocale) {
|
|
24
|
-
return path;
|
|
23
|
+
return path || "/";
|
|
24
|
+
}
|
|
25
|
+
if (path === "/" || !path.startsWith(docsPrefix)) {
|
|
26
|
+
return path === "/" ? "/docs/ko/introduction" : path;
|
|
25
27
|
}
|
|
26
|
-
return
|
|
28
|
+
return docsPrefix + "ko/" + path.slice(docsPrefix.length);
|
|
27
29
|
}
|
|
28
30
|
---
|
|
29
31
|
|
|
@@ -19,16 +19,12 @@ function normalizePath(path: string): string {
|
|
|
19
19
|
function isActive(page: string): boolean {
|
|
20
20
|
const normalized = normalizePath(currentPath);
|
|
21
21
|
const pagePath = normalizePath(page);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
normalized === `${currentLocale}/docs/${pagePath}`;
|
|
22
|
+
const docsSlug = currentLocale === defaultLocale ? pagePath : `ko/${pagePath}`;
|
|
23
|
+
return normalized === `docs/${pagePath}` || normalized === `docs/${docsSlug}`;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
function getPageHref(page: string): string {
|
|
28
|
-
|
|
29
|
-
return `/docs/${page}`;
|
|
30
|
-
}
|
|
31
|
-
return `/${currentLocale}/docs/${page}`;
|
|
27
|
+
return currentLocale === defaultLocale ? `/docs/${page}` : `/docs/ko/${page}`;
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
// Get page title from the page slug
|
|
@@ -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: {
|
|
@@ -26,17 +79,6 @@ function createThemeIntegration(options?: DocsThemeOptions): AstroIntegration {
|
|
|
26
79
|
entrypoint: "@barodoc/theme-docs/pages/docs/[...slug].astro",
|
|
27
80
|
});
|
|
28
81
|
|
|
29
|
-
// Inject localized routes for non-default locales
|
|
30
|
-
injectRoute({
|
|
31
|
-
pattern: "/[locale]",
|
|
32
|
-
entrypoint: "@barodoc/theme-docs/pages/index.astro",
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
injectRoute({
|
|
36
|
-
pattern: "/[locale]/docs/[...slug]",
|
|
37
|
-
entrypoint: "@barodoc/theme-docs/pages/docs/[...slug].astro",
|
|
38
|
-
});
|
|
39
|
-
|
|
40
82
|
// Update Astro config with integrations and Vite plugins
|
|
41
83
|
updateConfig({
|
|
42
84
|
integrations: [mdx(), react()],
|
|
@@ -52,6 +94,10 @@ function createThemeIntegration(options?: DocsThemeOptions): AstroIntegration {
|
|
|
52
94
|
light: "github-light",
|
|
53
95
|
dark: "github-dark",
|
|
54
96
|
},
|
|
97
|
+
transformers: [
|
|
98
|
+
createTrimEmptyLinesTransformer(),
|
|
99
|
+
...(lineNumbers ? [createLineNumbersTransformer()] : []),
|
|
100
|
+
],
|
|
55
101
|
},
|
|
56
102
|
},
|
|
57
103
|
});
|
|
@@ -65,7 +111,7 @@ function createThemeIntegration(options?: DocsThemeOptions): AstroIntegration {
|
|
|
65
111
|
export default function docsTheme(options?: DocsThemeOptions): ThemeExport {
|
|
66
112
|
return {
|
|
67
113
|
name: "@barodoc/theme-docs",
|
|
68
|
-
integration: () => createThemeIntegration(options),
|
|
114
|
+
integration: (config) => createThemeIntegration(config, options),
|
|
69
115
|
styles: options?.customCss || [],
|
|
70
116
|
};
|
|
71
117
|
}
|
|
@@ -30,11 +30,11 @@ const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
32
|
<BaseLayout title={title} description={description}>
|
|
33
|
-
<Header currentLocale={currentLocale} />
|
|
33
|
+
<Header currentLocale={currentLocale} currentPath={currentPath} />
|
|
34
34
|
|
|
35
35
|
<!-- Centered container for docs layout -->
|
|
36
|
-
<div class="w-full min-h-[calc(100vh-3.5rem)] flex justify-center">
|
|
37
|
-
<div class="flex w-full max-w-[1120px]">
|
|
36
|
+
<div class="w-full min-w-0 min-h-[calc(100vh-3.5rem)] flex justify-center overflow-x-hidden">
|
|
37
|
+
<div class="flex w-full max-w-[1120px] min-w-0">
|
|
38
38
|
<!-- Desktop Sidebar -->
|
|
39
39
|
<aside class="hidden lg:block w-[220px] shrink-0">
|
|
40
40
|
<div class="sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto py-6 pr-4">
|
|
@@ -42,10 +42,10 @@ const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
|
|
|
42
42
|
</div>
|
|
43
43
|
</aside>
|
|
44
44
|
|
|
45
|
-
<!-- Main Content -->
|
|
46
|
-
<main class="flex-1 min-w-0 min-w-[650px] max-w-[720px]">
|
|
47
|
-
<div class="px-8 py-8">
|
|
48
|
-
<article class="prose prose-gray dark:prose-invert max-w-none">
|
|
45
|
+
<!-- Main Content: no min-width on mobile to avoid horizontal scroll -->
|
|
46
|
+
<main class="flex-1 min-w-0 lg:min-w-[650px] max-w-[720px]">
|
|
47
|
+
<div class="px-4 py-6 sm:px-6 sm:py-8 lg:px-8 lg:py-8">
|
|
48
|
+
<article class="prose prose-gray dark:prose-invert max-w-none min-w-0 overflow-x-auto">
|
|
49
49
|
<slot />
|
|
50
50
|
</article>
|
|
51
51
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from "../layouts/BaseLayout.astro";
|
|
3
|
+
import config from "virtual:barodoc/config";
|
|
4
|
+
|
|
5
|
+
const pathname = new URL(Astro.request.url).pathname;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<BaseLayout title={`${config.name} · Page not found`} description="The page you are looking for does not exist.">
|
|
9
|
+
<div class="min-h-screen flex flex-col items-center justify-center px-4">
|
|
10
|
+
<div class="text-center max-w-md">
|
|
11
|
+
<p class="text-8xl font-bold text-[var(--color-text-muted)] select-none" aria-hidden="true">404</p>
|
|
12
|
+
<h1 class="text-2xl font-semibold text-[var(--color-text)] mt-4">Page not found</h1>
|
|
13
|
+
<p class="text-[var(--color-text-secondary)] mt-2">
|
|
14
|
+
The page at <code class="text-sm bg-[var(--color-bg-secondary)] px-2 py-1 rounded break-all">{pathname}</code> does not exist.
|
|
15
|
+
</p>
|
|
16
|
+
<div class="flex flex-col sm:flex-row items-center justify-center gap-3 mt-8">
|
|
17
|
+
<a
|
|
18
|
+
href="/"
|
|
19
|
+
class="px-5 py-2.5 bg-primary-600 text-white rounded-xl hover:bg-primary-700 transition-colors font-medium"
|
|
20
|
+
>
|
|
21
|
+
Home
|
|
22
|
+
</a>
|
|
23
|
+
<a
|
|
24
|
+
href="/docs/introduction"
|
|
25
|
+
class="px-5 py-2.5 border border-[var(--color-border)] text-[var(--color-text)] rounded-xl hover:bg-[var(--color-bg-secondary)] transition-colors font-medium"
|
|
26
|
+
>
|
|
27
|
+
Documentation
|
|
28
|
+
</a>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</BaseLayout>
|
|
@@ -21,7 +21,7 @@ export async function getStaticPaths() {
|
|
|
21
21
|
cleanSlug = slugParts.slice(1).join("/");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// URL: /docs/introduction (en), /docs/ko/introduction (ko) — locale only inside slug
|
|
25
25
|
const path = locale === defaultLocale
|
|
26
26
|
? cleanSlug
|
|
27
27
|
: `${locale}/${cleanSlug}`;
|
|
@@ -71,12 +71,9 @@ function getPageTitle(slug: string): string {
|
|
|
71
71
|
.join(' ');
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// Get page href
|
|
74
|
+
// Get page href (always /docs/...; non-default locale is inside slug)
|
|
75
75
|
function getPageHref(slug: string): string {
|
|
76
|
-
|
|
77
|
-
return `/docs/${slug}`;
|
|
78
|
-
}
|
|
79
|
-
return `/${locale}/docs/${slug}`;
|
|
76
|
+
return `/docs/${locale === defaultLocale ? slug : `${locale}/${slug}`}`;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
// Find prev/next pages
|
package/src/styles/global.css
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
background-color: var(--color-bg);
|
|
64
64
|
color: var(--color-text);
|
|
65
65
|
font-feature-settings: "rlig" 1, "calt" 1;
|
|
66
|
+
overflow-x: hidden;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/* Smooth transitions for all interactive elements */
|
|
@@ -76,29 +77,29 @@
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
/* Prose styling for MDX content -
|
|
80
|
+
/* Prose styling for MDX content - */
|
|
80
81
|
.prose {
|
|
81
82
|
--tw-prose-body: var(--color-text);
|
|
82
83
|
--tw-prose-headings: var(--color-text);
|
|
83
84
|
--tw-prose-links: var(--color-primary-600);
|
|
84
85
|
--tw-prose-code: var(--color-text);
|
|
85
86
|
--tw-prose-pre-bg: var(--color-bg-secondary);
|
|
86
|
-
font-size: 0.875rem !important;
|
|
87
|
-
line-height: 1.6 !important;
|
|
87
|
+
/* font-size: 0.875rem !important; */
|
|
88
|
+
/* line-height: 1.6 !important; */
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
.dark .prose {
|
|
91
92
|
--tw-prose-links: var(--color-primary-400);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
/* Headings -
|
|
95
|
+
/* Headings - */
|
|
95
96
|
.prose :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
|
|
96
97
|
font-size: 1.75rem !important;
|
|
97
98
|
font-weight: 700 !important;
|
|
98
99
|
letter-spacing: -0.025em !important;
|
|
99
100
|
margin-top: 0 !important;
|
|
100
101
|
margin-bottom: 0.375rem !important;
|
|
101
|
-
line-height: 1.25 !important;
|
|
102
|
+
/* line-height: 1.25 !important; */
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
.prose :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
|
|
@@ -142,14 +143,14 @@
|
|
|
142
143
|
text-underline-offset: 2px !important;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
/* Code block styling -
|
|
146
|
+
/* Code block styling - */
|
|
146
147
|
.prose :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
|
|
147
148
|
background-color: var(--color-bg) !important;
|
|
148
149
|
border: 1px solid var(--color-border) !important;
|
|
149
150
|
border-radius: 0.5rem !important;
|
|
150
151
|
padding: 0.75rem 1rem !important;
|
|
151
152
|
font-size: 0.8125rem !important;
|
|
152
|
-
line-height: 1.
|
|
153
|
+
line-height: 1.45 !important;
|
|
153
154
|
overflow-x: auto !important;
|
|
154
155
|
margin: 0.75rem 0 !important;
|
|
155
156
|
}
|
|
@@ -184,7 +185,7 @@
|
|
|
184
185
|
color: var(--color-text-muted) !important;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
/* Blockquotes -
|
|
188
|
+
/* Blockquotes - */
|
|
188
189
|
.prose :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
|
|
189
190
|
border-left: 3px solid var(--color-primary-500) !important;
|
|
190
191
|
background-color: var(--color-bg-secondary) !important;
|
|
@@ -240,6 +241,20 @@
|
|
|
240
241
|
.astro-code {
|
|
241
242
|
background-color: var(--color-bg) !important;
|
|
242
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;
|
|
243
258
|
}
|
|
244
259
|
|
|
245
260
|
.dark .astro-code,
|
|
@@ -248,6 +263,28 @@
|
|
|
248
263
|
background-color: var(--shiki-dark-bg) !important;
|
|
249
264
|
}
|
|
250
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
|
+
|
|
251
288
|
/* Pagefind search styling */
|
|
252
289
|
.pagefind-ui {
|
|
253
290
|
--pagefind-ui-scale: 1;
|