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
|
@@ -19,7 +19,10 @@ interface Props {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const { askEnabled = false, navigation, strings, locale } = Astro.props;
|
|
22
|
-
|
|
22
|
+
// Merge over the English baseline per key (rather than `strings ?? …`) so a
|
|
23
|
+
// partial — or empty `{}` — strings object still resolves every label to a
|
|
24
|
+
// default, matching the pattern PageActions uses for its own dictionary.
|
|
25
|
+
const s = { ...EN_UI.search, ...strings };
|
|
23
26
|
|
|
24
27
|
// Pages shown in the empty state, before the user has typed anything.
|
|
25
28
|
const popular = navigation ? flattenPages(navigation.sidebar).slice(0, 6) : [];
|
|
@@ -30,8 +33,13 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
30
33
|
<blume-search
|
|
31
34
|
class="contents"
|
|
32
35
|
data-ask={askEnabled ? "" : undefined}
|
|
36
|
+
data-i18n-ask={s.askAi}
|
|
37
|
+
data-i18n-ask-hint={s.askAiHint}
|
|
33
38
|
data-i18n-dev={s.devOnly}
|
|
34
39
|
data-i18n-empty={s.noResults}
|
|
40
|
+
data-i18n-error={s.error}
|
|
41
|
+
data-i18n-popular={s.popular}
|
|
42
|
+
data-i18n-results={s.results}
|
|
35
43
|
data-locale={locale || undefined}
|
|
36
44
|
>
|
|
37
45
|
<button
|
|
@@ -59,7 +67,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
59
67
|
<input
|
|
60
68
|
aria-label={s.label}
|
|
61
69
|
autocomplete="off"
|
|
62
|
-
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"
|
|
63
71
|
data-blume-search-input
|
|
64
72
|
placeholder={s.placeholder}
|
|
65
73
|
type="search"
|
|
@@ -116,15 +124,17 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
116
124
|
<span class="flex items-center gap-1">
|
|
117
125
|
<kbd class={`${kbd} text-[0.65rem]`}>↑</kbd>
|
|
118
126
|
<kbd class={`${kbd} text-[0.65rem]`}>↓</kbd>
|
|
119
|
-
navigate
|
|
127
|
+
{s.navigate}
|
|
120
128
|
</span>
|
|
121
129
|
<span class="flex items-center gap-1">
|
|
122
130
|
<kbd class={`${kbd} text-[0.65rem]`}>↵</kbd>
|
|
123
|
-
open
|
|
131
|
+
{s.open}
|
|
124
132
|
</span>
|
|
125
133
|
<span class="flex items-center gap-1 max-md:hidden">
|
|
126
|
-
<kbd class={`${kbd} text-[0.65rem]`}
|
|
127
|
-
|
|
134
|
+
<kbd class={`${kbd} text-[0.65rem]`} data-blume-search-preview-kbd
|
|
135
|
+
>⌘J</kbd
|
|
136
|
+
>
|
|
137
|
+
{s.preview}
|
|
128
138
|
</span>
|
|
129
139
|
</div>
|
|
130
140
|
</div>
|
|
@@ -139,6 +149,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
139
149
|
|
|
140
150
|
<script>
|
|
141
151
|
import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
|
|
152
|
+
import { prefixBase } from "../islands/base-path.ts";
|
|
142
153
|
import {
|
|
143
154
|
escapeHtml,
|
|
144
155
|
highlight,
|
|
@@ -185,6 +196,9 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
185
196
|
preview!: HTMLElement;
|
|
186
197
|
searchFn: SearchFn | null = null;
|
|
187
198
|
loaded = false;
|
|
199
|
+
loadFailed = false;
|
|
200
|
+
/** First-open client/index load still in flight. */
|
|
201
|
+
loading = false;
|
|
188
202
|
askEnabled = false;
|
|
189
203
|
popular: PopularPage[] = [];
|
|
190
204
|
selectables: Selectable[] = [];
|
|
@@ -194,6 +208,11 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
194
208
|
previewOn = true;
|
|
195
209
|
devOnlyMsg = "Search is available in the production build.";
|
|
196
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";
|
|
197
216
|
// The active locale to filter to (null when i18n is off), and whether the
|
|
198
217
|
// reader has opted to search across every language instead.
|
|
199
218
|
locale: string | null = null;
|
|
@@ -204,6 +223,14 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
204
223
|
this.getAttribute("data-i18n-dev") || this.devOnlyMsg;
|
|
205
224
|
this.noResultsMsg =
|
|
206
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;
|
|
207
234
|
this.locale = this.getAttribute("data-locale");
|
|
208
235
|
this.dialog = this.querySelector("[data-blume-search-dialog]")!;
|
|
209
236
|
this.input = this.querySelector("[data-blume-search-input]")!;
|
|
@@ -246,12 +273,18 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
246
273
|
});
|
|
247
274
|
}
|
|
248
275
|
|
|
249
|
-
// 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);
|
|
250
279
|
const hint = this.querySelector("[data-blume-search-kbd]");
|
|
251
280
|
if (hint) {
|
|
252
|
-
hint.textContent =
|
|
253
|
-
|
|
254
|
-
|
|
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";
|
|
255
288
|
}
|
|
256
289
|
|
|
257
290
|
this.querySelector("[data-blume-search-open]")?.addEventListener(
|
|
@@ -297,6 +330,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
297
330
|
this.input.focus();
|
|
298
331
|
this.input.select();
|
|
299
332
|
if (!this.loaded) {
|
|
333
|
+
this.loading = true;
|
|
300
334
|
try {
|
|
301
335
|
const { createSearch } = await import("blume:search-client");
|
|
302
336
|
this.searchFn = await createSearch();
|
|
@@ -304,8 +338,15 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
304
338
|
// fetching the index) must retry on the next open, not disable
|
|
305
339
|
// search until a full page reload.
|
|
306
340
|
this.loaded = true;
|
|
341
|
+
this.loadFailed = false;
|
|
307
342
|
} catch {
|
|
308
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;
|
|
309
350
|
}
|
|
310
351
|
}
|
|
311
352
|
this.render();
|
|
@@ -318,7 +359,9 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
318
359
|
} else if (event.key === "ArrowUp") {
|
|
319
360
|
event.preventDefault();
|
|
320
361
|
this.move(-1);
|
|
321
|
-
} 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.
|
|
322
365
|
const item = this.selectables[this.selectedIndex];
|
|
323
366
|
if (item) {
|
|
324
367
|
event.preventDefault();
|
|
@@ -357,16 +400,36 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
357
400
|
if (!this.searchFn) {
|
|
358
401
|
this.renderFilters([]);
|
|
359
402
|
this.clearPreview();
|
|
360
|
-
|
|
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
|
+
}
|
|
361
412
|
return;
|
|
362
413
|
}
|
|
363
414
|
|
|
364
415
|
const localeFilter =
|
|
365
416
|
this.locale && !this.allLocales ? this.locale : undefined;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
}
|
|
370
433
|
// Any newer render — a keystroke, a section pill, a locale toggle —
|
|
371
434
|
// supersedes this one mid-await, even for the same query text;
|
|
372
435
|
// appending the stale hits would duplicate rows and desync selection.
|
|
@@ -374,17 +437,30 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
374
437
|
return;
|
|
375
438
|
}
|
|
376
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
|
+
|
|
377
453
|
this.renderFilters(result.sections);
|
|
378
454
|
|
|
379
455
|
if (this.askEnabled) {
|
|
380
|
-
const group = this.addGroup(
|
|
456
|
+
const group = this.addGroup(this.askMsg);
|
|
381
457
|
const ask = this.createAskRow(query);
|
|
382
458
|
group.appendChild(ask.el);
|
|
383
459
|
this.selectables.push(ask);
|
|
384
460
|
}
|
|
385
461
|
|
|
386
462
|
if (result.hits.length > 0) {
|
|
387
|
-
const group = this.addGroup(
|
|
463
|
+
const group = this.addGroup(this.resultsMsg);
|
|
388
464
|
for (const hit of result.hits) {
|
|
389
465
|
const item = this.createHitRow(hit, query);
|
|
390
466
|
group.appendChild(item.el);
|
|
@@ -399,13 +475,13 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
399
475
|
|
|
400
476
|
renderEmpty() {
|
|
401
477
|
if (this.askEnabled) {
|
|
402
|
-
const group = this.addGroup(
|
|
478
|
+
const group = this.addGroup(this.askMsg);
|
|
403
479
|
const ask = this.createAskRow("");
|
|
404
480
|
group.appendChild(ask.el);
|
|
405
481
|
this.selectables.push(ask);
|
|
406
482
|
}
|
|
407
483
|
if (this.popular.length > 0) {
|
|
408
|
-
const group = this.addGroup(
|
|
484
|
+
const group = this.addGroup(this.popularMsg);
|
|
409
485
|
for (const page of this.popular) {
|
|
410
486
|
const item = this.createLinkRow(page.route, page.label);
|
|
411
487
|
group.appendChild(item.el);
|
|
@@ -460,13 +536,13 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
460
536
|
el.type = "button";
|
|
461
537
|
el.className = `${ROW_CLASS} ${MARK}`;
|
|
462
538
|
const title = query
|
|
463
|
-
?
|
|
464
|
-
:
|
|
539
|
+
? `${escapeHtml(this.askMsg)}: <span class="text-muted-foreground">“${escapeHtml(query)}”</span>`
|
|
540
|
+
: escapeHtml(this.askMsg);
|
|
465
541
|
el.innerHTML = `
|
|
466
542
|
<span class="mt-0.5 shrink-0 text-accent">${svg("sparkles")}</span>
|
|
467
543
|
<span class="flex-1">
|
|
468
544
|
<span class="block truncate font-normal text-foreground text-sm">${title}</span>
|
|
469
|
-
<span class="block truncate text-muted-foreground text-sm"
|
|
545
|
+
<span class="block truncate text-muted-foreground text-sm">${escapeHtml(this.askHintMsg)}</span>
|
|
470
546
|
</span>`;
|
|
471
547
|
const item: Selectable = { el, kind: "ask" };
|
|
472
548
|
this.bindRow(item);
|
|
@@ -475,7 +551,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
475
551
|
|
|
476
552
|
createHitRow(hit: SearchHit, query: string): Selectable {
|
|
477
553
|
const el = document.createElement("a");
|
|
478
|
-
|
|
554
|
+
// Index URLs are base-less logical routes; prefix the deployment base so
|
|
555
|
+
// clicking a result lands on the page's real served URL.
|
|
556
|
+
const href = prefixBase(import.meta.env.BASE_URL, hit.url);
|
|
557
|
+
el.href = href;
|
|
479
558
|
el.className = `${ROW_CLASS} ${MARK}`;
|
|
480
559
|
// `line-clamp-2` already sets `display`, so no `block` here (it would
|
|
481
560
|
// override the clamp and let the excerpt run to full height).
|
|
@@ -488,21 +567,22 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
488
567
|
<span class="block truncate font-normal text-foreground text-sm">${hit.title}</span>
|
|
489
568
|
${excerpt}
|
|
490
569
|
</span>`;
|
|
491
|
-
const item: Selectable = { el, hit, kind: "link", url:
|
|
570
|
+
const item: Selectable = { el, hit, kind: "link", url: href };
|
|
492
571
|
this.bindRow(item);
|
|
493
572
|
return item;
|
|
494
573
|
}
|
|
495
574
|
|
|
496
575
|
createLinkRow(url: string, label: string): Selectable {
|
|
497
576
|
const el = document.createElement("a");
|
|
498
|
-
|
|
577
|
+
const href = prefixBase(import.meta.env.BASE_URL, url);
|
|
578
|
+
el.href = href;
|
|
499
579
|
el.className = ROW_CLASS;
|
|
500
580
|
el.innerHTML = `
|
|
501
581
|
<span class="mt-0.5 shrink-0 text-muted-foreground">${svg("file")}</span>
|
|
502
582
|
<span class="flex-1">
|
|
503
583
|
<span class="block truncate font-normal text-foreground text-sm">${escapeHtml(label)}</span>
|
|
504
584
|
</span>`;
|
|
505
|
-
const item: Selectable = { el, kind: "link", url };
|
|
585
|
+
const item: Selectable = { el, kind: "link", url: href };
|
|
506
586
|
this.bindRow(item);
|
|
507
587
|
return item;
|
|
508
588
|
}
|
|
@@ -119,10 +119,14 @@ const sectionChildren = (nodes: NavNode[], base: string): NavNode[] | null => {
|
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
/** Whether a group maps to a header tab (matched on its path or link route). */
|
|
122
|
-
const isTabSection = (node: NavNode, tabPaths: Set<string>): boolean =>
|
|
123
|
-
node.kind
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
const isTabSection = (node: NavNode, tabPaths: Set<string>): boolean => {
|
|
123
|
+
if (node.kind !== "group") {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const byPath = node.path !== undefined && tabPaths.has(node.path);
|
|
127
|
+
const byRoute = node.route !== undefined && tabPaths.has(node.route);
|
|
128
|
+
return byPath || byRoute;
|
|
129
|
+
};
|
|
126
130
|
|
|
127
131
|
/**
|
|
128
132
|
* Drop the groups that already own a header tab from the tree, at any depth —
|
|
@@ -132,9 +136,12 @@ const isTabSection = (node: NavNode, tabPaths: Set<string>): boolean =>
|
|
|
132
136
|
* (`/`) spans everything, so it never removes anything.
|
|
133
137
|
*/
|
|
134
138
|
const withoutTabSections = (nodes: NavNode[], tabs: NavTab[]): NavNode[] => {
|
|
135
|
-
const tabPaths = new Set(
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
const tabPaths = new Set<string>();
|
|
140
|
+
for (const tab of tabs) {
|
|
141
|
+
if (tab.path !== "/") {
|
|
142
|
+
tabPaths.add(tab.path);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
138
145
|
if (tabPaths.size === 0) {
|
|
139
146
|
return nodes;
|
|
140
147
|
}
|
|
@@ -145,11 +152,12 @@ const withoutTabSections = (nodes: NavNode[], tabs: NavTab[]): NavNode[] => {
|
|
|
145
152
|
continue;
|
|
146
153
|
}
|
|
147
154
|
if (item.kind === "group") {
|
|
155
|
+
// A container left empty by pruning is dropped, so no bare heading is
|
|
156
|
+
// stranded.
|
|
148
157
|
const children = prune(item.children);
|
|
149
|
-
if (children.length
|
|
150
|
-
|
|
158
|
+
if (children.length > 0) {
|
|
159
|
+
kept.push({ ...item, children });
|
|
151
160
|
}
|
|
152
|
-
kept.push({ ...item, children });
|
|
153
161
|
} else {
|
|
154
162
|
kept.push(item);
|
|
155
163
|
}
|
|
@@ -20,10 +20,19 @@ export const createSearch = (opts: {
|
|
|
20
20
|
searchApiKey: string;
|
|
21
21
|
}): SearchFn => {
|
|
22
22
|
const client = liteClient(opts.appId, opts.searchApiKey);
|
|
23
|
-
return async (query) => {
|
|
23
|
+
return async (query, options) => {
|
|
24
24
|
const { results } = await client.search<AlgoliaRecord>({
|
|
25
25
|
requests: [
|
|
26
|
-
{
|
|
26
|
+
{
|
|
27
|
+
hitsPerPage: SEARCH_LIMIT,
|
|
28
|
+
indexName: opts.indexName,
|
|
29
|
+
query,
|
|
30
|
+
// The sync uploads `locale` on every record so an i18n site can
|
|
31
|
+
// scope hosted results to the active language.
|
|
32
|
+
...(options?.locale && {
|
|
33
|
+
facetFilters: [`locale:${options.locale}`],
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
27
36
|
],
|
|
28
37
|
});
|
|
29
38
|
const [first] = results;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { SEARCH_LIMIT } from "./types.ts";
|
|
1
|
+
import { highlight, SEARCH_LIMIT } from "./types.ts";
|
|
2
2
|
import type { SearchFn, SearchHit } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Server-proxied search (Mixedbread): POST the query to a generated endpoint
|
|
6
|
-
* that holds the secret key and talks to the service
|
|
7
|
-
*
|
|
6
|
+
* that holds the secret key and talks to the service. The returned hits carry
|
|
7
|
+
* service-derived text and the dialog injects title/excerpt as HTML, so both
|
|
8
|
+
* are escaped (and query matches marked) here, like every other provider.
|
|
8
9
|
*/
|
|
9
10
|
export const createSearch =
|
|
10
11
|
(opts: { api: string }): SearchFn =>
|
|
@@ -17,6 +18,11 @@ export const createSearch =
|
|
|
17
18
|
if (!response.ok) {
|
|
18
19
|
return { hits: [], sections: [] };
|
|
19
20
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
21
|
+
const records = (await response.json()) as SearchHit[];
|
|
22
|
+
const hits = records.slice(0, SEARCH_LIMIT).map((hit) => ({
|
|
23
|
+
...hit,
|
|
24
|
+
excerpt: highlight(hit.excerpt, query),
|
|
25
|
+
title: highlight(hit.title, query),
|
|
26
|
+
}));
|
|
27
|
+
return { hits, sections: [] };
|
|
22
28
|
};
|
|
@@ -22,8 +22,14 @@ export const createSearch = (opts: {
|
|
|
22
22
|
api_key: opts.apiKey,
|
|
23
23
|
endpoint: opts.endpoint,
|
|
24
24
|
});
|
|
25
|
-
return async (query) => {
|
|
26
|
-
const results = await client.search({
|
|
25
|
+
return async (query, options) => {
|
|
26
|
+
const results = await client.search({
|
|
27
|
+
limit: SEARCH_LIMIT,
|
|
28
|
+
term: query,
|
|
29
|
+
// The sync carries `locale` on every record so an i18n site can scope
|
|
30
|
+
// hosted results to the active language.
|
|
31
|
+
...(options?.locale && { where: { locale: options.locale } }),
|
|
32
|
+
});
|
|
27
33
|
const hits = (results?.hits ?? []).map((hit) => {
|
|
28
34
|
const doc = hit.document as unknown as OramaCloudRecord;
|
|
29
35
|
return {
|
|
@@ -21,6 +21,9 @@ interface PagefindModule {
|
|
|
21
21
|
export const createSearch = async (opts: {
|
|
22
22
|
url: string;
|
|
23
23
|
}): Promise<SearchFn> => {
|
|
24
|
+
// The pagefind bundle lives in the built site (not node_modules) and is
|
|
25
|
+
// resolved at runtime by URL — it can't be a static, code-splittable path.
|
|
26
|
+
// oxlint-disable-next-line react-doctor/no-dynamic-import-path
|
|
24
27
|
const pagefind = (await import(
|
|
25
28
|
/* @vite-ignore */
|
|
26
29
|
opts.url
|
|
@@ -134,7 +134,11 @@ export const excerptFor = (
|
|
|
134
134
|
if (query && matchIndex(content, query) >= 0) {
|
|
135
135
|
return matchSnippet(content, query, 160);
|
|
136
136
|
}
|
|
137
|
-
|
|
137
|
+
if (description) {
|
|
138
|
+
return description;
|
|
139
|
+
}
|
|
140
|
+
const head = content.slice(0, 140);
|
|
141
|
+
return head.length < content.length ? `${head}…` : head;
|
|
138
142
|
};
|
|
139
143
|
|
|
140
144
|
/** Tally how many matches fall into each section, in first-seen order. */
|
|
@@ -31,7 +31,7 @@ export const createSearch = (opts: {
|
|
|
31
31
|
},
|
|
32
32
|
],
|
|
33
33
|
});
|
|
34
|
-
return async (query) => {
|
|
34
|
+
return async (query, options) => {
|
|
35
35
|
const response = await client
|
|
36
36
|
.collections<TypesenseRecord>(opts.collection)
|
|
37
37
|
.documents()
|
|
@@ -40,6 +40,9 @@ export const createSearch = (opts: {
|
|
|
40
40
|
per_page: SEARCH_LIMIT,
|
|
41
41
|
q: query,
|
|
42
42
|
query_by: "title,description,content",
|
|
43
|
+
// The sync marks `locale` as a facet so an i18n site can scope
|
|
44
|
+
// hosted results to the active language.
|
|
45
|
+
...(options?.locale && { filter_by: `locale:=${options.locale}` }),
|
|
43
46
|
},
|
|
44
47
|
{}
|
|
45
48
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client
|
|
2
|
+
* Client behavior for the `<blume-toc>` custom element wrapping each "On this
|
|
3
3
|
* page" list. As the reader scrolls, it marks the link for the section currently
|
|
4
4
|
* in view with `aria-current="location"` — styled via Tailwind `aria-[current]`
|
|
5
5
|
* variants in RootLayout — giving the table of contents a live scrollspy.
|
|
@@ -52,7 +52,13 @@ class BlumeToc extends HTMLElement {
|
|
|
52
52
|
for (const { heading } of this.#entries) {
|
|
53
53
|
this.#observer.observe(heading);
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
// The scroll listener is the intentional fallback for the one case the
|
|
56
|
+
// IntersectionObserver can't cover (a final section too short to push its
|
|
57
|
+
// heading past the trigger line), so replacing it with an observer would
|
|
58
|
+
// change behavior. `scroll` isn't cancelable, so it never preventDefaults
|
|
59
|
+
// and `{ passive: true }` would be a no-op here.
|
|
60
|
+
// oxlint-disable-next-line github/prefer-observers, react-doctor/client-passive-event-listeners
|
|
61
|
+
window.addEventListener("scroll", this.#onScroll);
|
|
56
62
|
this.#update();
|
|
57
63
|
}
|
|
58
64
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
|
+
import { withBase } from "../islands/base-path.ts";
|
|
3
4
|
import MethodBadge from "./MethodBadge.astro";
|
|
4
5
|
|
|
5
6
|
// The operation-link list for one tag section of an API overview page. The
|
|
@@ -25,7 +26,7 @@ const operations = Object.values(specs[source]?.operations ?? {}).filter(
|
|
|
25
26
|
<li>
|
|
26
27
|
<a
|
|
27
28
|
class="flex items-center gap-3 rounded-blume border border-border p-3 text-inherit no-underline! transition-colors hover:border-accent hover:bg-muted hover:no-underline!"
|
|
28
|
-
href={operation.route}
|
|
29
|
+
href={withBase(operation.route)}
|
|
29
30
|
>
|
|
30
31
|
<MethodBadge method={operation.method} />
|
|
31
32
|
<span class="font-medium text-foreground text-sm">
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
mergeParameters,
|
|
5
|
+
type ParameterLike,
|
|
6
|
+
resolveComponentRef,
|
|
7
|
+
type SchemaLike,
|
|
8
|
+
} from "./helpers.ts";
|
|
4
9
|
import { buildRequestSample, sampleLanguages } from "./snippets.ts";
|
|
5
10
|
import MethodBadge from "./MethodBadge.astro";
|
|
6
11
|
import ParametersTable from "./ParametersTable.astro";
|
|
@@ -13,36 +18,31 @@ interface Props {
|
|
|
13
18
|
id: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
interface
|
|
21
|
+
interface MediaTypeLike {
|
|
22
|
+
schema?: SchemaLike;
|
|
23
|
+
example?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RequestBodyLike {
|
|
17
27
|
$ref?: string;
|
|
18
|
-
name?: string;
|
|
19
|
-
in?: string;
|
|
20
28
|
description?: string;
|
|
21
29
|
required?: boolean;
|
|
22
|
-
|
|
23
|
-
schema?: SchemaLike;
|
|
24
|
-
example?: unknown;
|
|
30
|
+
content?: Record<string, MediaTypeLike>;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
interface
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
interface ResponseLike {
|
|
34
|
+
$ref?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
content?: Record<string, MediaTypeLike>;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
interface FullOperation {
|
|
33
40
|
summary?: string;
|
|
34
41
|
description?: string;
|
|
35
42
|
deprecated?: boolean;
|
|
36
|
-
parameters?:
|
|
37
|
-
requestBody?:
|
|
38
|
-
|
|
39
|
-
required?: boolean;
|
|
40
|
-
content?: Record<string, MediaTypeLike>;
|
|
41
|
-
};
|
|
42
|
-
responses?: Record<
|
|
43
|
-
string,
|
|
44
|
-
{ description?: string; content?: Record<string, MediaTypeLike> }
|
|
45
|
-
>;
|
|
43
|
+
parameters?: ParameterLike[];
|
|
44
|
+
requestBody?: RequestBodyLike;
|
|
45
|
+
responses?: Record<string, ResponseLike>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const { source, id } = Astro.props;
|
|
@@ -50,10 +50,15 @@ const spec = specs[source];
|
|
|
50
50
|
const ref = spec?.operations[id];
|
|
51
51
|
|
|
52
52
|
const doc = (spec?.document ?? {}) as {
|
|
53
|
-
paths?: Record<
|
|
53
|
+
paths?: Record<
|
|
54
|
+
string,
|
|
55
|
+
Record<string, unknown> & { parameters?: ParameterLike[] }
|
|
56
|
+
>;
|
|
54
57
|
components?: {
|
|
55
58
|
schemas?: Record<string, SchemaLike>;
|
|
56
|
-
parameters?: Record<string,
|
|
59
|
+
parameters?: Record<string, ParameterLike>;
|
|
60
|
+
requestBodies?: Record<string, RequestBodyLike>;
|
|
61
|
+
responses?: Record<string, ResponseLike>;
|
|
57
62
|
};
|
|
58
63
|
servers?: { url?: string }[];
|
|
59
64
|
};
|
|
@@ -63,24 +68,26 @@ const operation = (
|
|
|
63
68
|
) as FullOperation | undefined;
|
|
64
69
|
|
|
65
70
|
const schemas = doc.components?.schemas ?? {};
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
const components = doc.components ?? {};
|
|
72
|
+
const params = mergeParameters(
|
|
73
|
+
pathItem?.parameters,
|
|
74
|
+
operation?.parameters,
|
|
75
|
+
components
|
|
76
|
+
);
|
|
77
|
+
const requestBody = operation?.requestBody
|
|
78
|
+
? resolveComponentRef(operation.requestBody, components, "requestBodies")
|
|
79
|
+
: undefined;
|
|
80
|
+
const responses = Object.fromEntries(
|
|
81
|
+
Object.entries(operation?.responses ?? {}).map(([status, response]) => [
|
|
82
|
+
status,
|
|
83
|
+
resolveComponentRef(response, components, "responses"),
|
|
84
|
+
])
|
|
85
|
+
);
|
|
79
86
|
|
|
80
87
|
const sample =
|
|
81
88
|
ref && operation
|
|
82
89
|
? buildRequestSample(
|
|
83
|
-
{ parameters: params, requestBody
|
|
90
|
+
{ parameters: params, requestBody },
|
|
84
91
|
ref.method,
|
|
85
92
|
ref.path,
|
|
86
93
|
doc.servers ?? [],
|
|
@@ -109,17 +116,17 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
|
109
116
|
<div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
|
|
110
117
|
<div>
|
|
111
118
|
<ParametersTable parameters={params} schemas={schemas} />
|
|
112
|
-
{
|
|
119
|
+
{requestBody && (
|
|
113
120
|
<RequestBody
|
|
114
121
|
expandAll={spec.expandSchemas}
|
|
115
|
-
requestBody={
|
|
122
|
+
requestBody={requestBody}
|
|
116
123
|
schemas={schemas}
|
|
117
124
|
/>
|
|
118
125
|
)}
|
|
119
126
|
{operation.responses && (
|
|
120
127
|
<Responses
|
|
121
128
|
expandAll={spec.expandSchemas}
|
|
122
|
-
responses={
|
|
129
|
+
responses={responses}
|
|
123
130
|
schemas={schemas}
|
|
124
131
|
/>
|
|
125
132
|
)}
|
|
@@ -128,7 +135,7 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
|
128
135
|
<div class="xl:sticky xl:top-24 xl:self-start">
|
|
129
136
|
<RequestPanel
|
|
130
137
|
languages={languages}
|
|
131
|
-
responses={
|
|
138
|
+
responses={responses}
|
|
132
139
|
sample={sample}
|
|
133
140
|
schemas={schemas}
|
|
134
141
|
/>
|