@barodoc/theme-docs 3.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/package.json +6 -2
  2. package/src/components/Breadcrumb.astro +8 -8
  3. package/src/components/CodeCopy.astro +14 -13
  4. package/src/components/Contributors.astro +71 -0
  5. package/src/components/DocHeader.tsx +24 -24
  6. package/src/components/DocsSidebar.tsx +11 -15
  7. package/src/components/KeyboardShortcuts.astro +108 -0
  8. package/src/components/LanguageSwitcher.astro +3 -3
  9. package/src/components/MobileNavSheet.tsx +1 -1
  10. package/src/components/Search.tsx +10 -10
  11. package/src/components/SearchDialog.tsx +8 -8
  12. package/src/components/TableOfContents.astro +5 -5
  13. package/src/components/ThemeToggle.tsx +4 -4
  14. package/src/components/VersionSwitcher.tsx +79 -0
  15. package/src/components/api/ApiEndpoint.astro +5 -5
  16. package/src/components/api/ApiParam.astro +4 -4
  17. package/src/components/api/ApiParams.astro +3 -3
  18. package/src/components/api/ApiResponse.astro +2 -2
  19. package/src/components/index.ts +5 -0
  20. package/src/components/mdx/Accordion.tsx +6 -6
  21. package/src/components/mdx/ApiPlayground.tsx +200 -0
  22. package/src/components/mdx/Badge.tsx +1 -1
  23. package/src/components/mdx/Callout.astro +20 -20
  24. package/src/components/mdx/Card.astro +5 -5
  25. package/src/components/mdx/CodeGroup.astro +23 -20
  26. package/src/components/mdx/CodeGroup.tsx +6 -6
  27. package/src/components/mdx/DocAccordion.tsx +5 -5
  28. package/src/components/mdx/DocCard.tsx +2 -2
  29. package/src/components/mdx/DocTabs.tsx +3 -3
  30. package/src/components/mdx/Expandable.tsx +11 -11
  31. package/src/components/mdx/FileTree.tsx +6 -6
  32. package/src/components/mdx/Frame.tsx +2 -2
  33. package/src/components/mdx/ImageZoom.tsx +35 -0
  34. package/src/components/mdx/Mermaid.tsx +3 -3
  35. package/src/components/mdx/ParamField.tsx +7 -7
  36. package/src/components/mdx/ResponseField.tsx +6 -6
  37. package/src/components/mdx/Step.astro +2 -2
  38. package/src/components/mdx/Steps.astro +3 -3
  39. package/src/components/mdx/Steps.tsx +10 -19
  40. package/src/components/mdx/Tabs.tsx +5 -8
  41. package/src/components/mdx/Tooltip.tsx +20 -19
  42. package/src/components/mdx/Video.tsx +71 -0
  43. package/src/components/ui/accordion.tsx +2 -2
  44. package/src/components/ui/alert.tsx +1 -1
  45. package/src/components/ui/button.tsx +3 -3
  46. package/src/components/ui/card.tsx +2 -2
  47. package/src/components/ui/dialog.tsx +2 -2
  48. package/src/components/ui/scroll-area.tsx +1 -1
  49. package/src/components/ui/separator.tsx +1 -1
  50. package/src/components/ui/sheet.tsx +3 -3
  51. package/src/components/ui/tabs.tsx +2 -2
  52. package/src/components/ui/tooltip.tsx +1 -1
  53. package/src/index.ts +33 -1
  54. package/src/layouts/BaseLayout.astro +10 -1
  55. package/src/layouts/BlogLayout.astro +93 -0
  56. package/src/layouts/DocsLayout.astro +72 -23
  57. package/src/pages/404.astro +5 -5
  58. package/src/pages/blog/[...slug].astro +39 -0
  59. package/src/pages/blog/index.astro +92 -0
  60. package/src/pages/changelog/index.astro +72 -0
  61. package/src/pages/docs/[...slug].astro +6 -2
  62. package/src/pages/index.astro +21 -21
  63. package/src/styles/global.css +1041 -166
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barodoc/theme-docs",
3
- "version": "3.0.0",
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": "3.0.0"
37
+ "@barodoc/core": "6.0.0"
34
38
  },
