blume 1.5.3 → 1.6.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 +79 -0
- package/dist/cli/index.js +3639 -1377
- package/dist/cli/index.js.map +103 -91
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +23 -1
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +8 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +117 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +23 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +21 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +21 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +27 -0
- package/docs/configuration/theming.mdx +17 -0
- package/docs/content/components.mdx +7 -0
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +23 -14
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/generate.ts +183 -116
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +3 -5
- package/src/astro/templates.ts +125 -76
- package/src/cli/commands/build.ts +84 -15
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +29 -8
- package/src/components/layout/PageLayout.astro +2 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +42 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +30 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +18 -2
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +11 -0
- package/src/core/includes.ts +632 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +226 -35
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +25 -2
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +2 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +2 -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 +2 -0
- package/src/core/ui-packs/el.ts +2 -0
- package/src/core/ui-packs/es.ts +2 -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 -0
- 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 +2 -0
- package/src/core/ui-packs/hu.ts +2 -0
- package/src/core/ui-packs/id.ts +2 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +2 -0
- package/src/core/ui-packs/ko.ts +2 -0
- package/src/core/ui-packs/nl.ts +2 -0
- package/src/core/ui-packs/no.ts +2 -0
- package/src/core/ui-packs/pl.ts +2 -0
- package/src/core/ui-packs/pt-br.ts +2 -0
- package/src/core/ui-packs/pt.ts +2 -0
- package/src/core/ui-packs/ro.ts +2 -0
- package/src/core/ui-packs/ru.ts +2 -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 +2 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +2 -0
- package/src/core/ui-packs/uk.ts +2 -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/core/version-cut.ts +21 -3
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +89 -13
- package/src/openapi/render-mdx.ts +48 -8
- package/src/openapi/scalar.ts +5 -12
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +11 -0
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +65 -11
- package/src/theme/fonts.ts +189 -16
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light-dismiss for the site's `<details>`-based dropdowns: the page actions
|
|
3
|
+
* (Export / Open in chat / Connect to MCP), the header language switcher and
|
|
4
|
+
* the nav selectors all render the same floating `<details>` + absolute panel,
|
|
5
|
+
* and a native `<details>` only closes when its own `<summary>` is clicked
|
|
6
|
+
* again, leaving the panel hanging over the page. Any dropdown that opts in
|
|
7
|
+
* with `data-blume-dropdown` gets the four legs of a real menu:
|
|
8
|
+
*
|
|
9
|
+
* - a pointer press outside the open panel closes it;
|
|
10
|
+
* - Escape closes it, restoring focus to the trigger when the keypress
|
|
11
|
+
* originated inside the panel;
|
|
12
|
+
* - keyboard focus leaving the panel closes it;
|
|
13
|
+
* - the window losing focus closes it — the only signal the parent document
|
|
14
|
+
* gets when a press lands inside an `<iframe>` embed, since pointer events
|
|
15
|
+
* in a child browsing context never propagate up.
|
|
16
|
+
*
|
|
17
|
+
* Every listener sits on `document`/`window` once per real page load and
|
|
18
|
+
* queries the DOM live, so client-router swaps (which rebuild the dropdown
|
|
19
|
+
* markup) need no re-init and never stack duplicate listeners. Per-component
|
|
20
|
+
* behavior — one-open-at-a-time within a group, viewport flipping — stays in
|
|
21
|
+
* the component that owns it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const OPEN_DROPDOWN = "details[data-blume-dropdown][open]";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Every dropdown currently open. Each component group keeps itself to one
|
|
28
|
+
* open panel, but a page holds several groups (the header selectors and the
|
|
29
|
+
* page actions), and a browser that doesn't focus a `<summary>` on click lets
|
|
30
|
+
* a keyboard-opened panel join a pointer-opened one — so the dismissal rules
|
|
31
|
+
* apply to all of them, never just the first in DOM order.
|
|
32
|
+
*/
|
|
33
|
+
const openDropdowns = (): HTMLDetailsElement[] => [
|
|
34
|
+
...document.querySelectorAll<HTMLDetailsElement>(OPEN_DROPDOWN),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Close `open`. `restoreFocus` moves focus back to its trigger, which a
|
|
39
|
+
* keyboard dismissal wants and a pointer or focus-driven one does not.
|
|
40
|
+
*/
|
|
41
|
+
const dismiss = (open: HTMLDetailsElement, restoreFocus: boolean): void => {
|
|
42
|
+
open.open = false;
|
|
43
|
+
if (restoreFocus) {
|
|
44
|
+
open.querySelector<HTMLElement>("summary")?.focus();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const isInside = (
|
|
49
|
+
dropdown: HTMLDetailsElement,
|
|
50
|
+
target: EventTarget | null
|
|
51
|
+
): boolean => target instanceof Node && dropdown.contains(target);
|
|
52
|
+
|
|
53
|
+
const onPointerDown = (event: PointerEvent): void => {
|
|
54
|
+
for (const open of openDropdowns()) {
|
|
55
|
+
if (!isInside(open, event.target)) {
|
|
56
|
+
dismiss(open, false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
62
|
+
// `isComposing`: an IME cancel arrives as Escape and isn't a dismissal.
|
|
63
|
+
if (event.key !== "Escape" || event.isComposing) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// An Escape aimed at a modal surface stacked on top (the search dialog
|
|
67
|
+
// traps focus inside itself) dismisses that surface only — the same guard
|
|
68
|
+
// the Ask panel applies. Everything outside the modal is inert, so a focus
|
|
69
|
+
// restore here could not land anyway.
|
|
70
|
+
if (event.target instanceof Element && event.target.closest("dialog")) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Only a keypress that originated inside a panel gets its focus returned
|
|
74
|
+
// to that trigger; yanking focus from an unrelated control the user had
|
|
75
|
+
// moved on to would be a surprise.
|
|
76
|
+
for (const open of openDropdowns()) {
|
|
77
|
+
dismiss(open, isInside(open, event.target));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const onFocusOut = (event: FocusEvent): void => {
|
|
82
|
+
// Only focus *leaving a panel* counts: a move between two unrelated controls
|
|
83
|
+
// (a dialog handing focus back to its trigger, say) must not close a panel
|
|
84
|
+
// that never held focus. And a null `relatedTarget` means focus went
|
|
85
|
+
// nowhere focusable (a click on plain content, the panel being hidden, the
|
|
86
|
+
// window blurring) — the pointer and blur legs own those, and closing here
|
|
87
|
+
// would hide a panel item before its own click lands in browsers that
|
|
88
|
+
// don't focus buttons on press.
|
|
89
|
+
const { relatedTarget } = event;
|
|
90
|
+
if (!(relatedTarget instanceof Node)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const open of openDropdowns()) {
|
|
94
|
+
if (isInside(open, event.target) && !open.contains(relatedTarget)) {
|
|
95
|
+
dismiss(open, false);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const onWindowBlur = (): void => {
|
|
101
|
+
for (const open of openDropdowns()) {
|
|
102
|
+
dismiss(open, false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let installed = false;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Register the document/window listeners. Idempotent: every component that
|
|
110
|
+
* renders a dropdown calls this from its own script, and a page may render
|
|
111
|
+
* several of them.
|
|
112
|
+
*/
|
|
113
|
+
export const installDropdownDismiss = (): void => {
|
|
114
|
+
if (installed) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
installed = true;
|
|
118
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
119
|
+
document.addEventListener("keydown", onKeyDown);
|
|
120
|
+
document.addEventListener("focusout", onFocusOut);
|
|
121
|
+
window.addEventListener("blur", onWindowBlur);
|
|
122
|
+
};
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Emits the optimized @font-face declarations + preload links for each
|
|
3
3
|
// configured font (Astro's Fonts API). Renders nothing when no fonts are set;
|
|
4
4
|
// the CSS variables match the astro.config `fonts:` entries. Preloads are
|
|
5
|
-
// narrowed to the weights above-the-fold text renders in
|
|
5
|
+
// narrowed to the weights above-the-fold text renders in and, for providers
|
|
6
|
+
// that split faces by subset, to the subsets the site's locales need (see
|
|
6
7
|
// `theme/fonts.ts`); a bare string entry — an older generated template — keeps
|
|
7
8
|
// the previous preload-everything behavior.
|
|
8
9
|
import { Font } from "astro:assets";
|
|
@@ -14,16 +15,22 @@ interface Props {
|
|
|
14
15
|
|
|
15
16
|
const { cssVars } = Astro.props;
|
|
16
17
|
|
|
18
|
+
/** The preload filters for one head entry: every weight × every subset. */
|
|
19
|
+
const preloadFilters = (head: FontHead) =>
|
|
20
|
+
head.preloadWeights.flatMap((weight) =>
|
|
21
|
+
head.preloadSubsets
|
|
22
|
+
? head.preloadSubsets.map((subset) => ({
|
|
23
|
+
style: "normal" as const,
|
|
24
|
+
subset,
|
|
25
|
+
weight,
|
|
26
|
+
}))
|
|
27
|
+
: [{ style: "normal" as const, weight }]
|
|
28
|
+
);
|
|
29
|
+
|
|
17
30
|
const entries = cssVars.map((value) =>
|
|
18
31
|
typeof value === "string"
|
|
19
32
|
? { cssVariable: value, preload: true as const }
|
|
20
|
-
: {
|
|
21
|
-
cssVariable: value.cssVariable,
|
|
22
|
-
preload: value.preloadWeights.map((weight) => ({
|
|
23
|
-
style: "normal" as const,
|
|
24
|
-
weight,
|
|
25
|
-
})),
|
|
26
|
-
}
|
|
33
|
+
: { cssVariable: value.cssVariable, preload: preloadFilters(value) }
|
|
27
34
|
);
|
|
28
35
|
---
|
|
29
36
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import Ask from "blume:ask";
|
|
3
3
|
import data from "blume:data";
|
|
4
4
|
import { withBase } from "../islands/base-path.ts";
|
|
5
|
+
import { isExternalUrl } from "../../core/base-path.ts";
|
|
5
6
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
6
7
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
7
8
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
@@ -120,6 +121,19 @@ const tabsNavClass = hasSidebar
|
|
|
120
121
|
const iconButton =
|
|
121
122
|
"inline-flex size-9 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground";
|
|
122
123
|
|
|
124
|
+
// Header links: plain actions are secondary and hide below `sm`, where the
|
|
125
|
+
// header has room for the logo and the drawer toggle and nothing else. The one
|
|
126
|
+
// call to action hides there too — but only when a drawer toggle is actually
|
|
127
|
+
// competing for the space. A page with no drawer (a PageLayout landing page
|
|
128
|
+
// with no tabs) has nowhere else to surface it, so it stays. `withBase` passes
|
|
129
|
+
// external, protocol-relative and fragment hrefs through untouched.
|
|
130
|
+
const actionClass =
|
|
131
|
+
"hidden rounded-full px-3 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:text-foreground sm:inline-flex";
|
|
132
|
+
const ctaClass = [
|
|
133
|
+
showNavToggle ? "hidden sm:inline-flex" : "inline-flex",
|
|
134
|
+
"items-center gap-1 rounded-full bg-accent px-3.5 py-1.5 font-medium text-accent-foreground text-sm transition-opacity hover:opacity-90",
|
|
135
|
+
].join(" ");
|
|
136
|
+
|
|
123
137
|
// Delegated handlers for the theme toggle, mobile nav drawer, and banner
|
|
124
138
|
// dismiss. Living with the header means every page that renders it — docs pages
|
|
125
139
|
// and custom pages alike — gets identical behavior from one source.
|
|
@@ -212,6 +226,36 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
|
|
|
212
226
|
)
|
|
213
227
|
}
|
|
214
228
|
<div class="flex items-center gap-2">
|
|
229
|
+
{
|
|
230
|
+
/* Plain links first, then the one filled button: the reader's eye lands
|
|
231
|
+
on the button because nothing beside it competes. */
|
|
232
|
+
(navigation.actions ?? []).map((action) => {
|
|
233
|
+
const external = isExternalUrl(action.href);
|
|
234
|
+
return (
|
|
235
|
+
<a
|
|
236
|
+
class={actionClass}
|
|
237
|
+
href={withBase(action.href)}
|
|
238
|
+
rel={external ? "noreferrer" : undefined}
|
|
239
|
+
target={external ? "_blank" : undefined}
|
|
240
|
+
>
|
|
241
|
+
{action.label}
|
|
242
|
+
</a>
|
|
243
|
+
);
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
{
|
|
247
|
+
navigation.cta && (
|
|
248
|
+
<a
|
|
249
|
+
class={ctaClass}
|
|
250
|
+
href={withBase(navigation.cta.href)}
|
|
251
|
+
rel={isExternalUrl(navigation.cta.href) ? "noreferrer" : undefined}
|
|
252
|
+
target={isExternalUrl(navigation.cta.href) ? "_blank" : undefined}
|
|
253
|
+
>
|
|
254
|
+
{navigation.cta.label}
|
|
255
|
+
<Icon name="chevron-right" size={14} />
|
|
256
|
+
</a>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
215
259
|
{
|
|
216
260
|
navigation.repoUrl && (
|
|
217
261
|
<a
|
|
@@ -20,7 +20,7 @@ const menuRowClass =
|
|
|
20
20
|
|
|
21
21
|
{
|
|
22
22
|
options.length > 1 && (
|
|
23
|
-
<details class="group relative">
|
|
23
|
+
<details class="group relative" data-blume-dropdown>
|
|
24
24
|
<summary
|
|
25
25
|
aria-label={label}
|
|
26
26
|
class={`${iconButton} list-none [&::-webkit-details-marker]:hidden`}
|
|
@@ -56,3 +56,11 @@ const menuRowClass =
|
|
|
56
56
|
</details>
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
<script>
|
|
61
|
+
import { installDropdownDismiss } from "../dropdown-dismiss.ts";
|
|
62
|
+
|
|
63
|
+
// Outside-click / Escape / focus-out dismissal, shared with the nav
|
|
64
|
+
// selectors and the page actions.
|
|
65
|
+
installDropdownDismiss();
|
|
66
|
+
</script>
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { withBase } from "../islands/base-path.ts";
|
|
3
3
|
// A top-level navigation selector: a dropdown that switches
|
|
4
4
|
// between partitions of the site — a product, a version, or any grouped set of
|
|
5
|
-
// destinations (`navigation.selectors` in the config).
|
|
6
|
-
// <details>/<summary> like the language switcher
|
|
5
|
+
// destinations (`navigation.selectors` in the config). Built on
|
|
6
|
+
// <details>/<summary> like the language switcher; the only script is the
|
|
7
|
+
// shared light-dismiss so an open panel doesn't linger over the page.
|
|
7
8
|
import type { NavSelector } from "../../core/types.ts";
|
|
8
9
|
import Icon from "../Icon.astro";
|
|
9
10
|
import { isUnderPath } from "../../core/navigation.ts";
|
|
@@ -40,7 +41,7 @@ const menuRowClass =
|
|
|
40
41
|
|
|
41
42
|
{
|
|
42
43
|
selector.items.length > 0 && (
|
|
43
|
-
<details class="group relative">
|
|
44
|
+
<details class="group relative" data-blume-dropdown>
|
|
44
45
|
<summary
|
|
45
46
|
aria-label={selector.label}
|
|
46
47
|
class={`${iconButton} list-none [&::-webkit-details-marker]:hidden`}
|
|
@@ -85,3 +86,11 @@ const menuRowClass =
|
|
|
85
86
|
</details>
|
|
86
87
|
)
|
|
87
88
|
}
|
|
89
|
+
|
|
90
|
+
<script>
|
|
91
|
+
import { installDropdownDismiss } from "../dropdown-dismiss.ts";
|
|
92
|
+
|
|
93
|
+
// Outside-click / Escape / focus-out dismissal, shared with the language
|
|
94
|
+
// switcher and the page actions.
|
|
95
|
+
installDropdownDismiss();
|
|
96
|
+
</script>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
3
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
4
4
|
import type { NavNode } from "../../core/types.ts";
|
|
5
|
+
import { methodColor } from "../colors.ts";
|
|
5
6
|
import Icon from "../Icon.astro";
|
|
6
7
|
import { withBase } from "../islands/base-path.ts";
|
|
7
8
|
import Self from "./NavTree.astro";
|
|
@@ -44,24 +45,11 @@ const n = { ...EN_UI.nav, ...strings };
|
|
|
44
45
|
|
|
45
46
|
const badgeBase =
|
|
46
47
|
"shrink-0 rounded-full px-1.5 py-0.5 font-medium text-[0.65rem] leading-none";
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
GET: "bg-green-500/15 text-green-700 dark:text-green-300",
|
|
53
|
-
HEAD: "bg-muted text-muted-foreground",
|
|
54
|
-
OPTIONS: "bg-muted text-muted-foreground",
|
|
55
|
-
PATCH: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
|
|
56
|
-
POST: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
57
|
-
PUT: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
|
|
58
|
-
RECEIVE: "bg-teal-500/15 text-teal-700 dark:text-teal-300",
|
|
59
|
-
SEND: "bg-violet-500/15 text-violet-700 dark:text-violet-300",
|
|
60
|
-
};
|
|
61
|
-
const badgeClassFor = (badge: string): string => {
|
|
62
|
-
const method = METHOD_BADGE[badge.toUpperCase()];
|
|
63
|
-
return method ? `${badgeBase} ${method}` : badgeClass;
|
|
64
|
-
};
|
|
48
|
+
// HTTP-method, AsyncAPI-action, and GraphQL operation-kind badges (from a
|
|
49
|
+
// reference's sidebar) share MethodBadge's table; every other badge falls
|
|
50
|
+
// through to the same neutral style.
|
|
51
|
+
const badgeClassFor = (badge: string): string =>
|
|
52
|
+
`${badgeBase} ${methodColor(badge)}`;
|
|
65
53
|
const deprecatedClass =
|
|
66
54
|
"shrink-0 rounded-full bg-amber-500/10 px-1.5 py-0.5 font-medium text-[0.65rem] text-amber-700 leading-none dark:text-amber-300";
|
|
67
55
|
|
|
@@ -24,6 +24,12 @@ interface Props {
|
|
|
24
24
|
openInChat?: readonly OpenInChatProvider[];
|
|
25
25
|
/** Localized action labels. */
|
|
26
26
|
strings?: UIStrings["actions"];
|
|
27
|
+
/**
|
|
28
|
+
* Draw the top rule and spacing that separate the block from the table of
|
|
29
|
+
* contents above it. The rail passes `false` when no contents rendered, so
|
|
30
|
+
* the block does not open an otherwise empty column with a divider.
|
|
31
|
+
*/
|
|
32
|
+
divider?: boolean;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
const {
|
|
@@ -35,6 +41,7 @@ const {
|
|
|
35
41
|
mcpName,
|
|
36
42
|
openInChat,
|
|
37
43
|
strings,
|
|
44
|
+
divider = true,
|
|
38
45
|
} = Astro.props;
|
|
39
46
|
// Merge over the English defaults so a label missing from a translation (or from
|
|
40
47
|
// a not-yet-regenerated snapshot) still renders instead of coming out blank.
|
|
@@ -93,8 +100,9 @@ const menuRowClass =
|
|
|
93
100
|
---
|
|
94
101
|
|
|
95
102
|
<div
|
|
96
|
-
class="
|
|
103
|
+
class:list={["space-y-0.5", divider && "mt-8 border-border border-t pt-4"]}
|
|
97
104
|
data-blume-page-actions
|
|
105
|
+
data-i18n-chat-prompt={a.openInChatPrompt}
|
|
98
106
|
data-i18n-copied={a.copied}
|
|
99
107
|
data-i18n-generating={a.generating}
|
|
100
108
|
data-mcp-name={mcpName ?? undefined}
|
|
@@ -122,7 +130,7 @@ const menuRowClass =
|
|
|
122
130
|
|
|
123
131
|
{
|
|
124
132
|
(exportPdf || exportEpub) && (
|
|
125
|
-
<details class="group relative">
|
|
133
|
+
<details class="group relative" data-blume-dropdown>
|
|
126
134
|
<summary
|
|
127
135
|
class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
|
|
128
136
|
>
|
|
@@ -159,7 +167,7 @@ const menuRowClass =
|
|
|
159
167
|
|
|
160
168
|
{
|
|
161
169
|
providers.length > 0 && (
|
|
162
|
-
<details class="group relative">
|
|
170
|
+
<details class="group relative" data-blume-dropdown>
|
|
163
171
|
<summary
|
|
164
172
|
class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
|
|
165
173
|
>
|
|
@@ -199,7 +207,7 @@ const menuRowClass =
|
|
|
199
207
|
|
|
200
208
|
{
|
|
201
209
|
mcpUrl && (
|
|
202
|
-
<details class="group relative">
|
|
210
|
+
<details class="group relative" data-blume-dropdown>
|
|
203
211
|
<summary
|
|
204
212
|
class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
|
|
205
213
|
>
|
|
@@ -252,6 +260,7 @@ const menuRowClass =
|
|
|
252
260
|
|
|
253
261
|
<script>
|
|
254
262
|
import { copyText, flashLabel } from "../copy-feedback.ts";
|
|
263
|
+
import { installDropdownDismiss } from "../dropdown-dismiss.ts";
|
|
255
264
|
import { prefixBase } from "../islands/base-path.ts";
|
|
256
265
|
import { rafThrottle } from "../raf-throttle.ts";
|
|
257
266
|
|
|
@@ -329,6 +338,11 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
329
338
|
menu.classList.toggle("mb-1", flipUp);
|
|
330
339
|
};
|
|
331
340
|
|
|
341
|
+
const openDropdown = () =>
|
|
342
|
+
document.querySelector<HTMLDetailsElement>(
|
|
343
|
+
"[data-blume-page-actions] details[open]"
|
|
344
|
+
);
|
|
345
|
+
|
|
332
346
|
// rAF-coalesced so a live resize drag re-reads layout once per frame, not
|
|
333
347
|
// once per event, while the open menu still tracks the viewport. Registered
|
|
334
348
|
// once at module scope (this bundle runs once per real page load) and
|
|
@@ -336,15 +350,17 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
336
350
|
window.addEventListener(
|
|
337
351
|
"resize",
|
|
338
352
|
rafThrottle(() => {
|
|
339
|
-
const open =
|
|
340
|
-
"[data-blume-page-actions] details[open]"
|
|
341
|
-
);
|
|
353
|
+
const open = openDropdown();
|
|
342
354
|
if (open) {
|
|
343
355
|
placeMenu(open);
|
|
344
356
|
}
|
|
345
357
|
})
|
|
346
358
|
);
|
|
347
359
|
|
|
360
|
+
// Outside-click / Escape / focus-out dismissal is the shared <details>
|
|
361
|
+
// dropdown behavior (also the header language switcher and nav selectors).
|
|
362
|
+
installDropdownDismiss();
|
|
363
|
+
|
|
348
364
|
// Per-page setup, run on the initial load and again after every
|
|
349
365
|
// client-router swap: the swap rebuilds the actions menu from
|
|
350
366
|
// server-rendered markup, so every handler below binds to fresh elements.
|
|
@@ -379,9 +395,14 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
379
395
|
const copiedLabel = root.getAttribute("data-i18n-copied") || "Copied!";
|
|
380
396
|
const generatingLabel =
|
|
381
397
|
root.getAttribute("data-i18n-generating") || "Generating…";
|
|
398
|
+
// Always stamped: the frontmatter merges `EN_UI.actions` under any
|
|
399
|
+
// override, so the English default lives only in core/i18n-ui.ts.
|
|
400
|
+
const promptTemplate = root.getAttribute("data-i18n-chat-prompt") ?? "";
|
|
382
401
|
const absolute = new URL(md, location.origin).href;
|
|
402
|
+
// Replacer function, not a string: a URL may contain `$`, which a string
|
|
403
|
+
// replacement would interpret as a substitution pattern.
|
|
383
404
|
const query = encodeURIComponent(
|
|
384
|
-
|
|
405
|
+
promptTemplate.replaceAll("{url}", () => absolute)
|
|
385
406
|
);
|
|
386
407
|
|
|
387
408
|
for (const link of root.querySelectorAll("[data-open-in]")) {
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
BlumeFavicon,
|
|
23
23
|
BlumeLogo,
|
|
24
24
|
} from "../../core/data.ts";
|
|
25
|
+
import data from "blume:data";
|
|
25
26
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
26
27
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
27
28
|
import type { FontHead } from "../../theme/fonts.ts";
|
|
@@ -217,6 +218,7 @@ const structuredData =
|
|
|
217
218
|
base: import.meta.env.BASE_URL,
|
|
218
219
|
breadcrumbs: [],
|
|
219
220
|
description,
|
|
221
|
+
identity: data.config.identity,
|
|
220
222
|
locale,
|
|
221
223
|
route,
|
|
222
224
|
siteName: site.title,
|
|
@@ -10,7 +10,11 @@ import WebMcp from "./WebMcp.astro";
|
|
|
10
10
|
import Banner from "./Banner.astro";
|
|
11
11
|
import Favicon from "./Favicon.astro";
|
|
12
12
|
import Fonts from "./Fonts.astro";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
BANNER_INIT_SCRIPT,
|
|
15
|
+
SCALAR_THEME_INIT_SCRIPT,
|
|
16
|
+
THEME_INIT_SCRIPT,
|
|
17
|
+
} from "./head-scripts.ts";
|
|
14
18
|
import Header from "./Header.astro";
|
|
15
19
|
|
|
16
20
|
// A minimal shell for the Scalar API/AsyncAPI reference: Blume's banner + navbar
|
|
@@ -129,6 +133,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
129
133
|
id="blume-content"
|
|
130
134
|
>
|
|
131
135
|
<slot />
|
|
136
|
+
<script is:inline set:html={SCALAR_THEME_INIT_SCRIPT} />
|
|
132
137
|
</div>
|
|
133
138
|
<WebMcp />
|
|
134
139
|
</body>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import data from "blume:data";
|
|
2
3
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
3
4
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
4
5
|
import { resolveDateFormatOptions } from "../../core/date-format.ts";
|
|
@@ -12,6 +13,15 @@ import type {
|
|
|
12
13
|
NavSelector as NavSelectorType,
|
|
13
14
|
} from "../../core/types.ts";
|
|
14
15
|
import "blume:theme";
|
|
16
|
+
// medium-zoom's default build injects its stylesheet into <head> as a bare
|
|
17
|
+
// <style> the moment the module evaluates — once per document. The client
|
|
18
|
+
// router's head swap drops every non-persisted head element on the first
|
|
19
|
+
// navigation, and the cached module never re-injects, so the zoom cursor
|
|
20
|
+
// vanished and `close()` hung forever waiting for a `transitionend` its
|
|
21
|
+
// (now absent) transition rule never fires. Importing the stylesheet here
|
|
22
|
+
// puts it in the page CSS the router carries across swaps; the script below
|
|
23
|
+
// loads the `pure` build, which ships no injection.
|
|
24
|
+
import "medium-zoom/dist/style.css";
|
|
15
25
|
import { ClientRouter } from "astro:transitions";
|
|
16
26
|
import type { FontHead } from "../../theme/fonts.ts";
|
|
17
27
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
@@ -23,6 +33,7 @@ import {
|
|
|
23
33
|
import { buildStructuredData, toIso } from "../../seo/jsonld.ts";
|
|
24
34
|
import { normalizeXHandle } from "../../seo/x-handle.ts";
|
|
25
35
|
import { withBase } from "../islands/base-path.ts";
|
|
36
|
+
import { isExternalUrl } from "../../core/base-path.ts";
|
|
26
37
|
import Analytics from "./Analytics.astro";
|
|
27
38
|
import WebMcp from "./WebMcp.astro";
|
|
28
39
|
import Banner from "./Banner.astro";
|
|
@@ -295,6 +306,9 @@ const tocHeadings =
|
|
|
295
306
|
toc.enabled && showToc
|
|
296
307
|
? headings.filter((h) => h.depth >= toc.minLevel && h.depth <= toc.maxLevel)
|
|
297
308
|
: [];
|
|
309
|
+
// One predicate for both the outline and the page-actions divider under it,
|
|
310
|
+
// so the rail cannot draw a rule under contents that were never rendered.
|
|
311
|
+
const hasToc = tocHeadings.length > 0;
|
|
298
312
|
let gridClass =
|
|
299
313
|
"lg:grid-cols-[17.5rem_minmax(0,1fr)] xl:grid-cols-[17.5rem_minmax(0,1fr)_17.5rem]";
|
|
300
314
|
if (isBare) {
|
|
@@ -387,6 +401,7 @@ const structuredData =
|
|
|
387
401
|
base: import.meta.env.BASE_URL,
|
|
388
402
|
breadcrumbs: crumbs,
|
|
389
403
|
description,
|
|
404
|
+
identity: data.config.identity,
|
|
390
405
|
locale,
|
|
391
406
|
modified: lastModified,
|
|
392
407
|
pageType,
|
|
@@ -524,6 +539,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
524
539
|
</head>
|
|
525
540
|
<body
|
|
526
541
|
class="bg-background font-sans text-foreground antialiased"
|
|
542
|
+
data-blume-code-copy=""
|
|
527
543
|
data-blume-code-wrap={codeWrap ? "" : undefined}
|
|
528
544
|
data-blume-image-zoom={imageZoom ? "" : undefined}
|
|
529
545
|
data-i18n-copied={actionStrings.copied}
|
|
@@ -579,7 +595,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
579
595
|
>
|
|
580
596
|
<ul class="m-0 list-none p-0">
|
|
581
597
|
{navigation.featured.map((link) => {
|
|
582
|
-
const external =
|
|
598
|
+
const external = isExternalUrl(link.href);
|
|
583
599
|
return (
|
|
584
600
|
<li>
|
|
585
601
|
<a
|
|
@@ -680,11 +696,15 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
680
696
|
strings={navStrings}
|
|
681
697
|
wide={isApiOperation}
|
|
682
698
|
/>
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
699
|
+
{
|
|
700
|
+
hasToc && (
|
|
701
|
+
<TableOfContentsSlot
|
|
702
|
+
headings={tocHeadings}
|
|
703
|
+
title={strings.toc.title}
|
|
704
|
+
variant="mobile"
|
|
705
|
+
/>
|
|
706
|
+
)
|
|
707
|
+
}
|
|
688
708
|
<PageHeaderSlot headings={tocHeadings} page={page} route={page.route} />
|
|
689
709
|
<article class:list={[articleClass]} dir={contentDir}>
|
|
690
710
|
<slot />
|
|
@@ -715,12 +735,15 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
715
735
|
class="sticky top-16 hidden h-[calc(100dvh-4rem)] scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent overflow-y-auto px-4 pt-6 pb-10 text-sm xl:block"
|
|
716
736
|
data-blume-toc
|
|
717
737
|
>
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
738
|
+
{hasToc && (
|
|
739
|
+
<TableOfContentsSlot
|
|
740
|
+
headings={tocHeadings}
|
|
741
|
+
title={strings.toc.title}
|
|
742
|
+
variant="desktop"
|
|
743
|
+
/>
|
|
744
|
+
)}
|
|
723
745
|
<PageActions
|
|
746
|
+
divider={hasToc}
|
|
724
747
|
editUrl={editUrl}
|
|
725
748
|
exportEpub={exportEpub}
|
|
726
749
|
exportPdf={exportPdf}
|
|
@@ -910,7 +933,9 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
910
933
|
if (zoomTargets.length > 0) {
|
|
911
934
|
// Lazy: pages without a zoomable image never load the library,
|
|
912
935
|
// matching how mermaid is only fetched on pages with a diagram.
|
|
913
|
-
const { default: mediumZoom } = await import(
|
|
936
|
+
const { default: mediumZoom } = await import(
|
|
937
|
+
"medium-zoom/dist/pure"
|
|
938
|
+
);
|
|
914
939
|
mediumZoom(zoomTargets, {
|
|
915
940
|
background:
|
|
916
941
|
"color-mix(in oklab, var(--color-background) 80%, transparent)",
|
|
@@ -927,7 +952,9 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
927
952
|
</script>
|
|
928
953
|
<style is:global>
|
|
929
954
|
/* medium-zoom ships no z-index; lift the lightbox above the chrome
|
|
930
|
-
(header/sidebar) the way the previous z-[100] overlay sat.
|
|
955
|
+
(header/sidebar) the way the previous z-[100] overlay sat. Its own
|
|
956
|
+
rules come from the `medium-zoom/dist/style.css` import in the
|
|
957
|
+
frontmatter, not runtime injection. */
|
|
931
958
|
.medium-zoom-overlay {
|
|
932
959
|
backdrop-filter: blur(4px);
|
|
933
960
|
z-index: 100;
|
|
@@ -936,8 +963,8 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
936
963
|
z-index: 101;
|
|
937
964
|
}
|
|
938
965
|
@media (prefers-reduced-motion: reduce) {
|
|
939
|
-
/* html prefix outranks the library's
|
|
940
|
-
|
|
966
|
+
/* html prefix outranks the library's rules regardless of sheet
|
|
967
|
+
order; its transition declarations carry !important. */
|
|
941
968
|
html .medium-zoom-image,
|
|
942
969
|
html .medium-zoom-overlay {
|
|
943
970
|
transition: none !important;
|