blume 1.5.0 → 1.5.2
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 +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +449 -135
- package/dist/cli/index.js.map +24 -23
- package/dist/types/ai/ask-context.d.ts +78 -0
- package/dist/types/core/config-input.d.ts +54 -2
- package/dist/types/core/data.d.ts +19 -2
- package/dist/types/core/open-in-chat.d.ts +9 -0
- package/dist/types/core/schema.d.ts +48 -1
- package/dist/types/core/types.d.ts +10 -3
- package/dist/types/openapi/references.d.ts +9 -0
- package/dist/types/search/orama-index.d.ts +70 -0
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +67 -5
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/ai.mdx +35 -0
- package/docs/configuration/index.mdx +14 -2
- package/docs/configuration/search.mdx +4 -4
- package/docs/configuration/theming.mdx +4 -2
- package/docs/reference/cli.mdx +2 -2
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/ask-context.ts +51 -11
- package/src/ai/mcp/data.ts +3 -2
- package/src/ai/mcp/server.ts +3 -2
- package/src/assets/icon-dark.png +0 -0
- package/src/astro/generate.ts +172 -18
- package/src/astro/templates.ts +89 -15
- package/src/components/content/AccordionItem.astro +4 -0
- package/src/components/content/Update.astro +3 -0
- package/src/components/islands/AskAI.astro +6 -0
- package/src/components/islands/ask-ai.tsx +39 -9
- package/src/components/layout/Analytics.astro +9 -1
- package/src/components/layout/Favicon.astro +29 -8
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +2 -2
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/PageActions.astro +120 -78
- package/src/components/layout/PageFeedback.astro +12 -3
- package/src/components/layout/PageLayout.astro +79 -5
- package/src/components/layout/ReferenceLayout.astro +12 -9
- package/src/components/layout/RootLayout.astro +153 -121
- package/src/components/layout/Search.astro +41 -26
- package/src/components/layout/drawer-inert.ts +10 -5
- package/src/components/layout/head-scripts.ts +34 -16
- package/src/components/layout/nav-utils.ts +34 -15
- package/src/components/layout/search/orama.ts +3 -2
- package/src/components/openapi/AsyncApiOperation.astro +22 -7
- package/src/components/openapi/MessageComposer.astro +238 -0
- package/src/components/openapi/Operation.astro +26 -12
- package/src/components/openapi/PanelTabs.astro +7 -0
- package/src/components/openapi/Playground.astro +320 -0
- package/src/components/openapi/RequestPanel.astro +1 -0
- package/src/components/openapi/async-snippets.ts +20 -7
- package/src/components/openapi/async.ts +13 -2
- package/src/components/openapi/message-composer.ts +242 -0
- package/src/components/openapi/message-model.ts +108 -0
- package/src/components/openapi/message.ts +153 -0
- package/src/components/openapi/operation-model.ts +260 -0
- package/src/components/openapi/playground-client.ts +486 -0
- package/src/components/openapi/playground-schema.ts +109 -0
- package/src/components/openapi/request.ts +287 -0
- package/src/components/openapi/security.ts +0 -56
- package/src/components/openapi/snippets.ts +23 -136
- package/src/components/openapi/validate-json.ts +144 -0
- package/src/components/openapi/ws-client.ts +194 -0
- package/src/core/config-input.ts +67 -1
- package/src/core/content-assets.ts +66 -15
- package/src/core/data.ts +16 -2
- package/src/core/last-modified.ts +76 -2
- package/src/core/links.ts +30 -4
- package/src/core/navigation.ts +26 -1
- package/src/core/open-in-chat.ts +17 -0
- package/src/core/project-graph.ts +11 -0
- package/src/core/schema.ts +60 -1
- package/src/core/server-features.ts +11 -0
- package/src/core/sources/normalize.ts +10 -2
- package/src/core/types.ts +10 -3
- package/src/deploy/vercel-negotiation.ts +34 -14
- package/src/og/card.ts +3 -1
- package/src/openapi/model.ts +7 -0
- package/src/openapi/proxy.ts +217 -0
- package/src/openapi/references.ts +8 -0
- package/src/openapi/source.ts +13 -0
- package/src/registry/eject.ts +4 -5
- package/src/search/orama-index.ts +109 -36
- package/src/theme/entry.ts +15 -2
- package/src/theme/fonts.ts +75 -3
|
@@ -3,7 +3,8 @@ import { EN_UI } from "../../core/i18n-ui.ts";
|
|
|
3
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
4
4
|
import { resolveDateFormatOptions } from "../../core/date-format.ts";
|
|
5
5
|
import type { ResolvedDateFormat } from "../../core/schema.ts";
|
|
6
|
-
import type {
|
|
6
|
+
import type { OpenInChatProvider } from "../../core/open-in-chat.ts";
|
|
7
|
+
import type { BlumeClientData, BlumeFavicon } from "../../core/data.ts";
|
|
7
8
|
import type {
|
|
8
9
|
Heading,
|
|
9
10
|
LocaleSwitchOption,
|
|
@@ -11,6 +12,8 @@ import type {
|
|
|
11
12
|
NavSelector as NavSelectorType,
|
|
12
13
|
} from "../../core/types.ts";
|
|
13
14
|
import "blume:theme";
|
|
15
|
+
import { ClientRouter } from "astro:transitions";
|
|
16
|
+
import type { FontHead } from "../../theme/fonts.ts";
|
|
14
17
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
15
18
|
import {
|
|
16
19
|
OG_IMAGE_HEIGHT,
|
|
@@ -35,7 +38,7 @@ import {
|
|
|
35
38
|
import Header from "./Header.astro";
|
|
36
39
|
import Icon from "../Icon.astro";
|
|
37
40
|
import {
|
|
38
|
-
|
|
41
|
+
currentTabForRoute,
|
|
39
42
|
findBreadcrumbs,
|
|
40
43
|
flattenPages,
|
|
41
44
|
getPagination,
|
|
@@ -59,14 +62,8 @@ interface Props {
|
|
|
59
62
|
href: string;
|
|
60
63
|
text?: string;
|
|
61
64
|
} | null;
|
|
62
|
-
favicon?:
|
|
63
|
-
|
|
64
|
-
type?: string;
|
|
65
|
-
} | null;
|
|
66
|
-
appleIcon?: {
|
|
67
|
-
href: string;
|
|
68
|
-
type?: string;
|
|
69
|
-
} | null;
|
|
65
|
+
favicon?: BlumeFavicon | null;
|
|
66
|
+
appleIcon?: BlumeFavicon | null;
|
|
70
67
|
banner?: {
|
|
71
68
|
content: string;
|
|
72
69
|
link?: { text: string; href: string };
|
|
@@ -90,7 +87,7 @@ interface Props {
|
|
|
90
87
|
imageZoom?: boolean;
|
|
91
88
|
codeWrap?: boolean;
|
|
92
89
|
themeMode: "system" | "light" | "dark";
|
|
93
|
-
fontCssVars?: string[];
|
|
90
|
+
fontCssVars?: (string | FontHead)[];
|
|
94
91
|
searchEnabled: boolean;
|
|
95
92
|
indexable: boolean;
|
|
96
93
|
ogImage?: string | null;
|
|
@@ -118,6 +115,11 @@ interface Props {
|
|
|
118
115
|
feedback?: boolean;
|
|
119
116
|
exportPdf?: boolean;
|
|
120
117
|
exportEpub?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* "Open in chat" providers to list, in order; an empty list hides the
|
|
120
|
+
* action, and omitting the prop shows every provider.
|
|
121
|
+
*/
|
|
122
|
+
openInChat?: readonly OpenInChatProvider[];
|
|
121
123
|
feeds?: { title: string; href: string }[];
|
|
122
124
|
/**
|
|
123
125
|
* Which agent-discovery resources exist, advertised as `describedby` head
|
|
@@ -217,6 +219,7 @@ const {
|
|
|
217
219
|
feedback = false,
|
|
218
220
|
exportPdf,
|
|
219
221
|
exportEpub,
|
|
222
|
+
openInChat,
|
|
220
223
|
feeds,
|
|
221
224
|
discovery,
|
|
222
225
|
siteUrl,
|
|
@@ -355,14 +358,21 @@ const mcpUrl =
|
|
|
355
358
|
// pages. Without tabs — or on a route under none — this is the full sidebar.
|
|
356
359
|
// `navigation.root` keeps the root-tab check in the tabs' localized/based
|
|
357
360
|
// path space (`/en`, `/docs`), so a locale or base prefix doesn't misread the
|
|
358
|
-
// root tab as a section tab
|
|
361
|
+
// root tab as a section tab; in an archived version tree the root is
|
|
362
|
+
// versionized (`/v1.0`) while tabs stay in current-docs space, so the root
|
|
363
|
+
// tab is matched by containment and the snapshot renders unscoped — the same
|
|
364
|
+
// reason no tab is marked current there (tabs link back to current docs).
|
|
359
365
|
const sidebar = sidebarForRoute(
|
|
360
366
|
navigation.sidebar,
|
|
361
367
|
navigation.tabs,
|
|
362
368
|
page.route,
|
|
363
369
|
navigation.root
|
|
364
370
|
);
|
|
365
|
-
const activeTab =
|
|
371
|
+
const activeTab = currentTabForRoute(
|
|
372
|
+
navigation.tabs,
|
|
373
|
+
page.route,
|
|
374
|
+
navigation.root
|
|
375
|
+
);
|
|
366
376
|
const crumbs = findBreadcrumbs(sidebar, page.route);
|
|
367
377
|
const { prev, next } = getPagination(flattenPages(sidebar), page.route);
|
|
368
378
|
|
|
@@ -397,6 +407,13 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
397
407
|
<head>
|
|
398
408
|
<meta charset="utf-8" />
|
|
399
409
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
410
|
+
{/* Client-side navigation: same-origin link clicks swap the DOM in place
|
|
411
|
+
instead of tearing the document down, so no browser ever paints a blank
|
|
412
|
+
frame between pages (Firefox has no cross-document paint holding and
|
|
413
|
+
flickered on every full load). Uses native view transitions where
|
|
414
|
+
supported, with Astro's simulated fade elsewhere; pairs with the
|
|
415
|
+
prefetch option in the generated Astro config. */}
|
|
416
|
+
<ClientRouter />
|
|
400
417
|
<title>{pageTitle}</title>
|
|
401
418
|
<Favicon favicon={favicon} appleIcon={appleIcon} />
|
|
402
419
|
<Fonts cssVars={fontCssVars ?? []} />
|
|
@@ -705,6 +722,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
705
722
|
exportPdf={exportPdf}
|
|
706
723
|
mcpName={mcp?.name}
|
|
707
724
|
mcpUrl={mcpUrl}
|
|
725
|
+
openInChat={openInChat}
|
|
708
726
|
route={page.route}
|
|
709
727
|
strings={strings.actions}
|
|
710
728
|
/>
|
|
@@ -743,6 +761,8 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
743
761
|
// Tree-shaken out of production builds.
|
|
744
762
|
import "./hydration-hint.ts";
|
|
745
763
|
|
|
764
|
+
// Runs once per real page load; re-syncs itself after client-router
|
|
765
|
+
// swaps (see drawer-inert.ts).
|
|
746
766
|
syncDrawerInert();
|
|
747
767
|
|
|
748
768
|
const svg = (name: string, cls = "") =>
|
|
@@ -754,14 +774,6 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
754
774
|
"absolute right-3 z-[2] inline-flex size-[1.875rem] select-none items-center justify-center rounded-md text-muted-foreground bg-background transition-colors [&_svg]:pointer-events-none [&_svg]:shrink-0";
|
|
755
775
|
const idleClasses = ["hover:bg-muted", "hover:text-foreground"];
|
|
756
776
|
|
|
757
|
-
// Localized copy-button labels, stamped on <body> by the layout markup
|
|
758
|
-
// (the Search.astro data-attribute channel) since this bundled script
|
|
759
|
-
// can't interpolate server values directly.
|
|
760
|
-
const copyCodeLabel =
|
|
761
|
-
document.body.getAttribute("data-i18n-copy-code") || "Copy code";
|
|
762
|
-
const copiedLabel =
|
|
763
|
-
document.body.getAttribute("data-i18n-copied") || "Copied!";
|
|
764
|
-
|
|
765
777
|
const languageLabels: Record<string, string> = {
|
|
766
778
|
astro: "Astro",
|
|
767
779
|
bash: "Bash",
|
|
@@ -783,111 +795,131 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
783
795
|
zsh: "Zsh",
|
|
784
796
|
};
|
|
785
797
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
// The code element is the scroll container (see the theme entry), but
|
|
804
|
-
// Shiki's tab stop lands on the pre, which no longer scrolls. Move the
|
|
805
|
-
// stop to the code so keyboard users can actually scroll the block
|
|
806
|
-
// (WCAG 2.1.1 — the same rule the table wrapper handles). Twoslash and
|
|
807
|
-
// API-panel blocks keep the pre as their scroller, so theirs stays.
|
|
808
|
-
const scroller = pre.querySelector("code");
|
|
809
|
-
if (scroller && !pre.matches(".twoslash, blume-panel-tabs *")) {
|
|
810
|
-
scroller.setAttribute("tabindex", "0");
|
|
811
|
-
pre.removeAttribute("tabindex");
|
|
812
|
-
}
|
|
813
|
-
const button = document.createElement("button");
|
|
814
|
-
button.type = "button";
|
|
815
|
-
// The language-label bar (prose) vs flush code (tabs) need a different
|
|
816
|
-
// offset; pick the Tailwind class by context instead of a CSS override.
|
|
817
|
-
const topClass = pre.closest("blume-tabs, .not-prose")
|
|
818
|
-
? "top-2.5"
|
|
819
|
-
: "top-2";
|
|
820
|
-
button.className = `${buttonClass} ${idleClasses.join(" ")} ${topClass}`;
|
|
821
|
-
button.setAttribute("data-blume-copy", "");
|
|
822
|
-
button.setAttribute("aria-label", copyCodeLabel);
|
|
823
|
-
button.innerHTML =
|
|
824
|
-
svg(
|
|
825
|
-
"check",
|
|
826
|
-
"scale-0 text-green-600 transition-transform dark:text-green-500"
|
|
827
|
-
) + svg("copy", "absolute transition-transform");
|
|
828
|
-
const [checkIcon, copyIcon] = button.querySelectorAll("svg");
|
|
829
|
-
// While checked the button drops its idle hover tint so the green
|
|
830
|
-
// check reads as a steady confirmation, mirroring Lina's copied state.
|
|
831
|
-
// The accessible name tracks the visual state for anyone probing the
|
|
832
|
-
// button mid-confirmation.
|
|
833
|
-
const setChecked = (checked: boolean) => {
|
|
834
|
-
checkIcon?.classList.toggle("scale-0", !checked);
|
|
835
|
-
copyIcon?.classList.toggle("scale-0", checked);
|
|
836
|
-
button.setAttribute(
|
|
837
|
-
"aria-label",
|
|
838
|
-
checked ? copiedLabel : copyCodeLabel
|
|
839
|
-
);
|
|
840
|
-
for (const cls of idleClasses) {
|
|
841
|
-
button.classList.toggle(cls, !checked);
|
|
798
|
+
// Per-page setup, run on the initial load and again after every
|
|
799
|
+
// client-router swap (which replaces the body with server-rendered
|
|
800
|
+
// markup that has none of this applied). Everything in here binds to
|
|
801
|
+
// elements the swap just created, so nothing double-attaches.
|
|
802
|
+
const initPage = async () => {
|
|
803
|
+
// Localized copy-button labels, stamped on <body> by the layout markup
|
|
804
|
+
// (the Search.astro data-attribute channel) since this bundled script
|
|
805
|
+
// can't interpolate server values directly. Read per page so a
|
|
806
|
+
// cross-locale navigation picks up the new language's labels.
|
|
807
|
+
const copyCodeLabel =
|
|
808
|
+
document.body.getAttribute("data-i18n-copy-code") || "Copy code";
|
|
809
|
+
const copiedLabel =
|
|
810
|
+
document.body.getAttribute("data-i18n-copied") || "Copied!";
|
|
811
|
+
|
|
812
|
+
for (const pre of document.querySelectorAll(".prose pre")) {
|
|
813
|
+
if (pre.querySelector("[data-blume-copy]")) {
|
|
814
|
+
continue;
|
|
842
815
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
let text = code?.textContent ?? "";
|
|
848
|
-
// Twoslash nests each hover popup's type signature and docs inside
|
|
849
|
-
// the <code>; copying textContent verbatim would interleave them
|
|
850
|
-
// with the source. Strip the popups from a clone first.
|
|
851
|
-
if (code?.querySelector(".twoslash-popup-container")) {
|
|
852
|
-
const clone = code.cloneNode(true) as HTMLElement;
|
|
853
|
-
for (const popup of clone.querySelectorAll(
|
|
854
|
-
".twoslash-popup-container"
|
|
855
|
-
)) {
|
|
856
|
-
popup.remove();
|
|
857
|
-
}
|
|
858
|
-
text = clone.textContent ?? "";
|
|
816
|
+
// Skip nested <pre>: Twoslash renders each hover popup's type signature
|
|
817
|
+
// as a <pre> inside the code block, which shouldn't get its own button.
|
|
818
|
+
if (pre.parentElement?.closest("pre")) {
|
|
819
|
+
continue;
|
|
859
820
|
}
|
|
860
|
-
|
|
861
|
-
|
|
821
|
+
const language = pre.getAttribute("data-language");
|
|
822
|
+
if (language) {
|
|
823
|
+
pre.setAttribute(
|
|
824
|
+
"data-language",
|
|
825
|
+
languageLabels[language.toLowerCase()] ?? language
|
|
826
|
+
);
|
|
862
827
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
"
|
|
887
|
-
|
|
828
|
+
pre.classList.add("group", "relative");
|
|
829
|
+
// The code element is the scroll container (see the theme entry), but
|
|
830
|
+
// Shiki's tab stop lands on the pre, which no longer scrolls. Move the
|
|
831
|
+
// stop to the code so keyboard users can actually scroll the block
|
|
832
|
+
// (WCAG 2.1.1 — the same rule the table wrapper handles). Twoslash and
|
|
833
|
+
// API-panel blocks keep the pre as their scroller, so theirs stays.
|
|
834
|
+
const scroller = pre.querySelector("code");
|
|
835
|
+
if (scroller && !pre.matches(".twoslash, blume-panel-tabs *")) {
|
|
836
|
+
scroller.setAttribute("tabindex", "0");
|
|
837
|
+
pre.removeAttribute("tabindex");
|
|
838
|
+
}
|
|
839
|
+
const button = document.createElement("button");
|
|
840
|
+
button.type = "button";
|
|
841
|
+
// The language-label bar (prose) vs flush code (tabs) need a different
|
|
842
|
+
// offset; pick the Tailwind class by context instead of a CSS override.
|
|
843
|
+
const topClass = pre.closest("blume-tabs, .not-prose")
|
|
844
|
+
? "top-2.5"
|
|
845
|
+
: "top-2";
|
|
846
|
+
button.className = `${buttonClass} ${idleClasses.join(" ")} ${topClass}`;
|
|
847
|
+
button.setAttribute("data-blume-copy", "");
|
|
848
|
+
button.setAttribute("aria-label", copyCodeLabel);
|
|
849
|
+
button.innerHTML =
|
|
850
|
+
svg(
|
|
851
|
+
"check",
|
|
852
|
+
"scale-0 text-green-600 transition-transform dark:text-green-500"
|
|
853
|
+
) + svg("copy", "absolute transition-transform");
|
|
854
|
+
const [checkIcon, copyIcon] = button.querySelectorAll("svg");
|
|
855
|
+
// While checked the button drops its idle hover tint so the green
|
|
856
|
+
// check reads as a steady confirmation, mirroring Lina's copied state.
|
|
857
|
+
// The accessible name tracks the visual state for anyone probing the
|
|
858
|
+
// button mid-confirmation.
|
|
859
|
+
const setChecked = (checked: boolean) => {
|
|
860
|
+
checkIcon?.classList.toggle("scale-0", !checked);
|
|
861
|
+
copyIcon?.classList.toggle("scale-0", checked);
|
|
862
|
+
button.setAttribute(
|
|
863
|
+
"aria-label",
|
|
864
|
+
checked ? copiedLabel : copyCodeLabel
|
|
865
|
+
);
|
|
866
|
+
for (const cls of idleClasses) {
|
|
867
|
+
button.classList.toggle(cls, !checked);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
const flash = createCopyFlash(setChecked, copiedLabel);
|
|
871
|
+
button.addEventListener("click", async () => {
|
|
872
|
+
const code = pre.querySelector("code");
|
|
873
|
+
let text = code?.textContent ?? "";
|
|
874
|
+
// Twoslash nests each hover popup's type signature and docs inside
|
|
875
|
+
// the <code>; copying textContent verbatim would interleave them
|
|
876
|
+
// with the source. Strip the popups from a clone first.
|
|
877
|
+
if (code?.querySelector(".twoslash-popup-container")) {
|
|
878
|
+
const clone = code.cloneNode(true) as HTMLElement;
|
|
879
|
+
for (const popup of clone.querySelectorAll(
|
|
880
|
+
".twoslash-popup-container"
|
|
881
|
+
)) {
|
|
882
|
+
popup.remove();
|
|
883
|
+
}
|
|
884
|
+
text = clone.textContent ?? "";
|
|
885
|
+
}
|
|
886
|
+
if (await copyText(text)) {
|
|
887
|
+
flash();
|
|
888
|
+
}
|
|
888
889
|
});
|
|
890
|
+
pre.appendChild(button);
|
|
889
891
|
}
|
|
890
|
-
|
|
892
|
+
|
|
893
|
+
// Click-to-zoom for content images (gated by `markdown.imageZoom`),
|
|
894
|
+
// via medium-zoom: ESC/scroll/click dismissal, natural-size capping,
|
|
895
|
+
// and the open/close transition races are its problem, not ours.
|
|
896
|
+
// Opt out per-image with `data-no-zoom`.
|
|
897
|
+
if (document.body.hasAttribute("data-blume-image-zoom")) {
|
|
898
|
+
const zoomTargets = Array.from(
|
|
899
|
+
document.querySelectorAll<HTMLImageElement>(
|
|
900
|
+
".prose img:not([data-no-zoom])"
|
|
901
|
+
)
|
|
902
|
+
// An image that is itself a link navigates on click — binding zoom
|
|
903
|
+
// to it would flash a zoom overlay in the instant before navigation
|
|
904
|
+
// and advertise (via the cursor) a zoom that never happens.
|
|
905
|
+
).filter((image) => !image.closest("a"));
|
|
906
|
+
if (zoomTargets.length > 0) {
|
|
907
|
+
// Lazy: pages without a zoomable image never load the library,
|
|
908
|
+
// matching how mermaid is only fetched on pages with a diagram.
|
|
909
|
+
const { default: mediumZoom } = await import("medium-zoom");
|
|
910
|
+
mediumZoom(zoomTargets, {
|
|
911
|
+
background:
|
|
912
|
+
"color-mix(in oklab, var(--color-background) 80%, transparent)",
|
|
913
|
+
margin: 24,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
void initPage();
|
|
920
|
+
document.addEventListener("astro:after-swap", () => {
|
|
921
|
+
void initPage();
|
|
922
|
+
});
|
|
891
923
|
</script>
|
|
892
924
|
<style is:global>
|
|
893
925
|
/* medium-zoom ships no z-index; lift the lightbox above the chrome
|
|
@@ -203,6 +203,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
203
203
|
/>
|
|
204
204
|
|
|
205
205
|
<script>
|
|
206
|
+
import { navigate } from "astro:transitions/client";
|
|
206
207
|
import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
|
|
207
208
|
import { prefixBase } from "../islands/base-path.ts";
|
|
208
209
|
import { escape as escapeHtml } from "html-escaper";
|
|
@@ -301,6 +302,36 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
301
302
|
version = "";
|
|
302
303
|
allVersions = false;
|
|
303
304
|
|
|
305
|
+
// Document-level, so it outlives this element unless removed: each
|
|
306
|
+
// client-router swap rebuilds the header (and this element with it), and
|
|
307
|
+
// an orphaned copy would keep toggling a dialog that is no longer in the
|
|
308
|
+
// document. Held as a field so disconnectedCallback can detach it.
|
|
309
|
+
#onDocumentKeydown = (event: KeyboardEvent) => {
|
|
310
|
+
if (
|
|
311
|
+
(event.key === "k" || event.key === "K") &&
|
|
312
|
+
(event.metaKey || event.ctrlKey) &&
|
|
313
|
+
// Ctrl+Shift+K is Firefox's web console; a shifted or alted chord
|
|
314
|
+
// belongs to the browser, not the search dialog.
|
|
315
|
+
!event.shiftKey &&
|
|
316
|
+
!event.altKey
|
|
317
|
+
) {
|
|
318
|
+
// ⌘K toggles, mirroring the Ask AI panel's ⌘I: pressing it with
|
|
319
|
+
// the dialog open must close it, not re-showModal an open dialog
|
|
320
|
+
// (an InvalidStateError on older engines).
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
if (this.dialog.open) {
|
|
323
|
+
this.dialog.close();
|
|
324
|
+
} else {
|
|
325
|
+
this.open();
|
|
326
|
+
}
|
|
327
|
+
} else if (event.key === "/" && !this.isField(event.target)) {
|
|
328
|
+
// "/" stays open-only; the field guard keeps it inert while
|
|
329
|
+
// typing (including in the search input itself).
|
|
330
|
+
event.preventDefault();
|
|
331
|
+
this.open();
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
304
335
|
connectedCallback() {
|
|
305
336
|
this.devOnlyMsg =
|
|
306
337
|
this.getAttribute("data-i18n-dev") || this.devOnlyMsg;
|
|
@@ -397,31 +428,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
397
428
|
() => this.open()
|
|
398
429
|
);
|
|
399
430
|
|
|
400
|
-
document.addEventListener("keydown",
|
|
401
|
-
if (
|
|
402
|
-
(event.key === "k" || event.key === "K") &&
|
|
403
|
-
(event.metaKey || event.ctrlKey) &&
|
|
404
|
-
// Ctrl+Shift+K is Firefox's web console; a shifted or alted chord
|
|
405
|
-
// belongs to the browser, not the search dialog.
|
|
406
|
-
!event.shiftKey &&
|
|
407
|
-
!event.altKey
|
|
408
|
-
) {
|
|
409
|
-
// ⌘K toggles, mirroring the Ask AI panel's ⌘I: pressing it with
|
|
410
|
-
// the dialog open must close it, not re-showModal an open dialog
|
|
411
|
-
// (an InvalidStateError on older engines).
|
|
412
|
-
event.preventDefault();
|
|
413
|
-
if (this.dialog.open) {
|
|
414
|
-
this.dialog.close();
|
|
415
|
-
} else {
|
|
416
|
-
this.open();
|
|
417
|
-
}
|
|
418
|
-
} else if (event.key === "/" && !this.isField(event.target)) {
|
|
419
|
-
// "/" stays open-only; the field guard keeps it inert while
|
|
420
|
-
// typing (including in the search input itself).
|
|
421
|
-
event.preventDefault();
|
|
422
|
-
this.open();
|
|
423
|
-
}
|
|
424
|
-
});
|
|
431
|
+
document.addEventListener("keydown", this.#onDocumentKeydown);
|
|
425
432
|
|
|
426
433
|
this.input.addEventListener("input", () => this.render());
|
|
427
434
|
this.dialog.addEventListener("keydown", (event) =>
|
|
@@ -434,6 +441,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
434
441
|
});
|
|
435
442
|
}
|
|
436
443
|
|
|
444
|
+
disconnectedCallback() {
|
|
445
|
+
document.removeEventListener("keydown", this.#onDocumentKeydown);
|
|
446
|
+
}
|
|
447
|
+
|
|
437
448
|
isField(target: EventTarget | null): boolean {
|
|
438
449
|
const el = target as HTMLElement | null;
|
|
439
450
|
return Boolean(
|
|
@@ -799,7 +810,11 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
799
810
|
new CustomEvent("blume:open-ask-ai", { detail: { query } })
|
|
800
811
|
);
|
|
801
812
|
} else if (item.url) {
|
|
802
|
-
|
|
813
|
+
// Close first so the dialog isn't left open over the transition;
|
|
814
|
+
// navigate() rides the client router (and falls back to a normal
|
|
815
|
+
// full load on pages without it).
|
|
816
|
+
this.dialog.close();
|
|
817
|
+
navigate(item.url);
|
|
803
818
|
}
|
|
804
819
|
}
|
|
805
820
|
|
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
* stay focusable on every page. Mirrors the header's `data-blume-nav-open`
|
|
5
5
|
* toggle into `inert`/`aria-hidden` — but only below `lg` (64rem), where the
|
|
6
6
|
* same element isn't the static sidebar (RootLayout) or is display-hidden
|
|
7
|
-
* anyway (PageLayout). Shared by both layouts'
|
|
7
|
+
* anyway (PageLayout). Shared by both layouts' bundled scripts, which run once
|
|
8
|
+
* per real page load; the drawer element is rebuilt by every client-router
|
|
9
|
+
* swap, so `sync` re-queries it each time and re-runs on `astro:after-swap`.
|
|
8
10
|
*/
|
|
9
11
|
export const syncDrawerInert = (): void => {
|
|
10
|
-
const drawer = document.querySelector<HTMLElement>("[data-blume-nav-drawer]");
|
|
11
|
-
if (!drawer) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
12
|
const desktop = window.matchMedia("(min-width: 64rem)");
|
|
15
13
|
const sync = () => {
|
|
14
|
+
const drawer = document.querySelector<HTMLElement>(
|
|
15
|
+
"[data-blume-nav-drawer]"
|
|
16
|
+
);
|
|
17
|
+
if (!drawer) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
16
20
|
const hidden =
|
|
17
21
|
!desktop.matches &&
|
|
18
22
|
!Object.hasOwn(document.documentElement.dataset, "blumeNavOpen");
|
|
@@ -28,4 +32,5 @@ export const syncDrawerInert = (): void => {
|
|
|
28
32
|
new MutationObserver(sync).observe(document.documentElement, {
|
|
29
33
|
attributeFilter: ["data-blume-nav-open"],
|
|
30
34
|
});
|
|
35
|
+
document.addEventListener("astro:after-swap", sync);
|
|
31
36
|
};
|
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* scrolled away from the current page. Kept in one place so the layouts can't
|
|
7
7
|
* drift on this timing-critical logic.
|
|
8
8
|
*
|
|
9
|
+
* Under the client router (`<ClientRouter />`), each script's element reappears
|
|
10
|
+
* on every navigated-to page but is executed only once per real page load —
|
|
11
|
+
* Astro skips scripts whose content it has already run. Anything that must hold
|
|
12
|
+
* per navigation therefore also registers an `astro:after-swap` listener on the
|
|
13
|
+
* first (and only) execution: the swap replaces the `<html>` attributes and the
|
|
14
|
+
* body wholesale, wiping `data-theme`/`data-blume-banner-hidden` and rebuilding
|
|
15
|
+
* the sidebar, and `after-swap` fires before the new page paints — the same
|
|
16
|
+
* no-flash timing the initial inline run has.
|
|
17
|
+
*
|
|
9
18
|
* All are constants, never built by interpolating config into source text: any
|
|
10
19
|
* values they need ride in as `data-*` attributes on the script tag and are read
|
|
11
20
|
* back through `document.currentScript`. Baking a config string into JS — even
|
|
@@ -16,31 +25,40 @@
|
|
|
16
25
|
|
|
17
26
|
/**
|
|
18
27
|
* Set `data-theme` from the stored preference (or the configured default, or the
|
|
19
|
-
* OS setting for `"system"`) before the body paints, avoiding a theme flash
|
|
28
|
+
* OS setting for `"system"`) before the body paints, avoiding a theme flash —
|
|
29
|
+
* and again after every client-router swap, which resets `<html>` attributes to
|
|
30
|
+
* the incoming page's server-rendered (theme-less) set.
|
|
20
31
|
*
|
|
21
32
|
* Reads `data-mode` — `"system" | "light" | "dark"`.
|
|
22
33
|
*/
|
|
23
|
-
export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";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);})();`;
|
|
34
|
+
export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";const apply=()=>{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);};apply();document.addEventListener("astro:after-swap",apply);})();`;
|
|
24
35
|
|
|
25
36
|
/**
|
|
26
|
-
* Hide a previously-dismissed banner before it can flash in
|
|
37
|
+
* Hide a previously-dismissed banner before it can flash in — and again after
|
|
38
|
+
* every client-router swap, which wipes the `<html>` marker attribute.
|
|
27
39
|
*
|
|
28
40
|
* Reads `data-key` — the banner's dismissal key.
|
|
29
41
|
*/
|
|
30
|
-
export const BANNER_INIT_SCRIPT = `(()=>{const k=document.currentScript?.dataset.key;if(k
|
|
42
|
+
export const BANNER_INIT_SCRIPT = `(()=>{const k=document.currentScript?.dataset.key;if(!k){return;}const apply=()=>{if(localStorage.getItem("blume-banner:"+k)){document.documentElement.setAttribute("data-blume-banner-hidden","");}};apply();document.addEventListener("astro:after-swap",apply);})();`;
|
|
31
43
|
|
|
32
44
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
45
|
+
* Keep the sidebar's scroll useful across page changes. The sidebar is its own
|
|
46
|
+
* scroll container, reborn scrolled to the top whenever its markup is rebuilt —
|
|
47
|
+
* on a long sidebar the viewport would visibly jump away from the link you just
|
|
48
|
+
* clicked.
|
|
49
|
+
*
|
|
50
|
+
* On the initial load it centers the current page's link before the sidebar
|
|
51
|
+
* paints (it runs inline immediately after the sidebar `<aside>`, not in
|
|
52
|
+
* `<head>`: it needs that markup parsed). On client-router navigations it first
|
|
53
|
+
* restores the exact scroll position saved at `astro:before-swap` — so clicking
|
|
54
|
+
* through nearby links doesn't move the sidebar at all — and only re-centers
|
|
55
|
+
* when the new page's link sits outside the visible scroll area.
|
|
37
56
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* (`
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* scroll area, so a short sidebar never moves.
|
|
57
|
+
* The lookup is scoped to the page tree (`data-blume-nav-tree`) because the
|
|
58
|
+
* drawer also holds the mobile tabs list, whose active tab is `aria-current`
|
|
59
|
+
* too. `getClientRects()` skips links that aren't rendered — `hidden` drill-in
|
|
60
|
+
* panels and breakpoint-hidden duplicates — and centering no-ops when the
|
|
61
|
+
* active link is already inside the visible scroll area, so a short sidebar
|
|
62
|
+
* never moves.
|
|
45
63
|
*/
|
|
46
|
-
export const SIDEBAR_SCROLL_INIT_SCRIPT = `(()=>{const
|
|
64
|
+
export const SIDEBAR_SCROLL_INIT_SCRIPT = `(()=>{const drawer=()=>document.querySelector("[data-blume-nav-drawer]");const center=()=>{const n=drawer();const s=n&&(n.querySelector("[data-blume-nav-tree]")||n);if(!s)return;let l=null;for(const a of s.querySelectorAll('a[aria-current="page"]')){if(a.getClientRects().length){l=a;break;}}if(!l)return;const r=n.getBoundingClientRect();const t=l.getBoundingClientRect();if(t.top>=r.top&&t.bottom<=r.bottom)return;n.scrollTop+=t.top-r.top-(n.clientHeight-t.height)/2;};let saved=-1;document.addEventListener("astro:before-swap",()=>{const n=drawer();saved=n?n.scrollTop:-1;});document.addEventListener("astro:after-swap",()=>{const n=drawer();if(n&&saved>=0){n.scrollTop=saved;}center();});center();})();`;
|