35
39
  "peerDependencies": {
36
40
  "astro": "^5.0.0",
@@ -11,20 +11,20 @@ interface Props {
11
11
  const { items } = Astro.props;
12
12
  ---
13
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>
14
+ <nav aria-label="Breadcrumb" class="mb-4">
15
+ <ol class="flex flex-wrap items-center gap-1.5 text-[13px] text-[var(--bd-text-muted)]">
16
+ <li class="flex items-center gap-1.5">
17
+ <a href="/docs" class="hover:text-[var(--bd-text)] transition-colors">Docs</a>
18
18
  </li>
19
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">
20
+ <li class="flex items-center gap-1.5">
21
+ <svg class="w-3 h-3 text-[var(--bd-text-muted)] opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
22
22
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
23
23
  </svg>
24
24
  {item.href && i < items.length - 1 ? (
25
- <a href={item.href} class="hover:text-[var(--color-text-secondary)] transition-colors">{item.label}</a>
25
+ <a href={item.href} class="hover:text-[var(--bd-text)] transition-colors">{item.label}</a>
26
26
  ) : (
27
- <span class="text-[var(--color-text-secondary)]">{item.label}</span>
27
+ <span class="text-[var(--bd-text-secondary)] font-medium">{item.label}</span>
28
28
  )}
29
29
  </li>
30
30
  ))}
@@ -25,12 +25,13 @@
25
25
  const wrapper = document.createElement('div');
26
26
  wrapper.className = 'code-block-wrapper';
27
27
 
28
- // Create header if filename exists
29
- if (filename) {
28
+ // Create header for filename or language label
29
+ const displayLabel = filename || lang;
30
+ if (displayLabel) {
30
31
  const header = document.createElement('div');
31
32
  header.className = 'code-block-header';
32
33
  header.innerHTML = `
33
- <span class="code-block-filename">${filename}</span>
34
+ <span class="code-block-filename">${displayLabel}</span>
34
35
  `;
35
36
  wrapper.appendChild(header);
36
37
  }
@@ -89,8 +90,8 @@
89
90
  margin: 1rem 0;
90
91
  border-radius: 0.5rem;
91
92
  overflow: hidden;
92
- background: var(--color-bg-secondary);
93
- border: 1px solid var(--color-border);
93
+ background: var(--bd-bg-subtle);
94
+ border: 1px solid var(--bd-border);
94
95
  }
95
96
 
96
97
  .code-block-wrapper pre {
@@ -145,14 +146,14 @@
145
146
  align-items: center;
146
147
  justify-content: space-between;
147
148
  padding: 0.5rem 1rem;
148
- background: var(--color-bg-tertiary);
149
- border-bottom: 1px solid var(--color-border);
149
+ background: var(--bd-bg-muted);
150
+ border-bottom: 1px solid var(--bd-border);
150
151
  }
151
152
 
152
153
  .code-block-filename {
153
154
  font-size: 0.8125rem;
154
155
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
155
- color: var(--color-text-secondary);
156
+ color: var(--bd-text-secondary);
156
157
  }
157
158
 
158
159
  .copy-button {
@@ -165,13 +166,13 @@
165
166
  padding: 0.375rem 0.625rem;
166
167
  font-size: 0.75rem;
167
168
  font-weight: 500;
168
- background: var(--color-bg);
169
- border: 1px solid var(--color-border);
169
+ background: var(--bd-bg);
170
+ border: 1px solid var(--bd-border);
170
171
  border-radius: 0.375rem;
171
172
  cursor: pointer;
172
173
  opacity: 0;
173
174
  transition: opacity 150ms ease, background 150ms ease;
174
- color: var(--color-text-muted);
175
+ color: var(--bd-text-muted);
175
176
  z-index: 10;
176
177
  }
177
178
 
@@ -180,8 +181,8 @@
180
181
  }
181
182
 
182
183
  .copy-button:hover {
183
- background: var(--color-bg-secondary);
184
- color: var(--color-text);
184
+ background: var(--bd-bg-subtle);
185
+ color: var(--bd-text);
185
186
  }
186
187
 
187
188
  .copy-button .copy-icon,
