blume 1.4.2 → 1.5.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 +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
|
@@ -152,9 +152,13 @@ const basedRoute = withBase(route);
|
|
|
152
152
|
// strip it before joining with the root-relative route so canonical/og URLs
|
|
153
153
|
// don't come out double-slashed — the catch-all strips it the same way.
|
|
154
154
|
const siteBase = siteUrl ? siteUrl.replace(/\/$/u, "") : null;
|
|
155
|
+
// The route-derived part is percent-encoded (the sitemap convention) so a
|
|
156
|
+
// Unicode route slug yields a legal URI that byte-matches the sitemap <loc>.
|
|
155
157
|
const resolvedCanonical =
|
|
156
158
|
canonical ??
|
|
157
|
-
(siteBase
|
|
159
|
+
(siteBase
|
|
160
|
+
? `${siteBase}${basedRoute === "/" ? "" : encodeURI(basedRoute)}`
|
|
161
|
+
: null);
|
|
158
162
|
// An explicit `ogImage` wins. A root-relative path (e.g. an image dropped in
|
|
159
163
|
// `public/`) is resolved against the site URL so crawlers get an absolute
|
|
160
164
|
// `og:image`; an already-absolute URL passes through untouched. Otherwise fall
|
|
@@ -164,7 +168,7 @@ const absolutizeOgImage = (value: string): string =>
|
|
|
164
168
|
const resolvedOgImage = ogImage
|
|
165
169
|
? absolutizeOgImage(ogImage)
|
|
166
170
|
: ogEnabled && siteBase
|
|
167
|
-
? `${siteBase}${withBase(`/og/${ogSlug}.png`)}`
|
|
171
|
+
? `${siteBase}${encodeURI(withBase(`/og/${ogSlug}.png`))}`
|
|
168
172
|
: null;
|
|
169
173
|
// Only the generated card has a known size and format, so its dimensions are
|
|
170
174
|
// declared for crawlers; a user-supplied `ogImage` could be any image.
|
|
@@ -305,36 +309,12 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
305
309
|
)
|
|
306
310
|
}
|
|
307
311
|
<script>
|
|
312
|
+
import { syncDrawerInert } from "./drawer-inert.ts";
|
|
308
313
|
// Dev-only: friendly hint after a React island hydration mismatch;
|
|
309
314
|
// tree-shaken out of production builds.
|
|
310
315
|
import "./hydration-hint.ts";
|
|
311
316
|
|
|
312
|
-
|
|
313
|
-
// would stay in the tab order. Mirror the header's `data-blume-nav-open`
|
|
314
|
-
// toggle into `inert`/`aria-hidden` below `lg` (64rem), matching the
|
|
315
|
-
// breakpoint where the drawer is display-hidden anyway.
|
|
316
|
-
const drawer = document.querySelector<HTMLElement>(
|
|
317
|
-
"[data-blume-nav-drawer]"
|
|
318
|
-
);
|
|
319
|
-
if (drawer) {
|
|
320
|
-
const desktop = window.matchMedia("(min-width: 64rem)");
|
|
321
|
-
const syncDrawer = () => {
|
|
322
|
-
const hidden =
|
|
323
|
-
!desktop.matches &&
|
|
324
|
-
!document.documentElement.hasAttribute("data-blume-nav-open");
|
|
325
|
-
drawer.inert = hidden;
|
|
326
|
-
if (hidden) {
|
|
327
|
-
drawer.setAttribute("aria-hidden", "true");
|
|
328
|
-
} else {
|
|
329
|
-
drawer.removeAttribute("aria-hidden");
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
syncDrawer();
|
|
333
|
-
desktop.addEventListener("change", syncDrawer);
|
|
334
|
-
new MutationObserver(syncDrawer).observe(document.documentElement, {
|
|
335
|
-
attributeFilter: ["data-blume-nav-open"],
|
|
336
|
-
});
|
|
337
|
-
}
|
|
317
|
+
syncDrawerInert();
|
|
338
318
|
</script>
|
|
339
319
|
</body>
|
|
340
320
|
</html>
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
Heading,
|
|
9
9
|
LocaleSwitchOption,
|
|
10
10
|
Navigation,
|
|
11
|
+
NavSelector as NavSelectorType,
|
|
11
12
|
} from "../../core/types.ts";
|
|
12
13
|
import "blume:theme";
|
|
13
14
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
@@ -46,6 +47,7 @@ import PageActions from "./PageActions.astro";
|
|
|
46
47
|
import PageFeedback from "./PageFeedback.astro";
|
|
47
48
|
import Pagination from "./Pagination.astro";
|
|
48
49
|
import TableOfContents from "./TableOfContents.astro";
|
|
50
|
+
import VersionBanner from "./VersionBanner.astro";
|
|
49
51
|
|
|
50
52
|
interface Props {
|
|
51
53
|
site: { title: string; description?: string };
|
|
@@ -149,6 +151,16 @@ interface Props {
|
|
|
149
151
|
xDefault?: string | null;
|
|
150
152
|
/** Language-switcher entries for the current page. */
|
|
151
153
|
localeSwitch?: LocaleSwitchOption[];
|
|
154
|
+
/** Auto-populated version switcher (`null` when versioning is off or a user selector owns it). */
|
|
155
|
+
versionSelector?: NavSelectorType | null;
|
|
156
|
+
/** Old-version notice for archived pages (`null` on current-docs pages). */
|
|
157
|
+
versionNotice?: {
|
|
158
|
+
message: string;
|
|
159
|
+
latestHref: string;
|
|
160
|
+
latestLabel: string;
|
|
161
|
+
} | null;
|
|
162
|
+
/** Viewed docs version for search filtering (`""` = current; `null`/absent = off). */
|
|
163
|
+
searchVersion?: string | null;
|
|
152
164
|
/**
|
|
153
165
|
* User layout-slot overrides from `components.ts` (`defineComponents`). Each
|
|
154
166
|
* key replaces the matching built-in; unknown keys are ignored. Wired slots:
|
|
@@ -220,6 +232,9 @@ const {
|
|
|
220
232
|
localeAlternates,
|
|
221
233
|
xDefault,
|
|
222
234
|
localeSwitch,
|
|
235
|
+
versionSelector,
|
|
236
|
+
versionNotice,
|
|
237
|
+
searchVersion = null,
|
|
223
238
|
layout = {},
|
|
224
239
|
clientData,
|
|
225
240
|
toc = { enabled: true, maxLevel: 3, minLevel: 2 },
|
|
@@ -499,16 +514,19 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
499
514
|
href="#blume-content">{strings.page.skipToContent}</a
|
|
500
515
|
>
|
|
501
516
|
<Banner banner={banner} strings={strings.banner} />
|
|
517
|
+
<VersionBanner notice={versionNotice ?? null} />
|
|
502
518
|
<HeaderSlot
|
|
503
519
|
askEnabled={askEnabled}
|
|
504
520
|
askStrings={strings.ask}
|
|
505
521
|
layout={layout}
|
|
506
522
|
localeSwitch={localeSwitch}
|
|
523
|
+
versionSelector={versionSelector}
|
|
507
524
|
logo={logo}
|
|
508
525
|
navigation={navigation}
|
|
509
526
|
route={page.route}
|
|
510
527
|
searchEnabled={searchEnabled}
|
|
511
528
|
searchLocale={searchLocale}
|
|
529
|
+
searchVersion={searchVersion}
|
|
512
530
|
navStrings={navStrings}
|
|
513
531
|
searchStrings={strings.search}
|
|
514
532
|
site={site}
|
|
@@ -712,6 +730,8 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
712
730
|
type="button"
|
|
713
731
|
></button>
|
|
714
732
|
<script>
|
|
733
|
+
import { copyText, createCopyFlash } from "../copy-feedback.ts";
|
|
734
|
+
import { syncDrawerInert } from "./drawer-inert.ts";
|
|
715
735
|
import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
|
|
716
736
|
// Registers the <blume-mermaid> custom element (emitted by ```mermaid
|
|
717
737
|
// fences). Mermaid itself is lazy-loaded only on pages that use a diagram.
|
|
@@ -723,32 +743,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
723
743
|
// Tree-shaken out of production builds.
|
|
724
744
|
import "./hydration-hint.ts";
|
|
725
745
|
|
|
726
|
-
|
|
727
|
-
// would stay in the tab order on every page. Mirror the header's
|
|
728
|
-
// `data-blume-nav-open` toggle into `inert`/`aria-hidden` — but only
|
|
729
|
-
// below `lg` (64rem), where the same element isn't the static sidebar.
|
|
730
|
-
const drawer = document.querySelector<HTMLElement>(
|
|
731
|
-
"[data-blume-nav-drawer]"
|
|
732
|
-
);
|
|
733
|
-
if (drawer) {
|
|
734
|
-
const desktop = window.matchMedia("(min-width: 64rem)");
|
|
735
|
-
const syncDrawer = () => {
|
|
736
|
-
const hidden =
|
|
737
|
-
!desktop.matches &&
|
|
738
|
-
!document.documentElement.hasAttribute("data-blume-nav-open");
|
|
739
|
-
drawer.inert = hidden;
|
|
740
|
-
if (hidden) {
|
|
741
|
-
drawer.setAttribute("aria-hidden", "true");
|
|
742
|
-
} else {
|
|
743
|
-
drawer.removeAttribute("aria-hidden");
|
|
744
|
-
}
|
|
745
|
-
};
|
|
746
|
-
syncDrawer();
|
|
747
|
-
desktop.addEventListener("change", syncDrawer);
|
|
748
|
-
new MutationObserver(syncDrawer).observe(document.documentElement, {
|
|
749
|
-
attributeFilter: ["data-blume-nav-open"],
|
|
750
|
-
});
|
|
751
|
-
}
|
|
746
|
+
syncDrawerInert();
|
|
752
747
|
|
|
753
748
|
const svg = (name: string, cls = "") =>
|
|
754
749
|
`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"${cls ? ` class="${cls}"` : ""}>${icons[name]}</svg>`;
|
|
@@ -767,14 +762,6 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
767
762
|
const copiedLabel =
|
|
768
763
|
document.body.getAttribute("data-i18n-copied") || "Copied!";
|
|
769
764
|
|
|
770
|
-
// Shared polite live region: the icon swap is invisible to screen
|
|
771
|
-
// readers, so copy success is announced here. Cleared when a button's
|
|
772
|
-
// checked state resets so the next copy re-announces.
|
|
773
|
-
const copyStatus = document.createElement("span");
|
|
774
|
-
copyStatus.className = "sr-only";
|
|
775
|
-
copyStatus.setAttribute("aria-live", "polite");
|
|
776
|
-
document.body.appendChild(copyStatus);
|
|
777
|
-
|
|
778
765
|
const languageLabels: Record<string, string> = {
|
|
779
766
|
astro: "Astro",
|
|
780
767
|
bash: "Bash",
|
|
@@ -854,7 +841,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
854
841
|
button.classList.toggle(cls, !checked);
|
|
855
842
|
}
|
|
856
843
|
};
|
|
857
|
-
|
|
844
|
+
const flash = createCopyFlash(setChecked, copiedLabel);
|
|
858
845
|
button.addEventListener("click", async () => {
|
|
859
846
|
const code = pre.querySelector("code");
|
|
860
847
|
let text = code?.textContent ?? "";
|
|
@@ -870,20 +857,9 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
870
857
|
}
|
|
871
858
|
text = clone.textContent ?? "";
|
|
872
859
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
} catch {
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
if (resetTimeout) {
|
|
879
|
-
window.clearTimeout(resetTimeout);
|
|
860
|
+
if (await copyText(text)) {
|
|
861
|
+
flash();
|
|
880
862
|
}
|
|
881
|
-
setChecked(true);
|
|
882
|
-
copyStatus.textContent = copiedLabel;
|
|
883
|
-
resetTimeout = window.setTimeout(() => {
|
|
884
|
-
setChecked(false);
|
|
885
|
-
copyStatus.textContent = "";
|
|
886
|
-
}, 1500);
|
|
887
863
|
});
|
|
888
864
|
pre.appendChild(button);
|
|
889
865
|
}
|
|
@@ -19,10 +19,21 @@ interface Props {
|
|
|
19
19
|
strings?: UIStrings["search"];
|
|
20
20
|
/** Active locale to filter results to; omitted disables locale filtering. */
|
|
21
21
|
locale?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Docs version to filter results to (`""` = the current docs — a meaningful
|
|
24
|
+
* value, so `null`/omitted is what disables version filtering).
|
|
25
|
+
*/
|
|
26
|
+
version?: string | null;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
const {
|
|
25
|
-
|
|
29
|
+
const {
|
|
30
|
+
askEnabled = false,
|
|
31
|
+
navigation,
|
|
32
|
+
popularPages,
|
|
33
|
+
strings,
|
|
34
|
+
locale,
|
|
35
|
+
version = null,
|
|
36
|
+
} = Astro.props;
|
|
26
37
|
// Merge over the English baseline per key (rather than `strings ?? …`) so a
|
|
27
38
|
// partial — or empty `{}` — strings object still resolves every label to a
|
|
28
39
|
// default, matching the pattern PageActions uses for its own dictionary.
|
|
@@ -59,13 +70,15 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
59
70
|
data-i18n-popular={s.popular}
|
|
60
71
|
data-i18n-results={s.results}
|
|
61
72
|
data-locale={locale || undefined}
|
|
73
|
+
data-version={version ?? undefined}
|
|
74
|
+
data-versioned={version === null ? undefined : ""}
|
|
62
75
|
>
|
|
63
76
|
{/* The label and shortcut hint wait until `lg`: below it the hamburger and
|
|
64
77
|
inline tab bar share the header row, and a full-width search field would
|
|
65
78
|
press into the language switcher. */}
|
|
66
79
|
<button
|
|
67
80
|
aria-label={s.button}
|
|
68
|
-
class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground lg:min-w-
|
|
81
|
+
class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground lg:min-w-40"
|
|
69
82
|
data-blume-search-open
|
|
70
83
|
type="button"
|
|
71
84
|
>
|
|
@@ -86,11 +99,15 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
86
99
|
>
|
|
87
100
|
<Icon name="search" size={18} />
|
|
88
101
|
<input
|
|
102
|
+
aria-autocomplete="list"
|
|
103
|
+
aria-controls="blume-search-listbox"
|
|
104
|
+
aria-expanded="false"
|
|
89
105
|
aria-label={s.label}
|
|
90
106
|
autocomplete="off"
|
|
91
107
|
class="flex-1 border-0 bg-transparent text-foreground text-sm pointer-coarse:text-base focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
|
|
92
108
|
data-blume-search-input
|
|
93
109
|
placeholder={s.placeholder}
|
|
110
|
+
role="combobox"
|
|
94
111
|
type="search"
|
|
95
112
|
/>
|
|
96
113
|
<kbd class={`${kbd} text-[0.7rem]`}>Esc</kbd>
|
|
@@ -107,8 +124,11 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
107
124
|
>
|
|
108
125
|
</div>
|
|
109
126
|
<div
|
|
127
|
+
aria-label={s.label}
|
|
110
128
|
class="min-h-0 flex-1 scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent overflow-y-auto p-2"
|
|
111
129
|
data-blume-search-results
|
|
130
|
+
id="blume-search-listbox"
|
|
131
|
+
role="listbox"
|
|
112
132
|
>
|
|
113
133
|
</div>
|
|
114
134
|
<p
|
|
@@ -128,15 +148,29 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
128
148
|
class="flex items-center justify-between gap-3 border-border border-t px-3 py-2 text-muted-foreground text-xs"
|
|
129
149
|
>
|
|
130
150
|
{
|
|
131
|
-
locale ? (
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
class="
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
locale || version !== null ? (
|
|
152
|
+
<span class="flex items-center gap-3">
|
|
153
|
+
{locale && (
|
|
154
|
+
<label class="flex cursor-pointer select-none items-center gap-1.5">
|
|
155
|
+
<input
|
|
156
|
+
class="size-3.5 accent-accent"
|
|
157
|
+
data-blume-search-all-locales
|
|
158
|
+
type="checkbox"
|
|
159
|
+
/>
|
|
160
|
+
{s.allLanguages}
|
|
161
|
+
</label>
|
|
162
|
+
)}
|
|
163
|
+
{version !== null && (
|
|
164
|
+
<label class="flex cursor-pointer select-none items-center gap-1.5">
|
|
165
|
+
<input
|
|
166
|
+
class="size-3.5 accent-accent"
|
|
167
|
+
data-blume-search-all-versions
|
|
168
|
+
type="checkbox"
|
|
169
|
+
/>
|
|
170
|
+
{s.allVersions}
|
|
171
|
+
</label>
|
|
172
|
+
)}
|
|
173
|
+
</span>
|
|
140
174
|
) : (
|
|
141
175
|
<span />
|
|
142
176
|
)
|
|
@@ -207,6 +241,26 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
207
241
|
const svg = (name: string, size = 16): string =>
|
|
208
242
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icons[name] ?? ""}</svg>`;
|
|
209
243
|
|
|
244
|
+
// localStorage access throws SecurityError when storage is blocked (Safari
|
|
245
|
+
// "Block All Cookies", some embedded webviews). These guards make blocked
|
|
246
|
+
// storage degrade to session-default preferences instead of throwing inside
|
|
247
|
+
// connectedCallback before the open/keyboard listeners are attached — which
|
|
248
|
+
// would leave search completely dead.
|
|
249
|
+
const readStorage = (key: string): string | null => {
|
|
250
|
+
try {
|
|
251
|
+
return localStorage.getItem(key);
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const writeStorage = (key: string, value: string): void => {
|
|
257
|
+
try {
|
|
258
|
+
localStorage.setItem(key, value);
|
|
259
|
+
} catch {
|
|
260
|
+
// Preference simply isn't remembered.
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
210
264
|
class BlumeSearch extends HTMLElement {
|
|
211
265
|
dialog!: HTMLDialogElement;
|
|
212
266
|
input!: HTMLInputElement;
|
|
@@ -226,6 +280,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
226
280
|
selectedIndex = -1;
|
|
227
281
|
activeSection: string | null = null;
|
|
228
282
|
renderGeneration = 0;
|
|
283
|
+
/** Monotonic id source for option rows (aria-activedescendant). */
|
|
284
|
+
optionSeq = 0;
|
|
229
285
|
previewOn = true;
|
|
230
286
|
devOnlyMsg = "Search is available in the production build.";
|
|
231
287
|
noResultsMsg = "No results found.";
|
|
@@ -239,6 +295,11 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
239
295
|
// reader has opted to search across every language instead.
|
|
240
296
|
locale: string | null = null;
|
|
241
297
|
allLocales = false;
|
|
298
|
+
// The viewed docs version ("" = current; null when versioning is off),
|
|
299
|
+
// and the opt-in to search across every version.
|
|
300
|
+
versioned = false;
|
|
301
|
+
version = "";
|
|
302
|
+
allVersions = false;
|
|
242
303
|
|
|
243
304
|
connectedCallback() {
|
|
244
305
|
this.devOnlyMsg =
|
|
@@ -255,6 +316,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
255
316
|
this.resultsMsg =
|
|
256
317
|
this.getAttribute("data-i18n-results") || this.resultsMsg;
|
|
257
318
|
this.locale = this.getAttribute("data-locale");
|
|
319
|
+
// "" (the current docs) is a real version value, so a presence flag —
|
|
320
|
+
// not the attribute's truthiness — decides whether filtering is on.
|
|
321
|
+
this.versioned = this.hasAttribute("data-versioned");
|
|
322
|
+
this.version = this.getAttribute("data-version") ?? "";
|
|
258
323
|
this.dialog = this.querySelector("[data-blume-search-dialog]")!;
|
|
259
324
|
this.input = this.querySelector("[data-blume-search-input]")!;
|
|
260
325
|
this.grid = this.querySelector("[data-blume-search-grid]")!;
|
|
@@ -273,8 +338,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
273
338
|
this.popular = [];
|
|
274
339
|
}
|
|
275
340
|
|
|
276
|
-
this.previewOn =
|
|
277
|
-
localStorage.getItem("blume-search-preview") !== "0";
|
|
341
|
+
this.previewOn = readStorage("blume-search-preview") !== "0";
|
|
278
342
|
this.applyPreviewState();
|
|
279
343
|
|
|
280
344
|
// Per-language filtering: default to the active locale, with an opt-in
|
|
@@ -283,12 +347,11 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
283
347
|
"[data-blume-search-all-locales]"
|
|
284
348
|
);
|
|
285
349
|
if (allToggle) {
|
|
286
|
-
this.allLocales =
|
|
287
|
-
localStorage.getItem("blume-search-all-locales") === "1";
|
|
350
|
+
this.allLocales = readStorage("blume-search-all-locales") === "1";
|
|
288
351
|
allToggle.checked = this.allLocales;
|
|
289
352
|
allToggle.addEventListener("change", () => {
|
|
290
353
|
this.allLocales = allToggle.checked;
|
|
291
|
-
|
|
354
|
+
writeStorage(
|
|
292
355
|
"blume-search-all-locales",
|
|
293
356
|
this.allLocales ? "1" : "0"
|
|
294
357
|
);
|
|
@@ -296,6 +359,25 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
296
359
|
});
|
|
297
360
|
}
|
|
298
361
|
|
|
362
|
+
// Per-version filtering mirrors the locale toggle: default to the
|
|
363
|
+
// viewed version, with a remembered opt-in to search every version.
|
|
364
|
+
const allVersionsToggle = this.querySelector<HTMLInputElement>(
|
|
365
|
+
"[data-blume-search-all-versions]"
|
|
366
|
+
);
|
|
367
|
+
if (allVersionsToggle) {
|
|
368
|
+
this.allVersions =
|
|
369
|
+
readStorage("blume-search-all-versions") === "1";
|
|
370
|
+
allVersionsToggle.checked = this.allVersions;
|
|
371
|
+
allVersionsToggle.addEventListener("change", () => {
|
|
372
|
+
this.allVersions = allVersionsToggle.checked;
|
|
373
|
+
writeStorage(
|
|
374
|
+
"blume-search-all-versions",
|
|
375
|
+
this.allVersions ? "1" : "0"
|
|
376
|
+
);
|
|
377
|
+
this.render();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
299
381
|
// The handlers accept both ⌘ and Ctrl chords; show the right modifier
|
|
300
382
|
// per platform on the button hint and the footer's preview hint.
|
|
301
383
|
const isApple = /mac|iphone|ipad|ipod/iu.test(navigator.platform);
|
|
@@ -456,11 +538,14 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
456
538
|
|
|
457
539
|
const localeFilter =
|
|
458
540
|
this.locale && !this.allLocales ? this.locale : undefined;
|
|
541
|
+
const versionFilter =
|
|
542
|
+
this.versioned && !this.allVersions ? this.version : undefined;
|
|
459
543
|
let result: Awaited<ReturnType<SearchFn>>;
|
|
460
544
|
try {
|
|
461
545
|
result = await this.searchFn(query, {
|
|
462
546
|
locale: localeFilter,
|
|
463
547
|
section: this.activeSection ?? undefined,
|
|
548
|
+
version: versionFilter,
|
|
464
549
|
});
|
|
465
550
|
} catch {
|
|
466
551
|
// A hosted provider can reject (network error, outage); the results
|
|
@@ -568,7 +653,13 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
568
653
|
header.className =
|
|
569
654
|
"m-0 px-2.5 pt-3 pb-1 font-normal text-muted-foreground text-xs";
|
|
570
655
|
header.textContent = label;
|
|
656
|
+
// The listbox tree allows only group/option descendants: the group
|
|
657
|
+
// carries the label for assistive tech, the visual header is
|
|
658
|
+
// decoration.
|
|
659
|
+
header.setAttribute("aria-hidden", "true");
|
|
571
660
|
const container = document.createElement("div");
|
|
661
|
+
container.setAttribute("role", "group");
|
|
662
|
+
container.setAttribute("aria-label", label);
|
|
572
663
|
this.results.append(header, container);
|
|
573
664
|
return container;
|
|
574
665
|
}
|
|
@@ -603,10 +694,16 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
603
694
|
const excerpt = hit.excerpt
|
|
604
695
|
? `<span class="mt-0.5 line-clamp-2 text-muted-foreground text-xs">${hit.excerpt}</span>`
|
|
605
696
|
: "";
|
|
697
|
+
// A cross-version hit (all-versions search) names its version so the
|
|
698
|
+
// reader knows they're about to leave the docs they're viewing.
|
|
699
|
+
const versionTag =
|
|
700
|
+
this.versioned && hit.version !== undefined && hit.version !== this.version
|
|
701
|
+
? `<span class="ms-2 inline-block rounded-full bg-muted px-1.5 py-0.5 align-middle text-[0.65rem] text-muted-foreground">${escapeHtml(hit.version || "latest")}</span>`
|
|
702
|
+
: "";
|
|
606
703
|
el.innerHTML = `
|
|
607
704
|
<span class="mt-0.5 shrink-0 text-muted-foreground">${svg("file")}</span>
|
|
608
705
|
<span class="flex-1">
|
|
609
|
-
<span class="block truncate font-normal text-foreground text-sm">${hit.title}</span>
|
|
706
|
+
<span class="block truncate font-normal text-foreground text-sm">${hit.title}${versionTag}</span>
|
|
610
707
|
${excerpt}
|
|
611
708
|
</span>`;
|
|
612
709
|
const item: Selectable = { el, hit, kind: "link", url: href };
|
|
@@ -632,6 +729,13 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
632
729
|
}
|
|
633
730
|
|
|
634
731
|
bindRow(item: Selectable) {
|
|
732
|
+
// Options for the combobox pattern: selection is announced through
|
|
733
|
+
// aria-activedescendant on the input (focus never leaves it), so
|
|
734
|
+
// every row needs a stable id and an aria-selected to flip.
|
|
735
|
+
item.el.setAttribute("role", "option");
|
|
736
|
+
item.el.setAttribute("aria-selected", "false");
|
|
737
|
+
this.optionSeq += 1;
|
|
738
|
+
item.el.id = `blume-search-option-${this.optionSeq}`;
|
|
635
739
|
item.el.addEventListener("mouseenter", () => {
|
|
636
740
|
this.selectIndex(this.selectables.indexOf(item));
|
|
637
741
|
});
|
|
@@ -644,9 +748,14 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
644
748
|
}
|
|
645
749
|
|
|
646
750
|
finishRender() {
|
|
751
|
+
this.input.setAttribute(
|
|
752
|
+
"aria-expanded",
|
|
753
|
+
String(this.selectables.length > 0)
|
|
754
|
+
);
|
|
647
755
|
if (this.selectables.length > 0) {
|
|
648
756
|
this.selectIndex(0);
|
|
649
757
|
} else {
|
|
758
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
650
759
|
this.clearPreview();
|
|
651
760
|
}
|
|
652
761
|
}
|
|
@@ -659,11 +768,16 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
659
768
|
if (current) {
|
|
660
769
|
current.el.classList.remove(...ROW_ON);
|
|
661
770
|
current.el.classList.add(...ROW_OFF);
|
|
771
|
+
current.el.setAttribute("aria-selected", "false");
|
|
662
772
|
}
|
|
663
773
|
this.selectedIndex = index;
|
|
664
774
|
const next = this.selectables[index];
|
|
665
775
|
next.el.classList.remove(...ROW_OFF);
|
|
666
776
|
next.el.classList.add(...ROW_ON);
|
|
777
|
+
next.el.setAttribute("aria-selected", "true");
|
|
778
|
+
// Focus stays on the input; the selection is surfaced to assistive
|
|
779
|
+
// tech through the active descendant.
|
|
780
|
+
this.input.setAttribute("aria-activedescendant", next.el.id);
|
|
667
781
|
next.el.scrollIntoView({ block: "nearest" });
|
|
668
782
|
this.updatePreview();
|
|
669
783
|
}
|
|
@@ -714,10 +828,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
714
828
|
|
|
715
829
|
togglePreview() {
|
|
716
830
|
this.previewOn = !this.previewOn;
|
|
717
|
-
|
|
718
|
-
"blume-search-preview",
|
|
719
|
-
this.previewOn ? "1" : "0"
|
|
720
|
-
);
|
|
831
|
+
writeStorage("blume-search-preview", this.previewOn ? "1" : "0");
|
|
721
832
|
this.applyPreviewState();
|
|
722
833
|
this.updatePreview();
|
|
723
834
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { withBase } from "../islands/base-path.ts";
|
|
3
|
+
import Icon from "../Icon.astro";
|
|
4
|
+
|
|
5
|
+
// The "you're viewing an old version" notice, shared by RootLayout and
|
|
6
|
+
// ReferenceLayout so every shell shows the same bar on archived pages. Unlike
|
|
7
|
+
// the announcement Banner it is never dismissible — the reader should know
|
|
8
|
+
// they're on frozen docs for as long as they are. The message and link label
|
|
9
|
+
// arrive fully resolved (localized, `{version}` substituted) from the
|
|
10
|
+
// catch-all, so this component carries no string fallbacks of its own.
|
|
11
|
+
interface Props {
|
|
12
|
+
notice: {
|
|
13
|
+
message: string;
|
|
14
|
+
/** The same page in the latest docs when it exists, else the docs root. */
|
|
15
|
+
latestHref: string;
|
|
16
|
+
latestLabel: string;
|
|
17
|
+
} | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { notice } = Astro.props;
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
notice && (
|
|
25
|
+
<div
|
|
26
|
+
class="flex items-center justify-center gap-x-2 gap-y-0.5 border-amber-300 border-b bg-amber-100 px-10 py-2.5 text-center text-amber-900 text-sm max-sm:flex-wrap dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200"
|
|
27
|
+
data-blume-version-banner
|
|
28
|
+
>
|
|
29
|
+
<span>{notice.message}</span>
|
|
30
|
+
<a
|
|
31
|
+
class="inline-flex items-center gap-1 font-medium underline underline-offset-2"
|
|
32
|
+
href={withBase(notice.latestHref)}
|
|
33
|
+
>
|
|
34
|
+
{notice.latestLabel}
|
|
35
|
+
<Icon name="arrow-right" size={14} />
|
|
36
|
+
</a>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -19,10 +19,15 @@ interface AnalyticsWindow {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export const track = (event: string, props: TrackProps): void => {
|
|
22
|
-
|
|
22
|
+
// Read through `globalThis` so an SSR/import-time call sees `undefined`
|
|
23
|
+
// instead of a bare-identifier ReferenceError.
|
|
24
|
+
const browserWindow = globalThis.window;
|
|
25
|
+
if (browserWindow === undefined) {
|
|
23
26
|
return;
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
// SAFETY: AnalyticsWindow only adds optional provider globals, so any window
|
|
29
|
+
// satisfies the intersection; each provider is feature-checked before use.
|
|
30
|
+
const w = browserWindow as typeof browserWindow & AnalyticsWindow;
|
|
26
31
|
|
|
27
32
|
// Vercel Web Analytics — self-gates to a no-op until `window.va` is set up.
|
|
28
33
|
vercelTrack(event, props);
|
|
@@ -32,7 +37,5 @@ export const track = (event: string, props: TrackProps): void => {
|
|
|
32
37
|
w.gtag?.("event", event, props);
|
|
33
38
|
w.plausible?.(event, { props });
|
|
34
39
|
// Universal hook for any other integration.
|
|
35
|
-
|
|
36
|
-
new CustomEvent("blume:track", { detail: { event, props } })
|
|
37
|
-
);
|
|
40
|
+
w.dispatchEvent(new CustomEvent("blume:track", { detail: { event, props } }));
|
|
38
41
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep the mobile nav drawer out of the tab order while it is closed. The
|
|
3
|
+
* closed drawer is only translated off-canvas, so its links would otherwise
|
|
4
|
+
* stay focusable on every page. Mirrors the header's `data-blume-nav-open`
|
|
5
|
+
* toggle into `inert`/`aria-hidden` — but only below `lg` (64rem), where the
|
|
6
|
+
* same element isn't the static sidebar (RootLayout) or is display-hidden
|
|
7
|
+
* anyway (PageLayout). Shared by both layouts' inline scripts.
|
|
8
|
+
*/
|
|
9
|
+
export const syncDrawerInert = (): void => {
|
|
10
|
+
const drawer = document.querySelector<HTMLElement>("[data-blume-nav-drawer]");
|
|
11
|
+
if (!drawer) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const desktop = window.matchMedia("(min-width: 64rem)");
|
|
15
|
+
const sync = () => {
|
|
16
|
+
const hidden =
|
|
17
|
+
!desktop.matches &&
|
|
18
|
+
!Object.hasOwn(document.documentElement.dataset, "blumeNavOpen");
|
|
19
|
+
drawer.inert = hidden;
|
|
20
|
+
if (hidden) {
|
|
21
|
+
drawer.setAttribute("aria-hidden", "true");
|
|
22
|
+
} else {
|
|
23
|
+
drawer.removeAttribute("aria-hidden");
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
sync();
|
|
27
|
+
desktop.addEventListener("change", sync);
|
|
28
|
+
new MutationObserver(sync).observe(document.documentElement, {
|
|
29
|
+
attributeFilter: ["data-blume-nav-open"],
|
|
30
|
+
});
|
|
31
|
+
};
|
|
@@ -13,7 +13,7 @@ const PATTERNS = [
|
|
|
13
13
|
/server rendered html/iu,
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
-
if (import.meta.env.DEV &&
|
|
16
|
+
if (import.meta.env.DEV && "window" in globalThis) {
|
|
17
17
|
const original = console.error.bind(console);
|
|
18
18
|
let shown = false;
|
|
19
19
|
console.error = (...args: unknown[]) => {
|
|
@@ -209,10 +209,7 @@ export const sidebarForRoute = (
|
|
|
209
209
|
};
|
|
210
210
|
|
|
211
211
|
/** Resolve previous/next pages around the current route. */
|
|
212
|
-
export const getPagination = (
|
|
213
|
-
flat: FlatPage[],
|
|
214
|
-
route: string
|
|
215
|
-
): { prev: FlatPage | null; next: FlatPage | null } => {
|
|
212
|
+
export const getPagination = (flat: FlatPage[], route: string) => {
|
|
216
213
|
const index = flat.findIndex((page) => page.route === route);
|
|
217
214
|
if (index === -1) {
|
|
218
215
|
return { next: null, prev: null };
|