blume 0.7.0 → 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 +1487 -360
- package/dist/cli/index.js.map +91 -85
- 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 +9 -0
- package/dist/types/core/config-input.d.ts +36 -2
- package/dist/types/core/config.d.ts +3 -2
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/i18n-ui.d.ts +476 -132
- package/dist/types/core/schema.d.ts +216 -145
- 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 +8 -8
- 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 +27 -3
- package/docs/configuration/seo.mdx +2 -2
- package/docs/content/components.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +4 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +8 -6
- package/package.json +14 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +9 -8
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +135 -26
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +25 -4
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +21 -7
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +11 -1
- package/src/astro/generate.ts +76 -45
- package/src/astro/integration.ts +1 -1
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/pages.ts +81 -19
- package/src/astro/templates.ts +99 -12
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +99 -19
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +26 -5
- 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 +43 -2
- package/src/cli/dev-lock.ts +8 -4
- 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/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/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 +7 -1
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/mermaid-element.ts +7 -2
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +56 -6
- package/src/components/islands/hooks.ts +28 -8
- package/src/components/layout/Banner.astro +10 -2
- package/src/components/layout/Header.astro +13 -4
- package/src/components/layout/Logo.astro +11 -3
- package/src/components/layout/NavTree.astro +17 -3
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageLayout.astro +45 -8
- package/src/components/layout/ReferenceLayout.astro +8 -1
- package/src/components/layout/RootLayout.astro +67 -9
- package/src/components/layout/Search.astro +94 -22
- 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/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +1 -1
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +1 -1
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/core/base-path.ts +24 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/config-input.ts +37 -2
- package/src/core/config.ts +3 -2
- package/src/core/data.ts +2 -0
- package/src/core/graph.ts +15 -5
- package/src/core/i18n-ui.ts +45 -0
- package/src/core/last-modified.ts +13 -6
- package/src/core/links.ts +32 -8
- package/src/core/navigation.ts +29 -4
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +15 -6
- package/src/core/schema.ts +36 -2
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/mdx-remote.ts +23 -14
- package/src/core/sources/normalize.ts +152 -50
- package/src/core/sources/notion.ts +8 -8
- 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 +7 -2
- package/src/deploy/sitemap.ts +53 -11
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +10 -8
- package/src/markdown/index.ts +15 -3
- package/src/markdown/inline-code.ts +7 -2
- package/src/markdown/package-commands.ts +10 -4
- package/src/openapi/model.ts +12 -4
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +38 -8
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +184 -12
- package/src/registry/registry.ts +0 -3
- package/src/search/documents.ts +34 -2
- package/src/seo/jsonld.ts +13 -12
|
@@ -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
|
});
|
|
@@ -110,6 +110,10 @@ const clientDataJson = clientData
|
|
|
110
110
|
: null;
|
|
111
111
|
|
|
112
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 };
|
|
113
117
|
// Filter search to the active language only when the site is multi-locale.
|
|
114
118
|
const searchLocale =
|
|
115
119
|
localeSwitch && localeSwitch.length > 1 ? locale : undefined;
|
|
@@ -125,19 +129,23 @@ const ogSlug = route === "/" ? "index" : route.slice(1);
|
|
|
125
129
|
// Absolute URLs carry the deployment base too (`site + base + route`), since the
|
|
126
130
|
// page is served under it. `withBase` handles a root base as a no-op.
|
|
127
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;
|
|
128
136
|
const resolvedCanonical =
|
|
129
137
|
canonical ??
|
|
130
|
-
(
|
|
138
|
+
(siteBase ? `${siteBase}${basedRoute === "/" ? "" : basedRoute}` : null);
|
|
131
139
|
// An explicit `ogImage` wins. A root-relative path (e.g. an image dropped in
|
|
132
140
|
// `public/`) is resolved against the site URL so crawlers get an absolute
|
|
133
141
|
// `og:image`; an already-absolute URL passes through untouched. Otherwise fall
|
|
134
142
|
// back to the generated OG card for this route.
|
|
135
143
|
const absolutizeOgImage = (value: string): string =>
|
|
136
|
-
value.startsWith("/") &&
|
|
144
|
+
value.startsWith("/") && siteBase ? `${siteBase}${withBase(value)}` : value;
|
|
137
145
|
const resolvedOgImage = ogImage
|
|
138
146
|
? absolutizeOgImage(ogImage)
|
|
139
|
-
: ogEnabled &&
|
|
140
|
-
? `${
|
|
147
|
+
: ogEnabled && siteBase
|
|
148
|
+
? `${siteBase}${withBase(`/og/${ogSlug}.png`)}`
|
|
141
149
|
: null;
|
|
142
150
|
|
|
143
151
|
const initialThemeScript = themeInitScript(themeMode);
|
|
@@ -178,12 +186,13 @@ const bannerScript = banner?.dismissible
|
|
|
178
186
|
class="absolute start-[-999px] top-0 z-[100] bg-accent px-4 py-2 text-accent-foreground focus:start-0"
|
|
179
187
|
href="#blume-content">{strings.page.skipToContent}</a
|
|
180
188
|
>
|
|
181
|
-
<Banner banner={banner} />
|
|
189
|
+
<Banner banner={banner} strings={strings.banner} />
|
|
182
190
|
<Header
|
|
183
191
|
askEnabled={askEnabled}
|
|
184
192
|
hasSidebar={false}
|
|
185
193
|
localeSwitch={localeSwitch}
|
|
186
194
|
logo={logo}
|
|
195
|
+
navStrings={navStrings}
|
|
187
196
|
navigation={navigation}
|
|
188
197
|
route={route}
|
|
189
198
|
searchEnabled={searchEnabled}
|
|
@@ -205,10 +214,11 @@ const bannerScript = banner?.dismissible
|
|
|
205
214
|
navigation.tabs.length > 0 && (
|
|
206
215
|
<>
|
|
207
216
|
<aside
|
|
208
|
-
aria-label=
|
|
217
|
+
aria-label={navStrings.navigation}
|
|
209
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
|
|
210
220
|
>
|
|
211
|
-
<nav aria-label=
|
|
221
|
+
<nav aria-label={navStrings.sections}>
|
|
212
222
|
<ul class="m-0 list-none p-0">
|
|
213
223
|
{navigation.tabs.map((tab) => (
|
|
214
224
|
<li>
|
|
@@ -230,7 +240,7 @@ const bannerScript = banner?.dismissible
|
|
|
230
240
|
</nav>
|
|
231
241
|
</aside>
|
|
232
242
|
<button
|
|
233
|
-
aria-label=
|
|
243
|
+
aria-label={navStrings.closeNavigation}
|
|
234
244
|
class="fixed inset-0 z-[30] hidden cursor-pointer border-0 bg-black/40 [:where([data-blume-nav-open])_&]:block lg:hidden"
|
|
235
245
|
data-blume-nav-toggle
|
|
236
246
|
type="button"
|
|
@@ -252,6 +262,33 @@ const bannerScript = banner?.dismissible
|
|
|
252
262
|
// Dev-only: friendly hint after a React island hydration mismatch;
|
|
253
263
|
// tree-shaken out of production builds.
|
|
254
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
|
+
}
|
|
255
292
|
</script>
|
|
256
293
|
</body>
|
|
257
294
|
</html>
|
|
@@ -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}
|
|
@@ -206,6 +206,12 @@ const PageFooterSlot = resolveSlot(layout.PageFooter, Empty);
|
|
|
206
206
|
const FeedbackSlot = resolveSlot(layout.Feedback, PageFeedback);
|
|
207
207
|
|
|
208
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 };
|
|
209
215
|
// Filter search to the active language only when the site is multi-locale.
|
|
210
216
|
const searchLocale =
|
|
211
217
|
localeSwitch && localeSwitch.length > 1 ? locale : undefined;
|
|
@@ -345,12 +351,14 @@ const bannerScript = banner?.dismissible
|
|
|
345
351
|
class="bg-background font-sans text-foreground antialiased"
|
|
346
352
|
data-blume-code-wrap={codeWrap ? "" : undefined}
|
|
347
353
|
data-blume-image-zoom={imageZoom ? "" : undefined}
|
|
354
|
+
data-i18n-copy-code={actionStrings.copyCode}
|
|
355
|
+
data-i18n-diagram-error={contentStrings.diagramError}
|
|
348
356
|
>
|
|
349
357
|
<a
|
|
350
358
|
class="absolute start-[-999px] top-0 z-[100] bg-accent px-4 py-2 text-accent-foreground focus:start-0"
|
|
351
359
|
href="#blume-content">{strings.page.skipToContent}</a
|
|
352
360
|
>
|
|
353
|
-
<Banner banner={banner} />
|
|
361
|
+
<Banner banner={banner} strings={strings.banner} />
|
|
354
362
|
<HeaderSlot
|
|
355
363
|
askEnabled={askEnabled}
|
|
356
364
|
layout={layout}
|
|
@@ -360,6 +368,7 @@ const bannerScript = banner?.dismissible
|
|
|
360
368
|
route={page.route}
|
|
361
369
|
searchEnabled={searchEnabled}
|
|
362
370
|
searchLocale={searchLocale}
|
|
371
|
+
navStrings={navStrings}
|
|
363
372
|
searchStrings={strings.search}
|
|
364
373
|
site={site}
|
|
365
374
|
switcherStrings={strings.languageSwitcher}
|
|
@@ -371,7 +380,8 @@ const bannerScript = banner?.dismissible
|
|
|
371
380
|
data-blume-doc-grid
|
|
372
381
|
>
|
|
373
382
|
<aside
|
|
374
|
-
aria-label=
|
|
383
|
+
aria-label={navStrings.primary}
|
|
384
|
+
data-blume-nav-drawer
|
|
375
385
|
class:list={[
|
|
376
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",
|
|
377
387
|
// A "bare" landing (the changelog index) has no sidebar column on
|
|
@@ -385,7 +395,10 @@ const bannerScript = banner?.dismissible
|
|
|
385
395
|
// all breakpoints, outside the tab-scoped sidebar so they never change
|
|
386
396
|
// with the active tab. External hrefs open in a new tab.
|
|
387
397
|
navigation.featured.length > 0 && (
|
|
388
|
-
<nav
|
|
398
|
+
<nav
|
|
399
|
+
aria-label={navStrings.featured}
|
|
400
|
+
class="mb-4 border-border border-b pb-4"
|
|
401
|
+
>
|
|
389
402
|
<ul class="m-0 list-none p-0">
|
|
390
403
|
{navigation.featured.map((link) => {
|
|
391
404
|
const external = /^https?:\/\//u.test(link.href);
|
|
@@ -431,7 +444,7 @@ const bannerScript = banner?.dismissible
|
|
|
431
444
|
// from opening blank.
|
|
432
445
|
navigation.tabs.length > 0 && (
|
|
433
446
|
<nav
|
|
434
|
-
aria-label=
|
|
447
|
+
aria-label={navStrings.sections}
|
|
435
448
|
class:list={[
|
|
436
449
|
"mb-4 border-border border-b pb-4",
|
|
437
450
|
sidebar.length > 0 && "md:hidden",
|
|
@@ -463,14 +476,26 @@ const bannerScript = banner?.dismissible
|
|
|
463
476
|
MobileNavSlot ? (
|
|
464
477
|
<>
|
|
465
478
|
<div class="lg:hidden">
|
|
466
|
-
<MobileNavSlot
|
|
479
|
+
<MobileNavSlot
|
|
480
|
+
currentRoute={page.route}
|
|
481
|
+
items={sidebar}
|
|
482
|
+
strings={navStrings}
|
|
483
|
+
/>
|
|
467
484
|
</div>
|
|
468
485
|
<div class="hidden lg:block">
|
|
469
|
-
<SidebarSlot
|
|
486
|
+
<SidebarSlot
|
|
487
|
+
currentRoute={page.route}
|
|
488
|
+
items={sidebar}
|
|
489
|
+
strings={navStrings}
|
|
490
|
+
/>
|
|
470
491
|
</div>
|
|
471
492
|
</>
|
|
472
493
|
) : (
|
|
473
|
-
<SidebarSlot
|
|
494
|
+
<SidebarSlot
|
|
495
|
+
currentRoute={page.route}
|
|
496
|
+
items={sidebar}
|
|
497
|
+
strings={navStrings}
|
|
498
|
+
/>
|
|
474
499
|
)
|
|
475
500
|
}
|
|
476
501
|
</nav>
|
|
@@ -542,7 +567,7 @@ const bannerScript = banner?.dismissible
|
|
|
542
567
|
)
|
|
543
568
|
}
|
|
544
569
|
<button
|
|
545
|
-
aria-label=
|
|
570
|
+
aria-label={navStrings.closeNavigation}
|
|
546
571
|
class="fixed inset-0 z-[30] hidden cursor-pointer border-0 bg-black/40 [:where([data-blume-nav-open])_&]:block lg:hidden"
|
|
547
572
|
data-blume-nav-toggle
|
|
548
573
|
type="button"
|
|
@@ -559,12 +584,45 @@ const bannerScript = banner?.dismissible
|
|
|
559
584
|
// Tree-shaken out of production builds.
|
|
560
585
|
import "./hydration-hint.ts";
|
|
561
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
|
+
|
|
562
614
|
const svg = (name: string) =>
|
|
563
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>`;
|
|
564
616
|
|
|
565
617
|
const buttonClass =
|
|
566
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";
|
|
567
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
|
+
|
|
568
626
|
const languageLabels: Record<string, string> = {
|
|
569
627
|
astro: "Astro",
|
|
570
628
|
bash: "Bash",
|
|
@@ -612,7 +670,7 @@ const bannerScript = banner?.dismissible
|
|
|
612
670
|
: "top-2";
|
|
613
671
|
button.className = `${buttonClass} ${topClass}`;
|
|
614
672
|
button.setAttribute("data-blume-copy", "");
|
|
615
|
-
button.setAttribute("aria-label",
|
|
673
|
+
button.setAttribute("aria-label", copyCodeLabel);
|
|
616
674
|
button.innerHTML = svg("copy");
|
|
617
675
|
button.addEventListener("click", async () => {
|
|
618
676
|
const code = pre.querySelector("code");
|
|
@@ -33,8 +33,13 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
33
33
|
<blume-search
|
|
34
34
|
class="contents"
|
|
35
35
|
data-ask={askEnabled ? "" : undefined}
|
|
36
|
+
data-i18n-ask={s.askAi}
|
|
37
|
+
data-i18n-ask-hint={s.askAiHint}
|
|
36
38
|
data-i18n-dev={s.devOnly}
|
|
37
39
|
data-i18n-empty={s.noResults}
|
|
40
|
+
data-i18n-error={s.error}
|
|
41
|
+
data-i18n-popular={s.popular}
|
|
42
|
+
data-i18n-results={s.results}
|
|
38
43
|
data-locale={locale || undefined}
|
|
39
44
|
>
|
|
40
45
|
<button
|
|
@@ -62,7 +67,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
62
67
|
<input
|
|
63
68
|
aria-label={s.label}
|
|
64
69
|
autocomplete="off"
|
|
65
|
-
class="flex-1 border-0 bg-transparent text-foreground text-sm focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
|
|
70
|
+
class="flex-1 border-0 bg-transparent text-foreground text-sm pointer-coarse:text-base focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
|
|
66
71
|
data-blume-search-input
|
|
67
72
|
placeholder={s.placeholder}
|
|
68
73
|
type="search"
|
|
@@ -119,15 +124,17 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
119
124
|
<span class="flex items-center gap-1">
|
|
120
125
|
<kbd class={`${kbd} text-[0.65rem]`}>↑</kbd>
|
|
121
126
|
<kbd class={`${kbd} text-[0.65rem]`}>↓</kbd>
|
|
122
|
-
navigate
|
|
127
|
+
{s.navigate}
|
|
123
128
|
</span>
|
|
124
129
|
<span class="flex items-center gap-1">
|
|
125
130
|
<kbd class={`${kbd} text-[0.65rem]`}>↵</kbd>
|
|
126
|
-
open
|
|
131
|
+
{s.open}
|
|
127
132
|
</span>
|
|
128
133
|
<span class="flex items-center gap-1 max-md:hidden">
|
|
129
|
-
<kbd class={`${kbd} text-[0.65rem]`}
|
|
130
|
-
|
|
134
|
+
<kbd class={`${kbd} text-[0.65rem]`} data-blume-search-preview-kbd
|
|
135
|
+
>⌘J</kbd
|
|
136
|
+
>
|
|
137
|
+
{s.preview}
|
|
131
138
|
</span>
|
|
132
139
|
</div>
|
|
133
140
|
</div>
|
|
@@ -189,6 +196,9 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
189
196
|
preview!: HTMLElement;
|
|
190
197
|
searchFn: SearchFn | null = null;
|
|
191
198
|
loaded = false;
|
|
199
|
+
loadFailed = false;
|
|
200
|
+
/** First-open client/index load still in flight. */
|
|
201
|
+
loading = false;
|
|
192
202
|
askEnabled = false;
|
|
193
203
|
popular: PopularPage[] = [];
|
|
194
204
|
selectables: Selectable[] = [];
|
|
@@ -198,6 +208,11 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
198
208
|
previewOn = true;
|
|
199
209
|
devOnlyMsg = "Search is available in the production build.";
|
|
200
210
|
noResultsMsg = "No results found.";
|
|
211
|
+
errorMsg = "Something went wrong. Please try again.";
|
|
212
|
+
askMsg = "Ask AI";
|
|
213
|
+
askHintMsg = "Get an instant answer from AI";
|
|
214
|
+
popularMsg = "Popular";
|
|
215
|
+
resultsMsg = "Results";
|
|
201
216
|
// The active locale to filter to (null when i18n is off), and whether the
|
|
202
217
|
// reader has opted to search across every language instead.
|
|
203
218
|
locale: string | null = null;
|
|
@@ -208,6 +223,14 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
208
223
|
this.getAttribute("data-i18n-dev") || this.devOnlyMsg;
|
|
209
224
|
this.noResultsMsg =
|
|
210
225
|
this.getAttribute("data-i18n-empty") || this.noResultsMsg;
|
|
226
|
+
this.errorMsg = this.getAttribute("data-i18n-error") || this.errorMsg;
|
|
227
|
+
this.askMsg = this.getAttribute("data-i18n-ask") || this.askMsg;
|
|
228
|
+
this.askHintMsg =
|
|
229
|
+
this.getAttribute("data-i18n-ask-hint") || this.askHintMsg;
|
|
230
|
+
this.popularMsg =
|
|
231
|
+
this.getAttribute("data-i18n-popular") || this.popularMsg;
|
|
232
|
+
this.resultsMsg =
|
|
233
|
+
this.getAttribute("data-i18n-results") || this.resultsMsg;
|
|
211
234
|
this.locale = this.getAttribute("data-locale");
|
|
212
235
|
this.dialog = this.querySelector("[data-blume-search-dialog]")!;
|
|
213
236
|
this.input = this.querySelector("[data-blume-search-input]")!;
|
|
@@ -250,12 +273,18 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
250
273
|
});
|
|
251
274
|
}
|
|
252
275
|
|
|
253
|
-
// The
|
|
276
|
+
// The handlers accept both ⌘ and Ctrl chords; show the right modifier
|
|
277
|
+
// per platform on the button hint and the footer's preview hint.
|
|
278
|
+
const isApple = /mac|iphone|ipad|ipod/iu.test(navigator.platform);
|
|
254
279
|
const hint = this.querySelector("[data-blume-search-kbd]");
|
|
255
280
|
if (hint) {
|
|
256
|
-
hint.textContent =
|
|
257
|
-
|
|
258
|
-
|
|
281
|
+
hint.textContent = isApple ? "⌘K" : "Ctrl K";
|
|
282
|
+
}
|
|
283
|
+
const previewHint = this.querySelector(
|
|
284
|
+
"[data-blume-search-preview-kbd]"
|
|
285
|
+
);
|
|
286
|
+
if (previewHint) {
|
|
287
|
+
previewHint.textContent = isApple ? "⌘J" : "Ctrl J";
|
|
259
288
|
}
|
|
260
289
|
|
|
261
290
|
this.querySelector("[data-blume-search-open]")?.addEventListener(
|
|
@@ -301,6 +330,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
301
330
|
this.input.focus();
|
|
302
331
|
this.input.select();
|
|
303
332
|
if (!this.loaded) {
|
|
333
|
+
this.loading = true;
|
|
304
334
|
try {
|
|
305
335
|
const { createSearch } = await import("blume:search-client");
|
|
306
336
|
this.searchFn = await createSearch();
|
|
@@ -308,8 +338,15 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
308
338
|
// fetching the index) must retry on the next open, not disable
|
|
309
339
|
// search until a full page reload.
|
|
310
340
|
this.loaded = true;
|
|
341
|
+
this.loadFailed = false;
|
|
311
342
|
} catch {
|
|
312
343
|
this.searchFn = null;
|
|
344
|
+
// In dev a client can be missing by design (Pagefind's bundle
|
|
345
|
+
// only exists in the production build) — that's the "dev only"
|
|
346
|
+
// hint. The same failure in production is a real error.
|
|
347
|
+
this.loadFailed = !import.meta.env.DEV;
|
|
348
|
+
} finally {
|
|
349
|
+
this.loading = false;
|
|
313
350
|
}
|
|
314
351
|
}
|
|
315
352
|
this.render();
|
|
@@ -322,7 +359,9 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
322
359
|
} else if (event.key === "ArrowUp") {
|
|
323
360
|
event.preventDefault();
|
|
324
361
|
this.move(-1);
|
|
325
|
-
} else if (event.key === "Enter") {
|
|
362
|
+
} else if (event.key === "Enter" && !event.isComposing) {
|
|
363
|
+
// `isComposing` guards IME input: Enter confirming a CJK conversion
|
|
364
|
+
// must commit the text, not activate the selected result.
|
|
326
365
|
const item = this.selectables[this.selectedIndex];
|
|
327
366
|
if (item) {
|
|
328
367
|
event.preventDefault();
|
|
@@ -361,16 +400,36 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
361
400
|
if (!this.searchFn) {
|
|
362
401
|
this.renderFilters([]);
|
|
363
402
|
this.clearPreview();
|
|
364
|
-
|
|
403
|
+
if (this.loading) {
|
|
404
|
+
// Typing while the first-open load (client import + index fetch)
|
|
405
|
+
// is still in flight: the dev-only hint would be wrong in
|
|
406
|
+
// production and the error message premature. Show a neutral
|
|
407
|
+
// placeholder; `open()` re-renders once the load settles.
|
|
408
|
+
this.setMessage("…");
|
|
409
|
+
} else {
|
|
410
|
+
this.setMessage(this.loadFailed ? this.errorMsg : this.devOnlyMsg);
|
|
411
|
+
}
|
|
365
412
|
return;
|
|
366
413
|
}
|
|
367
414
|
|
|
368
415
|
const localeFilter =
|
|
369
416
|
this.locale && !this.allLocales ? this.locale : undefined;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
417
|
+
let result: Awaited<ReturnType<SearchFn>>;
|
|
418
|
+
try {
|
|
419
|
+
result = await this.searchFn(query, {
|
|
420
|
+
locale: localeFilter,
|
|
421
|
+
section: this.activeSection ?? undefined,
|
|
422
|
+
});
|
|
423
|
+
} catch {
|
|
424
|
+
// A hosted provider can reject (network error, outage); the results
|
|
425
|
+
// list is already cleared, so show a message instead of a blank pane.
|
|
426
|
+
if (generation === this.renderGeneration) {
|
|
427
|
+
this.renderFilters([]);
|
|
428
|
+
this.clearPreview();
|
|
429
|
+
this.setMessage(this.errorMsg);
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
374
433
|
// Any newer render — a keystroke, a section pill, a locale toggle —
|
|
375
434
|
// supersedes this one mid-await, even for the same query text;
|
|
376
435
|
// appending the stale hits would duplicate rows and desync selection.
|
|
@@ -378,17 +437,30 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
378
437
|
return;
|
|
379
438
|
}
|
|
380
439
|
|
|
440
|
+
// A section picked for an earlier query can be missing from the new
|
|
441
|
+
// pool — and when the pool has fewer than two sections the pills that
|
|
442
|
+
// would clear it are hidden too, so the stale filter would silently
|
|
443
|
+
// empty the results. Drop it and search again unfiltered.
|
|
444
|
+
if (
|
|
445
|
+
this.activeSection &&
|
|
446
|
+
!result.sections.some((s) => s.label === this.activeSection)
|
|
447
|
+
) {
|
|
448
|
+
this.activeSection = null;
|
|
449
|
+
this.render();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
381
453
|
this.renderFilters(result.sections);
|
|
382
454
|
|
|
383
455
|
if (this.askEnabled) {
|
|
384
|
-
const group = this.addGroup(
|
|
456
|
+
const group = this.addGroup(this.askMsg);
|
|
385
457
|
const ask = this.createAskRow(query);
|
|
386
458
|
group.appendChild(ask.el);
|
|
387
459
|
this.selectables.push(ask);
|
|
388
460
|
}
|
|
389
461
|
|
|
390
462
|
if (result.hits.length > 0) {
|
|
391
|
-
const group = this.addGroup(
|
|
463
|
+
const group = this.addGroup(this.resultsMsg);
|
|
392
464
|
for (const hit of result.hits) {
|
|
393
465
|
const item = this.createHitRow(hit, query);
|
|
394
466
|
group.appendChild(item.el);
|
|
@@ -403,13 +475,13 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
403
475
|
|
|
404
476
|
renderEmpty() {
|
|
405
477
|
if (this.askEnabled) {
|
|
406
|
-
const group = this.addGroup(
|
|
478
|
+
const group = this.addGroup(this.askMsg);
|
|
407
479
|
const ask = this.createAskRow("");
|
|
408
480
|
group.appendChild(ask.el);
|
|
409
481
|
this.selectables.push(ask);
|
|
410
482
|
}
|
|
411
483
|
if (this.popular.length > 0) {
|
|
412
|
-
const group = this.addGroup(
|
|
484
|
+
const group = this.addGroup(this.popularMsg);
|
|
413
485
|
for (const page of this.popular) {
|
|
414
486
|
const item = this.createLinkRow(page.route, page.label);
|
|
415
487
|
group.appendChild(item.el);
|
|
@@ -464,13 +536,13 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
464
536
|
el.type = "button";
|
|
465
537
|
el.className = `${ROW_CLASS} ${MARK}`;
|
|
466
538
|
const title = query
|
|
467
|
-
?
|
|
468
|
-
:
|
|
539
|
+
? `${escapeHtml(this.askMsg)}: <span class="text-muted-foreground">“${escapeHtml(query)}”</span>`
|
|
540
|
+
: escapeHtml(this.askMsg);
|
|
469
541
|
el.innerHTML = `
|
|
470
542
|
<span class="mt-0.5 shrink-0 text-accent">${svg("sparkles")}</span>
|
|
471
543
|
<span class="flex-1">
|
|
472
544
|
<span class="block truncate font-normal text-foreground text-sm">${title}</span>
|
|
473
|
-
<span class="block truncate text-muted-foreground text-sm"
|
|
545
|
+
<span class="block truncate text-muted-foreground text-sm">${escapeHtml(this.askHintMsg)}</span>
|
|
474
546
|
</span>`;
|
|
475
547
|
const item: Selectable = { el, kind: "ask" };
|
|
476
548
|
this.bindRow(item);
|