@@ -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
+ )}
@@ -90,32 +90,32 @@ export function DocHeader({
90
90
 
91
91
  return (
92
92
  <TooltipProvider>
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">
93
+ <header className="sticky top-0 z-50 w-full min-w-0 border-b border-[var(--bd-border)] bg-[var(--bd-bg)]/95 backdrop-blur-md supports-[backdrop-filter]:bg-[var(--bd-bg)]/80">
94
+ <div className="flex h-14 items-center justify-between gap-2 px-4 sm:px-6 max-w-[1280px] mx-auto min-w-0">
95
95
  {/* Logo */}
96
96
  <div className="flex items-center gap-6 min-w-0 shrink">
97
97
  <a
98
98
  href="/"
99
- className="flex items-center gap-2 min-w-0 shrink overflow-hidden font-semibold text-[var(--color-text)] hover:opacity-80 transition-opacity"
99
+ className="flex items-center gap-2.5 min-w-0 shrink overflow-hidden font-semibold text-[var(--bd-text-heading)] hover:opacity-80 transition-opacity"
100
100
  >
101
101
  {logo && <img src={logo} alt={siteName} className="h-7 w-7 shrink-0" />}
102
- <span className="text-lg truncate">{siteName}</span>
102
+ <span className="text-[15px] tracking-tight truncate">{siteName}</span>
103
103
  </a>
104
104
  </div>
105
105
 
106
106
  {/* Right side actions */}
107
- <div className="flex items-center gap-1 shrink-0">
107
+ <div className="flex items-center gap-1.5 shrink-0">
108
108
  {/* Search button */}
109
109
  <Button
110
110
  variant="outline"
111
- className="hidden md:flex items-center gap-3 px-4 py-2 h-10 min-w-[200px] justify-start rounded-xl"
111
+ className="hidden md:flex items-center gap-3 px-3 py-1.5 h-9 min-w-[220px] justify-start rounded-lg border-[var(--bd-border)]"
112
112
  onClick={openSearch}
113
113
  >
114
- <Search className="h-4 w-4 shrink-0 text-[var(--color-text-muted)]" />
115
- <span className="flex-1 text-left text-[var(--color-text-muted)]">
114
+ <Search className="h-4 w-4 shrink-0 text-[var(--bd-text-muted)]" />
115
+ <span className="flex-1 text-left text-sm text-[var(--bd-text-muted)]">
116
116
  Search...
117
117
  </span>
118
- <kbd className="hidden lg:inline-flex items-center gap-0.5 px-2 py-1 text-xs font-medium bg-[var(--color-bg)] border border-[var(--color-border)] rounded-md text-[var(--color-text-muted)]">
118
+ <kbd className="hidden lg:inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-medium bg-[var(--bd-bg-subtle)] border border-[var(--bd-border)] rounded text-[var(--bd-text-muted)]">
119
119
  <span>⌘</span>K
120
120
  </kbd>
121
121
  </Button>
@@ -126,7 +126,7 @@ export function DocHeader({
126
126
  <Button
127
127
  variant="ghost"
128
128
  size="icon"
129
- className="md:hidden rounded-xl"
129
+ className="md:hidden"
130
130
  onClick={openSearch}
131
131
  >
132
132
  <Search className="h-5 w-5" />
@@ -137,7 +137,7 @@ export function DocHeader({
137
137
  </Tooltip>
138
138
 
139
139
  {/* Divider */}
140
- <Separator orientation="vertical" className="hidden md:block h-6 mx-2" />
140
+ <Separator orientation="vertical" className="hidden md:block h-5 mx-1.5" />
141
141
 
142
142
  {/* GitHub link */}
143
143
  {githubUrl && (
@@ -146,7 +146,7 @@ export function DocHeader({
146
146
  <Button
147
147
  variant="ghost"
148
148
  size="icon"
149
- className="rounded-xl"
149
+ className="h-8 w-8 text-[var(--bd-text-secondary)] hover:text-[var(--bd-text)]"
150
150
  asChild
151
151
  >
152
152
  <a
@@ -154,7 +154,7 @@ export function DocHeader({
154
154
  target="_blank"
155
155
  rel="noopener noreferrer"
156
156
  >
157
- <Github className="h-5 w-5" />
157
+ <Github className="h-[18px] w-[18px]" />
158
158
  <span className="sr-only">GitHub</span>
159
159
  </a>
160
160
  </Button>
@@ -166,20 +166,20 @@ export function DocHeader({
166
166
  {/* Language switcher */}
167
167
  {hasMultipleLocales && locales.length > 0 && (
168
168
  <>
169
- <Separator orientation="vertical" className="hidden md:block h-6 mx-2" />
169
+ <Separator orientation="vertical" className="hidden md:block h-5 mx-1" />
170
170
  <div className="relative" ref={langRef}>
171
171
  <Tooltip>
172
172
  <TooltipTrigger asChild>
173
173
  <Button
174
174
  variant="ghost"
175
- className="rounded-xl gap-1 px-2"
175
+ className="h-8 gap-1 px-2 text-[var(--bd-text-secondary)] hover:text-[var(--bd-text)]"
176
176
  onClick={(e) => {
177
177
  e.stopPropagation();
178
178
  setLangOpen((o) => !o);
179
179
  }}
180
180
  >
181
181
  <Globe className="h-4 w-4" />
182
- <span className="text-sm hidden sm:inline">
182
+ <span className="text-[13px] hidden sm:inline">
183
183
  {localeLabels[currentLocale] ?? currentLocale}
184
184
  </span>
185
185
  <ChevronDown className="h-3 w-3" />
@@ -190,7 +190,7 @@ export function DocHeader({
190
190
  </Tooltip>
191
191
  {langOpen && (
192
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"
193
+ className="absolute right-0 mt-1 py-1 min-w-[8rem] rounded-lg border border-[var(--bd-border)] bg-[var(--bd-bg)] shadow-[var(--bd-shadow-lg)] z-50"
194
194
  role="menu"
195
195
  >
196
196
  {locales.map((locale) => (
@@ -198,10 +198,10 @@ export function DocHeader({
198
198
  key={locale}
199
199
  href={getLocalizedUrl(currentPath, locale, defaultLocale)}
200
200
  className={cn(
201
- "block px-3 py-2 text-sm transition-colors",
201
+ "block px-3 py-1.5 text-[13px] transition-colors",
202
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)]"
203
+ ? "bg-primary-50 dark:bg-primary-950/50 text-primary-600 dark:text-primary-400 font-medium"
204
+ : "text-[var(--bd-text)] hover:bg-[var(--bd-bg-subtle)]"
205
205
  )}
206
206
  role="menuitem"
207
207
  >
@@ -220,13 +220,13 @@ export function DocHeader({
220
220
  <Button
221
221
  variant="ghost"
222
222
  size="icon"
223
- className="rounded-xl"
223
+ className="h-8 w-8 text-[var(--bd-text-secondary)] hover:text-[var(--bd-text)]"
224
224
  onClick={toggleTheme}
225
225
  >
226
226
  {theme === "light" ? (
227
- <Moon className="h-5 w-5" />
227
+ <Moon className="h-[18px] w-[18px]" />
228
228
  ) : (
229
- <Sun className="h-5 w-5" />
229
+ <Sun className="h-[18px] w-[18px]" />
230
230
  )}
231
231
  <span className="sr-only">Toggle theme</span>
232
232
  </Button>
@@ -240,7 +240,7 @@ export function DocHeader({
240
240
  <Button
241
241
  variant="ghost"
242
242
  size="icon"
243
- className="lg:hidden rounded-xl"
243
+ className="lg:hidden h-8 w-8 text-[var(--bd-text-secondary)] hover:text-[var(--bd-text)]"
244
244
  onClick={openMobileNav}
245
245
  >
246
246
  <Menu className="h-5 w-5" />
@@ -1,7 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { ChevronRight } from "lucide-react";
3
3
  import { cn } from "../lib/utils";
4
- import { ScrollArea } from "./ui/scroll-area";
5
4
  import {
6
5
  Collapsible,
7
6
  CollapsibleContent,
@@ -27,33 +26,30 @@ interface DocsSidebarProps {
27
26
 
28
27
  export function DocsSidebar({ groups, className }: DocsSidebarProps) {
29
28
  return (
30
- <ScrollArea className={cn("h-full", className)}>
31
- <nav className="space-y-4">
32
- {groups.map((group, index) => (
33
- <SidebarGroup key={index} group={group} />
34
- ))}
35
- </nav>
36
- </ScrollArea>
29
+ <nav className={cn("space-y-5", className)}>
30
+ {groups.map((group, index) => (
31
+ <SidebarGroup key={index} group={group} />
32
+ ))}
33
+ </nav>
37
34
  );
38
35
  }
39
36
 
40
37
  function SidebarGroup({ group }: { group: NavGroup }) {
41
38
  const [isOpen, setIsOpen] = React.useState(group.defaultOpen ?? true);
42
- const hasActiveItem = group.items.some((item) => item.isActive);
43
39
 
44
40
  return (
45
41
  <Collapsible open={isOpen} onOpenChange={setIsOpen}>
46
- <CollapsibleTrigger className="group flex w-full items-center gap-1.5 py-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition-colors">
42
+ <CollapsibleTrigger className="group flex w-full items-center gap-1.5 py-1.5 text-[13px] font-semibold text-[var(--bd-text)] hover:text-[var(--bd-text-heading)] transition-colors">
47
43
  <ChevronRight
48
44
  className={cn(
49
- "h-3 w-3 transition-transform duration-200",
45
+ "h-3.5 w-3.5 text-[var(--bd-text-muted)] transition-transform duration-200",
50
46
  isOpen && "rotate-90"
51
47
  )}
52
48
  />
53
49
  {group.title}
54
50
  </CollapsibleTrigger>
55
51
  <CollapsibleContent className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
56
- <ul className="mt-1 space-y-0.5 border-l border-[var(--color-border)] ml-1.5">
52
+ <ul className="mt-1 space-y-px border-l border-[var(--bd-border)] ml-2">
57
53
  {group.items.map((item, index) => (
58
54
  <SidebarItem key={index} item={item} />
59
55
  ))}
@@ -69,10 +65,10 @@ function SidebarItem({ item }: { item: NavItem }) {
69
65
  <a
70
66
  href={item.href}
71
67
  className={cn(
72
- "block pl-4 pr-2 py-1.5 text-sm transition-all duration-150 -ml-px border-l-2",
68
+ "block pl-4 pr-2 py-1.5 text-[13px] rounded-r-md transition-all duration-150 -ml-px border-l-2",
73
69
  item.isActive
74
- ? "border-primary-500 text-primary-600 dark:text-primary-400 font-medium bg-primary-50/50 dark:bg-primary-950/30"
75
- : "border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:border-[var(--color-border)]"
70
+ ? "border-[var(--bd-sidebar-active-border)] text-[var(--bd-sidebar-active-text)] font-medium bg-[var(--bd-sidebar-active-bg)]"
71
+ : "border-transparent text-[var(--bd-text-secondary)] hover:text-[var(--bd-text)] hover:border-[var(--bd-text-muted)]"
76
72
  )}
77
73
  >
78
74
  <span className="truncate">{item.title}</span>
@@ -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>
@@ -32,7 +32,7 @@ function getLocalizedUrl(locale: string): string {
32
32
  <div class="relative">
33
33
  <button
34
34
  id="lang-switcher-btn"
35
- class="flex items-center gap-1 px-2 py-1.5 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)] rounded-lg hover:bg-[var(--color-bg-secondary)] transition-colors"
35
+ class="flex items-center gap-1 px-2 py-1.5 text-sm text-[var(--bd-text-secondary)] hover:text-[var(--bd-text)] rounded-lg hover:bg-[var(--bd-bg-subtle)] transition-colors"
36
36
  aria-label="Select language"
37
37
  >
38
38
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -46,7 +46,7 @@ function getLocalizedUrl(locale: string): string {
46
46
 
47
47
  <div
48
48
  id="lang-switcher-menu"
49
- class="hidden absolute right-0 mt-1 py-1 w-32 bg-[var(--color-bg)] border border-[var(--color-border)] rounded-lg shadow-lg z-50"
49
+ class="hidden absolute right-0 mt-1 py-1 w-32 bg-[var(--bd-bg)] border border-[var(--bd-border)] rounded-lg shadow-lg z-50"
50
50
  >
51
51
  {locales.map((locale) => (
52
52
  <a
@@ -55,7 +55,7 @@ function getLocalizedUrl(locale: string): string {
55
55
  "block px-3 py-1.5 text-sm transition-colors",
56
56
  locale === currentLocale
57
57
  ? "bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300"
58
- : "text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)]"
58
+ : "text-[var(--bd-text-secondary)] hover:text-[var(--bd-text)] hover:bg-[var(--bd-bg-subtle)]"
59
59
  ]}
60
60
  >
61
61
  {getLocaleLabel(locale, i18n?.labels)}
@@ -54,7 +54,7 @@ export function MobileNavSheet({ groups, siteName, logo }: MobileNavSheetProps)
54
54
  return (
55
55
  <Sheet open={open} onOpenChange={setOpen}>
56
56
  <SheetContent side="left" className="w-80 p-0">
57
- <SheetHeader className="px-6 py-4 border-b border-[var(--color-border)]">
57
+ <SheetHeader className="px-6 py-4 border-b border-[var(--bd-border)]">
58
58
  <SheetTitle className="flex items-center gap-2">
59
59
  {logo && <img src={logo} alt={siteName} className="h-6 w-6" />}
60
60
  <span>{siteName}</span>
@@ -133,10 +133,10 @@ export function Search() {
133
133
  >
134
134
  <div className="fixed inset-0 bg-black/50" aria-hidden="true" />
135
135
  <div className="fixed inset-x-4 top-[15%] md:inset-x-auto md:left-1/2 md:-translate-x-1/2 md:w-full md:max-w-lg">
136
- <div className="bg-[var(--color-bg)] rounded-xl shadow-2xl border border-[var(--color-border)] overflow-hidden">
137
- <div className="flex items-center px-4 border-b border-[var(--color-border)]">
136
+ <div className="bg-[var(--bd-bg)] rounded-xl shadow-2xl border border-[var(--bd-border)] overflow-hidden">
137
+ <div className="flex items-center px-4 border-b border-[var(--bd-border)]">
138
138
  <svg
139
- className="w-5 h-5 text-[var(--color-text-secondary)]"
139
+ className="w-5 h-5 text-[var(--bd-text-secondary)]"
140
140
  fill="none"
141
141
  stroke="currentColor"
142
142
  viewBox="0 0 24 24"
@@ -155,11 +155,11 @@ export function Search() {
155
155
  onChange={(e) => setQuery(e.target.value)}
156
156
  onKeyDown={handleKeyDown}
157
157
  placeholder="Search documentation..."
158
- className="flex-1 px-4 py-4 bg-transparent text-[var(--color-text)] placeholder-[var(--color-text-secondary)] focus:outline-none"
158
+ className="flex-1 px-4 py-4 bg-transparent text-[var(--bd-text)] placeholder-[var(--bd-text-secondary)] focus:outline-none"
159
159
  />
160
160
  <button
161
161
  onClick={() => setIsOpen(false)}
162
- className="px-2 py-1 text-xs text-[var(--color-text-secondary)] bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded"
162
+ className="px-2 py-1 text-xs text-[var(--bd-text-secondary)] bg-[var(--bd-bg-subtle)] border border-[var(--bd-border)] rounded"
163
163
  >
164
164
  ESC
165
165
  </button>
@@ -168,7 +168,7 @@ export function Search() {
168
168
  {query.trim() && (
169
169
  <div className="max-h-[60vh] overflow-y-auto">
170
170
  {results.length === 0 ? (
171
- <div className="px-4 py-8 text-center text-[var(--color-text-secondary)]">
171
+ <div className="px-4 py-8 text-center text-[var(--bd-text-secondary)]">
172
172
  {pagefind ? "No results found" : "Search index not available"}
173
173
  </div>
174
174
  ) : (
@@ -180,14 +180,14 @@ export function Search() {
180
180
  className={`block px-4 py-3 transition-colors ${
181
181
  index === selectedIndex
182
182
  ? "bg-primary-50 dark:bg-primary-900/30"
183
- : "hover:bg-[var(--color-bg-secondary)]"
183
+ : "hover:bg-[var(--bd-bg-subtle)]"
184
184
  }`}
185
185
  >
186
- <div className="font-medium text-[var(--color-text)]">
186
+ <div className="font-medium text-[var(--bd-text)]">
187
187
  {result.meta.title}
188
188
  </div>
189
189
  <div
190
- className="text-sm text-[var(--color-text-secondary)] line-clamp-2 mt-0.5"
190
+ className="text-sm text-[var(--bd-text-secondary)] line-clamp-2 mt-0.5"
191
191
  dangerouslySetInnerHTML={{ __html: result.excerpt }}
192
192
  />
193
193
  </a>
@@ -199,7 +199,7 @@ export function Search() {
199
199
  )}
200
200
 
201
201
  {!query.trim() && (
202
- <div className="px-4 py-6 text-center text-[var(--color-text-secondary)] text-sm">
202
+ <div className="px-4 py-6 text-center text-[var(--bd-text-secondary)] text-sm">
203
203
  Start typing to search...
204
204
  </div>
205
205
  )}