blume 0.6.7 → 0.8.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/CHANGELOG.md +618 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli/index.js +2609 -1041
- package/dist/cli/index.js.map +110 -103
- package/dist/types/ai/component-markdown.d.ts +34 -0
- package/dist/types/components/content/youtube.d.ts +18 -0
- package/dist/types/core/base-path.d.ts +47 -0
- package/dist/types/core/config-input.d.ts +110 -12
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +477 -135
- package/dist/types/core/schema.d.ts +309 -195
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/openapi/references.d.ts +60 -0
- package/docs/01-quickstart.mdx +5 -2
- package/docs/02-deployment.mdx +24 -9
- package/docs/03-faq.mdx +46 -16
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +49 -10
- package/docs/configuration/customization.mdx +11 -0
- package/docs/configuration/index.mdx +33 -3
- package/docs/configuration/seo.mdx +2 -2
- package/docs/content/components.mdx +30 -3
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/islands.mdx +8 -0
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +17 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +8 -6
- package/package.json +15 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +11 -5
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/ask.ts +12 -7
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +143 -23
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +33 -8
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +24 -7
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +16 -4
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +393 -189
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +9 -5
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +2 -2
- package/src/astro/pages.ts +89 -22
- package/src/astro/templates.ts +259 -25
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +131 -38
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +71 -17
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/eject.ts +47 -19
- package/src/cli/commands/init.ts +120 -180
- package/src/cli/commands/preview.ts +4 -1
- package/src/cli/commands/validate.ts +44 -2
- package/src/cli/dev-lock.ts +34 -19
- package/src/cli/eject-scripts.ts +72 -0
- package/src/cli/env.ts +15 -5
- package/src/cli/init/questions.ts +158 -0
- package/src/cli/init/scaffold.ts +380 -0
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/AccordionItem.astro +23 -4
- package/src/components/content/Badge.astro +3 -1
- package/src/components/content/Card.astro +4 -2
- package/src/components/content/CodeBlock.astro +3 -0
- package/src/components/content/Component.astro +30 -16
- package/src/components/content/Diff.astro +3 -1
- package/src/components/content/Step.astro +10 -1
- package/src/components/content/Tabs.astro +15 -3
- package/src/components/content/Tile.astro +2 -1
- package/src/components/content/Tooltip.astro +3 -1
- package/src/components/content/Update.astro +9 -2
- package/src/components/content/auto-type-table.ts +25 -9
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +10 -2
- package/src/components/index.ts +23 -1
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +68 -12
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +44 -9
- package/src/components/layout/Banner.astro +12 -3
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +15 -5
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +13 -4
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +22 -7
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +51 -9
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/ReferenceLayout.astro +8 -1
- package/src/components/layout/RootLayout.astro +74 -13
- package/src/components/layout/Search.astro +107 -27
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/algolia.ts +11 -2
- package/src/components/layout/search/endpoint.ts +11 -5
- package/src/components/layout/search/orama-cloud.ts +8 -2
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/search/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +8 -2
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +8 -2
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +94 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +118 -17
- package/src/core/config.ts +8 -5
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +4 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +166 -88
- package/src/core/i18n-ui.ts +63 -3
- package/src/core/last-modified.ts +15 -6
- package/src/core/links.ts +69 -25
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +144 -58
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +25 -15
- package/src/core/schema.ts +605 -620
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +76 -63
- package/src/core/sources/normalize.ts +236 -91
- package/src/core/sources/notion.ts +27 -18
- package/src/core/sources/types.ts +2 -0
- package/src/core/tsconfig-aliases.ts +59 -30
- package/src/core/types.ts +6 -1
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -1
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +18 -8
- package/src/deploy/redirects.ts +25 -2
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +59 -13
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +60 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +46 -9
- package/src/markdown/inline-code.ts +14 -4
- package/src/markdown/package-commands.ts +10 -4
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +15 -5
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +75 -21
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +247 -19
- package/src/registry/registry.ts +0 -3
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +36 -4
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +28 -17
- package/src/theme/entry.ts +85 -20
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
3
|
+
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
2
4
|
import type { NavNode } from "../../core/types.ts";
|
|
3
5
|
import Icon from "../Icon.astro";
|
|
6
|
+
import { withBase } from "../islands/base-path.ts";
|
|
4
7
|
import Self from "./NavTree.astro";
|
|
5
8
|
|
|
6
9
|
interface PagePanel {
|
|
@@ -21,6 +24,8 @@ interface Props {
|
|
|
21
24
|
idPrefix?: string;
|
|
22
25
|
/** True only for the top-level call; renders the `<blume-nav>` panel stack. */
|
|
23
26
|
root?: boolean;
|
|
27
|
+
/** Localized labels ("Back", the deprecated badge); English when omitted. */
|
|
28
|
+
strings?: UIStrings["nav"];
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
const {
|
|
@@ -29,12 +34,18 @@ const {
|
|
|
29
34
|
depth = 0,
|
|
30
35
|
idPrefix = "n",
|
|
31
36
|
root = depth === 0,
|
|
37
|
+
strings,
|
|
32
38
|
} = Astro.props;
|
|
33
39
|
|
|
40
|
+
// Merge over the English defaults so a label missing from a translation (or
|
|
41
|
+
// from a not-yet-regenerated snapshot) still renders instead of coming out
|
|
42
|
+
// blank — the PageActions pattern.
|
|
43
|
+
const n = { ...EN_UI.nav, ...strings };
|
|
44
|
+
|
|
34
45
|
const badgeBase =
|
|
35
46
|
"shrink-0 rounded-full px-1.5 py-0.5 font-medium text-[0.65rem] leading-none";
|
|
36
47
|
const badgeClass = `${badgeBase} bg-muted text-muted-foreground`;
|
|
37
|
-
// HTTP-method badges (from an OpenAPI reference's sidebar) are
|
|
48
|
+
// HTTP-method badges (from an OpenAPI reference's sidebar) are color-coded;
|
|
38
49
|
// every other badge keeps the neutral style.
|
|
39
50
|
const METHOD_BADGE: Record<string, string> = {
|
|
40
51
|
DELETE: "bg-red-500/15 text-red-700 dark:text-red-300",
|
|
@@ -113,13 +124,14 @@ const initialId =
|
|
|
113
124
|
idPrefix={idPrefix}
|
|
114
125
|
items={items}
|
|
115
126
|
root={false}
|
|
127
|
+
strings={n}
|
|
116
128
|
/>
|
|
117
129
|
</div>
|
|
118
130
|
{panels.map((panel) => (
|
|
119
131
|
<div data-nav-panel={panel.id} hidden={panel.id !== initialId}>
|
|
120
132
|
<div class="mb-3 flex items-center gap-1.5">
|
|
121
133
|
<button
|
|
122
|
-
aria-label=
|
|
134
|
+
aria-label={n.back}
|
|
123
135
|
class="-ml-1 flex shrink-0 items-center justify-center rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
124
136
|
data-nav-back={panel.parentId}
|
|
125
137
|
type="button"
|
|
@@ -130,7 +142,7 @@ const initialId =
|
|
|
130
142
|
<a
|
|
131
143
|
aria-current={panel.route === currentRoute ? "page" : undefined}
|
|
132
144
|
class="flex-1 truncate font-semibold text-foreground text-sm hover:underline"
|
|
133
|
-
href={panel.route}
|
|
145
|
+
href={withBase(panel.route)}
|
|
134
146
|
>
|
|
135
147
|
{panel.label}
|
|
136
148
|
</a>
|
|
@@ -146,6 +158,7 @@ const initialId =
|
|
|
146
158
|
idPrefix={panel.id}
|
|
147
159
|
items={panel.children}
|
|
148
160
|
root={false}
|
|
161
|
+
strings={n}
|
|
149
162
|
/>
|
|
150
163
|
</div>
|
|
151
164
|
))}
|
|
@@ -159,7 +172,7 @@ const initialId =
|
|
|
159
172
|
<a
|
|
160
173
|
aria-current={item.route === currentRoute ? "page" : undefined}
|
|
161
174
|
class="block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground"
|
|
162
|
-
href={item.route}
|
|
175
|
+
href={withBase(item.route)}
|
|
163
176
|
>
|
|
164
177
|
<span class="flex items-center gap-2">
|
|
165
178
|
{item.icon && (
|
|
@@ -172,7 +185,7 @@ const initialId =
|
|
|
172
185
|
<span class="flex-1 truncate">{item.label}</span>
|
|
173
186
|
{item.badge && <span class={badgeClassFor(item.badge)}>{item.badge}</span>}
|
|
174
187
|
{item.deprecated && (
|
|
175
|
-
<span class={deprecatedClass}>deprecated</span>
|
|
188
|
+
<span class={deprecatedClass}>{n.deprecated}</span>
|
|
176
189
|
)}
|
|
177
190
|
</span>
|
|
178
191
|
</a>
|
|
@@ -231,7 +244,7 @@ const initialId =
|
|
|
231
244
|
item.route === currentRoute ? "page" : undefined
|
|
232
245
|
}
|
|
233
246
|
class="-my-1 flex flex-1 items-center gap-1.5 rounded py-1 transition-colors aria-[current=page]:text-foreground aria-[current=page]:font-semibold"
|
|
234
|
-
href={item.route}
|
|
247
|
+
href={withBase(item.route)}
|
|
235
248
|
>
|
|
236
249
|
{item.icon && (
|
|
237
250
|
<Icon
|
|
@@ -271,6 +284,7 @@ const initialId =
|
|
|
271
284
|
idPrefix={id}
|
|
272
285
|
items={item.children}
|
|
273
286
|
root={false}
|
|
287
|
+
strings={n}
|
|
274
288
|
/>
|
|
275
289
|
</div>
|
|
276
290
|
</details>
|
|
@@ -286,7 +300,7 @@ const initialId =
|
|
|
286
300
|
<a
|
|
287
301
|
aria-current={item.route === currentRoute ? "page" : undefined}
|
|
288
302
|
class="-ml-1 flex flex-1 items-center gap-1.5 rounded px-1 py-0.5 text-foreground transition-colors hover:bg-muted aria-[current=page]:bg-muted"
|
|
289
|
-
href={item.route}
|
|
303
|
+
href={withBase(item.route)}
|
|
290
304
|
>
|
|
291
305
|
{item.icon && (
|
|
292
306
|
<Icon
|
|
@@ -323,6 +337,7 @@ const initialId =
|
|
|
323
337
|
idPrefix={id}
|
|
324
338
|
items={item.children}
|
|
325
339
|
root={false}
|
|
340
|
+
strings={n}
|
|
326
341
|
/>
|
|
327
342
|
</div>
|
|
328
343
|
</li>
|
|
@@ -68,6 +68,7 @@ const menuRowClass =
|
|
|
68
68
|
class="mt-8 space-y-0.5 border-border border-t pt-4"
|
|
69
69
|
data-blume-page-actions
|
|
70
70
|
data-i18n-copied={a.copied}
|
|
71
|
+
data-i18n-generating={a.generating}
|
|
71
72
|
data-mcp-name={mcpName ?? undefined}
|
|
72
73
|
data-mcp-url={mcpUrl ?? undefined}
|
|
73
74
|
data-md={mdPath}
|
|
@@ -98,7 +99,7 @@ const menuRowClass =
|
|
|
98
99
|
class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
|
|
99
100
|
>
|
|
100
101
|
<Icon name="download" size={16} />
|
|
101
|
-
|
|
102
|
+
{a.export}
|
|
102
103
|
<Icon
|
|
103
104
|
class="ms-auto transition-transform group-open:rotate-180"
|
|
104
105
|
name="chevron-down"
|
|
@@ -112,14 +113,14 @@ const menuRowClass =
|
|
|
112
113
|
{exportPdf && (
|
|
113
114
|
<button class={menuRowClass} data-blume-export-pdf type="button">
|
|
114
115
|
<Icon name="file" size={16} />
|
|
115
|
-
<span class="flex-1">
|
|
116
|
+
<span class="flex-1">{a.exportPdf}</span>
|
|
116
117
|
</button>
|
|
117
118
|
)}
|
|
118
119
|
{exportEpub && (
|
|
119
120
|
<button class={menuRowClass} data-blume-export-epub type="button">
|
|
120
121
|
<Icon name="book-open" size={16} />
|
|
121
122
|
<span class="flex-1" data-blume-epub-label>
|
|
122
|
-
|
|
123
|
+
{a.exportEpub}
|
|
123
124
|
</span>
|
|
124
125
|
</button>
|
|
125
126
|
)}
|
|
@@ -218,6 +219,8 @@ const menuRowClass =
|
|
|
218
219
|
</div>
|
|
219
220
|
|
|
220
221
|
<script>
|
|
222
|
+
import { prefixBase } from "../islands/base-path.ts";
|
|
223
|
+
|
|
221
224
|
const CHAT_URLS: Record<string, (q: string) => string> = {
|
|
222
225
|
chatgpt: (q) => `https://chatgpt.com/?hints=search&prompt=${q}`,
|
|
223
226
|
claude: (q) => `https://claude.ai/new?q=${q}`,
|
|
@@ -317,8 +320,14 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
317
320
|
}
|
|
318
321
|
});
|
|
319
322
|
|
|
320
|
-
|
|
323
|
+
// `data-md` is the base-less logical route; the raw-markdown endpoint is a
|
|
324
|
+
// generated page route, so it's served under the deployment base like any
|
|
325
|
+
// other page. Prefix at emit time (the Search-island pattern).
|
|
326
|
+
const mdRoute = root.getAttribute("data-md") ?? "";
|
|
327
|
+
const md = prefixBase(import.meta.env.BASE_URL, mdRoute);
|
|
321
328
|
const copiedLabel = root.getAttribute("data-i18n-copied") || "Copied!";
|
|
329
|
+
const generatingLabel =
|
|
330
|
+
root.getAttribute("data-i18n-generating") || "Generating…";
|
|
322
331
|
const absolute = new URL(md, location.origin).href;
|
|
323
332
|
const query = encodeURIComponent(
|
|
324
333
|
`Read ${absolute} so I can ask you questions about this page.`
|
|
@@ -414,6 +423,9 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
414
423
|
?.addEventListener("click", async () => {
|
|
415
424
|
try {
|
|
416
425
|
const response = await fetch(md);
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
throw new Error(`Fetching ${md} failed (${response.status})`);
|
|
428
|
+
}
|
|
417
429
|
await navigator.clipboard.writeText(await response.text());
|
|
418
430
|
if (label) {
|
|
419
431
|
label.textContent = copiedLabel;
|
|
@@ -421,8 +433,9 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
421
433
|
label.textContent = original;
|
|
422
434
|
}, 1500);
|
|
423
435
|
}
|
|
424
|
-
} catch {
|
|
425
|
-
// Clipboard or fetch unavailable;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
// Clipboard or fetch unavailable; don't flash "Copied!" untruthfully.
|
|
438
|
+
console.error("[blume] Copy as Markdown failed", error);
|
|
426
439
|
}
|
|
427
440
|
});
|
|
428
441
|
|
|
@@ -432,6 +445,9 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
432
445
|
|
|
433
446
|
const epubButton = root.querySelector("[data-blume-export-epub]");
|
|
434
447
|
const epubLabel = root.querySelector("[data-blume-epub-label]");
|
|
448
|
+
// Captured once — inside the handler a click during a generation would
|
|
449
|
+
// capture and permanently restore "Generating…".
|
|
450
|
+
const epubReset = epubLabel?.textContent ?? "";
|
|
435
451
|
epubButton?.addEventListener("click", async () => {
|
|
436
452
|
const article = document.querySelector("#blume-content article");
|
|
437
453
|
if (!article) {
|
|
@@ -439,9 +455,8 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
439
455
|
}
|
|
440
456
|
const heading = document.querySelector("#blume-content h1");
|
|
441
457
|
const title = heading?.textContent?.trim() || document.title;
|
|
442
|
-
const reset = epubLabel?.textContent ?? "Export to EPUB";
|
|
443
458
|
if (epubLabel) {
|
|
444
|
-
epubLabel.textContent =
|
|
459
|
+
epubLabel.textContent = generatingLabel;
|
|
445
460
|
}
|
|
446
461
|
try {
|
|
447
462
|
// Browser bundle: avoids Node built-ins and returns a Blob. Lazy-loaded
|
|
@@ -461,7 +476,7 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
461
476
|
? result
|
|
462
477
|
: new Blob([result], { type: "application/epub+zip" });
|
|
463
478
|
const slug =
|
|
464
|
-
|
|
479
|
+
mdRoute
|
|
465
480
|
.replace(/\.md$/u, "")
|
|
466
481
|
.replace(/^\//u, "")
|
|
467
482
|
.replace(/\//gu, "-") || "index";
|
|
@@ -479,7 +494,7 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
479
494
|
console.error("[blume] EPUB export failed", error);
|
|
480
495
|
} finally {
|
|
481
496
|
if (epubLabel) {
|
|
482
|
-
epubLabel.textContent =
|
|
497
|
+
epubLabel.textContent = epubReset;
|
|
483
498
|
}
|
|
484
499
|
}
|
|
485
500
|
});
|
|
@@ -9,7 +9,10 @@ interface Props {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const { strings } = Astro.props;
|
|
12
|
-
|
|
12
|
+
// Merge over the English baseline per key (rather than `strings ?? …`) so a
|
|
13
|
+
// partial — or empty `{}` — strings object still resolves every label to a
|
|
14
|
+
// default, matching the pattern PageActions and Search use.
|
|
15
|
+
const f = { ...EN_UI.feedback, ...strings };
|
|
13
16
|
|
|
14
17
|
const buttonClass =
|
|
15
18
|
"inline-flex items-center gap-2 rounded-full border border-border px-4 py-2 text-foreground text-sm transition-colors hover:border-foreground";
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
26
26
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
27
27
|
import type { LocaleSwitchOption, Navigation } from "../../core/types.ts";
|
|
28
|
+
import { withBase } from "../islands/base-path.ts";
|
|
28
29
|
import "blume:theme";
|
|
29
30
|
import Analytics from "./Analytics.astro";
|
|
30
31
|
import Banner from "./Banner.astro";
|
|
@@ -109,6 +110,10 @@ const clientDataJson = clientData
|
|
|
109
110
|
: null;
|
|
110
111
|
|
|
111
112
|
const strings = ui ?? EN_UI;
|
|
113
|
+
// Merge over the English defaults so a label missing from a translation (or
|
|
114
|
+
// from a not-yet-regenerated snapshot) still renders instead of coming out
|
|
115
|
+
// blank — the PageActions pattern.
|
|
116
|
+
const navStrings = { ...EN_UI.nav, ...strings.nav };
|
|
112
117
|
// Filter search to the active language only when the site is multi-locale.
|
|
113
118
|
const searchLocale =
|
|
114
119
|
localeSwitch && localeSwitch.length > 1 ? locale : undefined;
|
|
@@ -121,18 +126,26 @@ const route = page?.route ?? "/";
|
|
|
121
126
|
// (`/og/<slug>.png`, `index` for the home) is generated for static custom pages
|
|
122
127
|
// when `og.enabled`. Explicit props still win.
|
|
123
128
|
const ogSlug = route === "/" ? "index" : route.slice(1);
|
|
129
|
+
// Absolute URLs carry the deployment base too (`site + base + route`), since the
|
|
130
|
+
// page is served under it. `withBase` handles a root base as a no-op.
|
|
131
|
+
const basedRoute = withBase(route);
|
|
132
|
+
// The schema accepts a trailing-slash site URL (`https://docs.example.com/`);
|
|
133
|
+
// strip it before joining with the root-relative route so canonical/og URLs
|
|
134
|
+
// don't come out double-slashed — the catch-all strips it the same way.
|
|
135
|
+
const siteBase = siteUrl ? siteUrl.replace(/\/$/u, "") : null;
|
|
124
136
|
const resolvedCanonical =
|
|
125
|
-
canonical ??
|
|
137
|
+
canonical ??
|
|
138
|
+
(siteBase ? `${siteBase}${basedRoute === "/" ? "" : basedRoute}` : null);
|
|
126
139
|
// An explicit `ogImage` wins. A root-relative path (e.g. an image dropped in
|
|
127
140
|
// `public/`) is resolved against the site URL so crawlers get an absolute
|
|
128
141
|
// `og:image`; an already-absolute URL passes through untouched. Otherwise fall
|
|
129
142
|
// back to the generated OG card for this route.
|
|
130
143
|
const absolutizeOgImage = (value: string): string =>
|
|
131
|
-
value.startsWith("/") &&
|
|
144
|
+
value.startsWith("/") && siteBase ? `${siteBase}${withBase(value)}` : value;
|
|
132
145
|
const resolvedOgImage = ogImage
|
|
133
146
|
? absolutizeOgImage(ogImage)
|
|
134
|
-
: ogEnabled &&
|
|
135
|
-
? `${
|
|
147
|
+
: ogEnabled && siteBase
|
|
148
|
+
? `${siteBase}${withBase(`/og/${ogSlug}.png`)}`
|
|
136
149
|
: null;
|
|
137
150
|
|
|
138
151
|
const initialThemeScript = themeInitScript(themeMode);
|
|
@@ -173,12 +186,13 @@ const bannerScript = banner?.dismissible
|
|
|
173
186
|
class="absolute start-[-999px] top-0 z-[100] bg-accent px-4 py-2 text-accent-foreground focus:start-0"
|
|
174
187
|
href="#blume-content">{strings.page.skipToContent}</a
|
|
175
188
|
>
|
|
176
|
-
<Banner banner={banner} />
|
|
189
|
+
<Banner banner={banner} strings={strings.banner} />
|
|
177
190
|
<Header
|
|
178
191
|
askEnabled={askEnabled}
|
|
179
192
|
hasSidebar={false}
|
|
180
193
|
localeSwitch={localeSwitch}
|
|
181
194
|
logo={logo}
|
|
195
|
+
navStrings={navStrings}
|
|
182
196
|
navigation={navigation}
|
|
183
197
|
route={route}
|
|
184
198
|
searchEnabled={searchEnabled}
|
|
@@ -200,10 +214,11 @@ const bannerScript = banner?.dismissible
|
|
|
200
214
|
navigation.tabs.length > 0 && (
|
|
201
215
|
<>
|
|
202
216
|
<aside
|
|
203
|
-
aria-label=
|
|
217
|
+
aria-label={navStrings.navigation}
|
|
204
218
|
class="fixed top-[var(--blume-drawer-top,4rem)] start-0 z-[35] h-[calc(100dvh-var(--blume-drawer-top,4rem))] w-64 max-w-[80vw] -translate-x-[105%] overflow-y-auto border-border border-e bg-background px-5 pt-4 pb-6 transition-transform rtl:translate-x-[105%] [:where([data-blume-nav-open])_&]:translate-x-0! lg:hidden"
|
|
219
|
+
data-blume-nav-drawer
|
|
205
220
|
>
|
|
206
|
-
<nav aria-label=
|
|
221
|
+
<nav aria-label={navStrings.sections}>
|
|
207
222
|
<ul class="m-0 list-none p-0">
|
|
208
223
|
{navigation.tabs.map((tab) => (
|
|
209
224
|
<li>
|
|
@@ -215,7 +230,7 @@ const bannerScript = banner?.dismissible
|
|
|
215
230
|
: undefined
|
|
216
231
|
}
|
|
217
232
|
class="block rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
|
|
218
|
-
href={tab.path}
|
|
233
|
+
href={withBase(tab.path)}
|
|
219
234
|
>
|
|
220
235
|
{tab.label}
|
|
221
236
|
</a>
|
|
@@ -225,7 +240,7 @@ const bannerScript = banner?.dismissible
|
|
|
225
240
|
</nav>
|
|
226
241
|
</aside>
|
|
227
242
|
<button
|
|
228
|
-
aria-label=
|
|
243
|
+
aria-label={navStrings.closeNavigation}
|
|
229
244
|
class="fixed inset-0 z-[30] hidden cursor-pointer border-0 bg-black/40 [:where([data-blume-nav-open])_&]:block lg:hidden"
|
|
230
245
|
data-blume-nav-toggle
|
|
231
246
|
type="button"
|
|
@@ -247,6 +262,33 @@ const bannerScript = banner?.dismissible
|
|
|
247
262
|
// Dev-only: friendly hint after a React island hydration mismatch;
|
|
248
263
|
// tree-shaken out of production builds.
|
|
249
264
|
import "./hydration-hint.ts";
|
|
265
|
+
|
|
266
|
+
// The closed tabs drawer is only translated off-canvas, so its links
|
|
267
|
+
// would stay in the tab order. Mirror the header's `data-blume-nav-open`
|
|
268
|
+
// toggle into `inert`/`aria-hidden` below `lg` (64rem), matching the
|
|
269
|
+
// breakpoint where the drawer is display-hidden anyway.
|
|
270
|
+
const drawer = document.querySelector<HTMLElement>(
|
|
271
|
+
"[data-blume-nav-drawer]"
|
|
272
|
+
);
|
|
273
|
+
if (drawer) {
|
|
274
|
+
const desktop = window.matchMedia("(min-width: 64rem)");
|
|
275
|
+
const syncDrawer = () => {
|
|
276
|
+
const hidden =
|
|
277
|
+
!desktop.matches &&
|
|
278
|
+
!document.documentElement.hasAttribute("data-blume-nav-open");
|
|
279
|
+
drawer.inert = hidden;
|
|
280
|
+
if (hidden) {
|
|
281
|
+
drawer.setAttribute("aria-hidden", "true");
|
|
282
|
+
} else {
|
|
283
|
+
drawer.removeAttribute("aria-hidden");
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
syncDrawer();
|
|
287
|
+
desktop.addEventListener("change", syncDrawer);
|
|
288
|
+
new MutationObserver(syncDrawer).observe(document.documentElement, {
|
|
289
|
+
attributeFilter: ["data-blume-nav-open"],
|
|
290
|
+
});
|
|
291
|
+
}
|
|
250
292
|
</script>
|
|
251
293
|
</body>
|
|
252
294
|
</html>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { withBase } from "../islands/base-path.ts";
|
|
2
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
3
4
|
import type { FlatPage } from "./nav-utils.ts";
|
|
4
5
|
import Icon from "../Icon.astro";
|
|
@@ -24,7 +25,7 @@ const { prev, next, strings } = Astro.props;
|
|
|
24
25
|
{prev ? (
|
|
25
26
|
<a
|
|
26
27
|
class="flex max-w-[48%] flex-1 items-center gap-2 rounded-full border border-border px-4 py-3 text-foreground transition-colors hover:border-foreground max-md:max-w-full"
|
|
27
|
-
href={prev.route}
|
|
28
|
+
href={withBase(prev.route)}
|
|
28
29
|
>
|
|
29
30
|
<Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
|
|
30
31
|
<span>
|
|
@@ -40,7 +41,7 @@ const { prev, next, strings } = Astro.props;
|
|
|
40
41
|
{next && (
|
|
41
42
|
<a
|
|
42
43
|
class="ms-auto flex max-w-[48%] flex-1 items-center justify-end gap-2 rounded-full border border-border px-4 py-3 text-end text-foreground transition-colors hover:border-foreground max-md:max-w-full"
|
|
43
|
-
href={next.route}
|
|
44
|
+
href={withBase(next.route)}
|
|
44
45
|
>
|
|
45
46
|
<span>
|
|
46
47
|
<span class="block text-muted-foreground text-xs max-md:hidden">
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import "blume:theme";
|
|
3
|
+
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
4
|
+
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
3
5
|
import type { Navigation } from "../../core/types.ts";
|
|
4
6
|
import Analytics from "./Analytics.astro";
|
|
5
7
|
import Banner from "./Banner.astro";
|
|
@@ -50,6 +52,8 @@ interface Props {
|
|
|
50
52
|
fontCssVars?: string[];
|
|
51
53
|
searchEnabled: boolean;
|
|
52
54
|
pageTitle: string;
|
|
55
|
+
/** Resolved UI dictionary; English baseline when omitted. */
|
|
56
|
+
ui?: UIStrings;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
const {
|
|
@@ -65,8 +69,11 @@ const {
|
|
|
65
69
|
fontCssVars,
|
|
66
70
|
searchEnabled,
|
|
67
71
|
pageTitle,
|
|
72
|
+
ui,
|
|
68
73
|
} = Astro.props;
|
|
69
74
|
|
|
75
|
+
const strings = ui ?? EN_UI;
|
|
76
|
+
|
|
70
77
|
// Set the theme before paint so the navbar never flashes the wrong colors
|
|
71
78
|
// (mirrors RootLayout's pre-paint script).
|
|
72
79
|
const initialThemeScript = `(()=>{const m=${JSON.stringify(themeMode)};const s=localStorage.getItem("blume-theme");const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.dataset.theme=s??(m==="system"?sys:m);})();`;
|
|
@@ -90,7 +97,7 @@ const bannerScript = banner?.dismissible
|
|
|
90
97
|
<Analytics analytics={analytics} />
|
|
91
98
|
</head>
|
|
92
99
|
<body class="bg-background font-sans text-foreground antialiased">
|
|
93
|
-
<Banner banner={banner} />
|
|
100
|
+
<Banner banner={banner} strings={strings.banner} />
|
|
94
101
|
<Header
|
|
95
102
|
hasDrawer={false}
|
|
96
103
|
hasSidebar={false}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
import "blume:theme";
|
|
11
11
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
12
12
|
import { buildStructuredData } from "../../seo/jsonld.ts";
|
|
13
|
+
import { withBase } from "../islands/base-path.ts";
|
|
13
14
|
import Analytics from "./Analytics.astro";
|
|
14
15
|
import Banner from "./Banner.astro";
|
|
15
16
|
import Breadcrumbs from "./Breadcrumbs.astro";
|
|
@@ -205,6 +206,12 @@ const PageFooterSlot = resolveSlot(layout.PageFooter, Empty);
|
|
|
205
206
|
const FeedbackSlot = resolveSlot(layout.Feedback, PageFeedback);
|
|
206
207
|
|
|
207
208
|
const strings = ui ?? EN_UI;
|
|
209
|
+
// Merge the groups this layout reads directly over the English defaults so a
|
|
210
|
+
// key missing from a translation (or from a not-yet-regenerated snapshot)
|
|
211
|
+
// still renders instead of coming out blank — the PageActions pattern.
|
|
212
|
+
const navStrings = { ...EN_UI.nav, ...strings.nav };
|
|
213
|
+
const actionStrings = { ...EN_UI.actions, ...strings.actions };
|
|
214
|
+
const contentStrings = { ...EN_UI.content, ...strings.content };
|
|
208
215
|
// Filter search to the active language only when the site is multi-locale.
|
|
209
216
|
const searchLocale =
|
|
210
217
|
localeSwitch && localeSwitch.length > 1 ? locale : undefined;
|
|
@@ -250,7 +257,8 @@ const formattedLastModified =
|
|
|
250
257
|
|
|
251
258
|
// The hosted MCP server's absolute URL, used by the page-actions install menu.
|
|
252
259
|
// Needs a configured site to be useful, so the menu is hidden without one.
|
|
253
|
-
const mcpUrl =
|
|
260
|
+
const mcpUrl =
|
|
261
|
+
mcp && siteUrl ? new URL(withBase(mcp.route), siteUrl).href : null;
|
|
254
262
|
|
|
255
263
|
// Scope the sidebar (and the breadcrumbs/pagination derived from it) to the
|
|
256
264
|
// active tab's section, so a multi-section site drills each tab into its own
|
|
@@ -266,6 +274,7 @@ const structuredData =
|
|
|
266
274
|
structuredDataEnabled === false || noindex
|
|
267
275
|
? null
|
|
268
276
|
: buildStructuredData({
|
|
277
|
+
base: import.meta.env.BASE_URL,
|
|
269
278
|
breadcrumbs: crumbs,
|
|
270
279
|
description,
|
|
271
280
|
locale,
|
|
@@ -318,7 +327,7 @@ const bannerScript = banner?.dismissible
|
|
|
318
327
|
{
|
|
319
328
|
feeds?.map((feed) => (
|
|
320
329
|
<link
|
|
321
|
-
href={feed.href}
|
|
330
|
+
href={withBase(feed.href)}
|
|
322
331
|
rel="alternate"
|
|
323
332
|
title={feed.title}
|
|
324
333
|
type="application/rss+xml"
|
|
@@ -342,12 +351,14 @@ const bannerScript = banner?.dismissible
|
|
|
342
351
|
class="bg-background font-sans text-foreground antialiased"
|
|
343
352
|
data-blume-code-wrap={codeWrap ? "" : undefined}
|
|
344
353
|
data-blume-image-zoom={imageZoom ? "" : undefined}
|
|
354
|
+
data-i18n-copy-code={actionStrings.copyCode}
|
|
355
|
+
data-i18n-diagram-error={contentStrings.diagramError}
|
|
345
356
|
>
|
|
346
357
|
<a
|
|
347
358
|
class="absolute start-[-999px] top-0 z-[100] bg-accent px-4 py-2 text-accent-foreground focus:start-0"
|
|
348
359
|
href="#blume-content">{strings.page.skipToContent}</a
|
|
349
360
|
>
|
|
350
|
-
<Banner banner={banner} />
|
|
361
|
+
<Banner banner={banner} strings={strings.banner} />
|
|
351
362
|
<HeaderSlot
|
|
352
363
|
askEnabled={askEnabled}
|
|
353
364
|
layout={layout}
|
|
@@ -357,6 +368,7 @@ const bannerScript = banner?.dismissible
|
|
|
357
368
|
route={page.route}
|
|
358
369
|
searchEnabled={searchEnabled}
|
|
359
370
|
searchLocale={searchLocale}
|
|
371
|
+
navStrings={navStrings}
|
|
360
372
|
searchStrings={strings.search}
|
|
361
373
|
site={site}
|
|
362
374
|
switcherStrings={strings.languageSwitcher}
|
|
@@ -368,7 +380,8 @@ const bannerScript = banner?.dismissible
|
|
|
368
380
|
data-blume-doc-grid
|
|
369
381
|
>
|
|
370
382
|
<aside
|
|
371
|
-
aria-label=
|
|
383
|
+
aria-label={navStrings.primary}
|
|
384
|
+
data-blume-nav-drawer
|
|
372
385
|
class:list={[
|
|
373
386
|
"fixed top-[var(--blume-drawer-top,4rem)] start-0 z-[35] h-[calc(100dvh-var(--blume-drawer-top,4rem))] w-64 max-w-[80vw] -translate-x-[105%] overflow-y-auto border-border border-e bg-background px-5 pt-4 pb-6 transition-transform rtl:translate-x-[105%] [:where([data-blume-nav-open])_&]:translate-x-0! lg:sticky lg:top-16 lg:z-auto lg:h-[calc(100dvh-4rem)] lg:w-auto lg:max-w-none lg:translate-x-0! lg:border-e-0 lg:bg-transparent lg:px-4",
|
|
374
387
|
// A "bare" landing (the changelog index) has no sidebar column on
|
|
@@ -382,7 +395,10 @@ const bannerScript = banner?.dismissible
|
|
|
382
395
|
// all breakpoints, outside the tab-scoped sidebar so they never change
|
|
383
396
|
// with the active tab. External hrefs open in a new tab.
|
|
384
397
|
navigation.featured.length > 0 && (
|
|
385
|
-
<nav
|
|
398
|
+
<nav
|
|
399
|
+
aria-label={navStrings.featured}
|
|
400
|
+
class="mb-4 border-border border-b pb-4"
|
|
401
|
+
>
|
|
386
402
|
<ul class="m-0 list-none p-0">
|
|
387
403
|
{navigation.featured.map((link) => {
|
|
388
404
|
const external = /^https?:\/\//u.test(link.href);
|
|
@@ -390,7 +406,7 @@ const bannerScript = banner?.dismissible
|
|
|
390
406
|
<li>
|
|
391
407
|
<a
|
|
392
408
|
class="block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground"
|
|
393
|
-
href={link.href}
|
|
409
|
+
href={withBase(link.href)}
|
|
394
410
|
rel={external ? "noreferrer" : undefined}
|
|
395
411
|
target={external ? "_blank" : undefined}
|
|
396
412
|
>
|
|
@@ -428,7 +444,7 @@ const bannerScript = banner?.dismissible
|
|
|
428
444
|
// from opening blank.
|
|
429
445
|
navigation.tabs.length > 0 && (
|
|
430
446
|
<nav
|
|
431
|
-
aria-label=
|
|
447
|
+
aria-label={navStrings.sections}
|
|
432
448
|
class:list={[
|
|
433
449
|
"mb-4 border-border border-b pb-4",
|
|
434
450
|
sidebar.length > 0 && "md:hidden",
|
|
@@ -445,7 +461,7 @@ const bannerScript = banner?.dismissible
|
|
|
445
461
|
: undefined
|
|
446
462
|
}
|
|
447
463
|
class="block rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
|
|
448
|
-
href={tab.path}
|
|
464
|
+
href={withBase(tab.path)}
|
|
449
465
|
>
|
|
450
466
|
{tab.label}
|
|
451
467
|
</a>
|
|
@@ -460,14 +476,26 @@ const bannerScript = banner?.dismissible
|
|
|
460
476
|
MobileNavSlot ? (
|
|
461
477
|
<>
|
|
462
478
|
<div class="lg:hidden">
|
|
463
|
-
<MobileNavSlot
|
|
479
|
+
<MobileNavSlot
|
|
480
|
+
currentRoute={page.route}
|
|
481
|
+
items={sidebar}
|
|
482
|
+
strings={navStrings}
|
|
483
|
+
/>
|
|
464
484
|
</div>
|
|
465
485
|
<div class="hidden lg:block">
|
|
466
|
-
<SidebarSlot
|
|
486
|
+
<SidebarSlot
|
|
487
|
+
currentRoute={page.route}
|
|
488
|
+
items={sidebar}
|
|
489
|
+
strings={navStrings}
|
|
490
|
+
/>
|
|
467
491
|
</div>
|
|
468
492
|
</>
|
|
469
493
|
) : (
|
|
470
|
-
<SidebarSlot
|
|
494
|
+
<SidebarSlot
|
|
495
|
+
currentRoute={page.route}
|
|
496
|
+
items={sidebar}
|
|
497
|
+
strings={navStrings}
|
|
498
|
+
/>
|
|
471
499
|
)
|
|
472
500
|
}
|
|
473
501
|
</nav>
|
|
@@ -539,7 +567,7 @@ const bannerScript = banner?.dismissible
|
|
|
539
567
|
)
|
|
540
568
|
}
|
|
541
569
|
<button
|
|
542
|
-
aria-label=
|
|
570
|
+
aria-label={navStrings.closeNavigation}
|
|
543
571
|
class="fixed inset-0 z-[30] hidden cursor-pointer border-0 bg-black/40 [:where([data-blume-nav-open])_&]:block lg:hidden"
|
|
544
572
|
data-blume-nav-toggle
|
|
545
573
|
type="button"
|
|
@@ -556,12 +584,45 @@ const bannerScript = banner?.dismissible
|
|
|
556
584
|
// Tree-shaken out of production builds.
|
|
557
585
|
import "./hydration-hint.ts";
|
|
558
586
|
|
|
587
|
+
// The closed mobile drawer is only translated off-canvas, so its links
|
|
588
|
+
// would stay in the tab order on every page. Mirror the header's
|
|
589
|
+
// `data-blume-nav-open` toggle into `inert`/`aria-hidden` — but only
|
|
590
|
+
// below `lg` (64rem), where the same element isn't the static sidebar.
|
|
591
|
+
const drawer = document.querySelector<HTMLElement>(
|
|
592
|
+
"[data-blume-nav-drawer]"
|
|
593
|
+
);
|
|
594
|
+
if (drawer) {
|
|
595
|
+
const desktop = window.matchMedia("(min-width: 64rem)");
|
|
596
|
+
const syncDrawer = () => {
|
|
597
|
+
const hidden =
|
|
598
|
+
!desktop.matches &&
|
|
599
|
+
!document.documentElement.hasAttribute("data-blume-nav-open");
|
|
600
|
+
drawer.inert = hidden;
|
|
601
|
+
if (hidden) {
|
|
602
|
+
drawer.setAttribute("aria-hidden", "true");
|
|
603
|
+
} else {
|
|
604
|
+
drawer.removeAttribute("aria-hidden");
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
syncDrawer();
|
|
608
|
+
desktop.addEventListener("change", syncDrawer);
|
|
609
|
+
new MutationObserver(syncDrawer).observe(document.documentElement, {
|
|
610
|
+
attributeFilter: ["data-blume-nav-open"],
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
559
614
|
const svg = (name: string) =>
|
|
560
615
|
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${icons[name]}</svg>`;
|
|
561
616
|
|
|
562
617
|
const buttonClass =
|
|
563
618
|
"absolute right-3 inline-flex size-7 items-center justify-center rounded-full bg-transparent text-muted-foreground opacity-70 transition hover:bg-muted hover:text-foreground hover:opacity-100 focus-visible:opacity-100";
|
|
564
619
|
|
|
620
|
+
// Localized copy-button label, stamped on <body> by the layout markup
|
|
621
|
+
// (the Search.astro data-attribute channel) since this bundled script
|
|
622
|
+
// can't interpolate server values directly.
|
|
623
|
+
const copyCodeLabel =
|
|
624
|
+
document.body.getAttribute("data-i18n-copy-code") || "Copy code";
|
|
625
|
+
|
|
565
626
|
const languageLabels: Record<string, string> = {
|
|
566
627
|
astro: "Astro",
|
|
567
628
|
bash: "Bash",
|
|
@@ -609,7 +670,7 @@ const bannerScript = banner?.dismissible
|
|
|
609
670
|
: "top-2";
|
|
610
671
|
button.className = `${buttonClass} ${topClass}`;
|
|
611
672
|
button.setAttribute("data-blume-copy", "");
|
|
612
|
-
button.setAttribute("aria-label",
|
|
673
|
+
button.setAttribute("aria-label", copyCodeLabel);
|
|
613
674
|
button.innerHTML = svg("copy");
|
|
614
675
|
button.addEventListener("click", async () => {
|
|
615
676
|
const code = pre.querySelector("code");
|