blume 1.5.2 → 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 +85 -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 +10 -1
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +46 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +53 -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
|
@@ -117,8 +117,17 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
117
117
|
data-blume-search-grid
|
|
118
118
|
>
|
|
119
119
|
<div class="flex min-h-0 flex-col md:border-border md:border-e">
|
|
120
|
+
{/* The cap shows two full rows of pills plus most of a third as the
|
|
121
|
+
scroll cue: pills are 26px (PILL_BASE below: text-xs, py-1, 1px
|
|
122
|
+
border — change that string and this cap together) on a 32px pitch
|
|
123
|
+
(gap-1.5) below the row's py-2, so their tops sit at 8, 40 and 72
|
|
124
|
+
inside the 91px scrollport 5.75rem leaves after border-b — the
|
|
125
|
+
third row shows 19px of its pills, a recognizable cut rather than
|
|
126
|
+
a sliver of rounded caps. Pills never wrap (whitespace-nowrap), so
|
|
127
|
+
the arithmetic holds for long section labels; one wider than the
|
|
128
|
+
column is clipped rather than growing a horizontal bar. */}
|
|
120
129
|
<div
|
|
121
|
-
class="flex flex-wrap gap-1.5 border-border border-b px-3 py-2"
|
|
130
|
+
class="flex max-h-[5.75rem] flex-wrap gap-1.5 overflow-x-hidden overflow-y-auto overscroll-contain border-border border-b px-3 py-2 scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent"
|
|
122
131
|
data-blume-search-filters
|
|
123
132
|
hidden
|
|
124
133
|
>
|
|
@@ -235,7 +244,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
235
244
|
const MARK =
|
|
236
245
|
"[&_mark]:rounded-sm [&_mark]:bg-accent/25 [&_mark]:text-inherit";
|
|
237
246
|
const PILL_BASE =
|
|
238
|
-
"inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 font-medium text-xs transition-colors";
|
|
247
|
+
"inline-flex cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-1 font-medium text-xs transition-colors";
|
|
239
248
|
const PILL_ON = "border-transparent bg-foreground text-background";
|
|
240
249
|
const PILL_OFF = "border-border text-muted-foreground hover:text-foreground";
|
|
241
250
|
|
|
@@ -280,6 +289,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
280
289
|
selectables: Selectable[] = [];
|
|
281
290
|
selectedIndex = -1;
|
|
282
291
|
activeSection: string | null = null;
|
|
292
|
+
/** The section pill for `activeSection` in the current filter row. */
|
|
293
|
+
activePill: HTMLElement | undefined;
|
|
283
294
|
renderGeneration = 0;
|
|
284
295
|
/** Monotonic id source for option rows (aria-activedescendant). */
|
|
285
296
|
optionSeq = 0;
|
|
@@ -629,6 +640,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
629
640
|
}
|
|
630
641
|
|
|
631
642
|
renderFilters(sections: { count: number; label: string }[]) {
|
|
643
|
+
this.activePill = undefined;
|
|
632
644
|
if (sections.length < 2) {
|
|
633
645
|
this.filters.hidden = true;
|
|
634
646
|
this.filters.replaceChildren();
|
|
@@ -639,9 +651,15 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
639
651
|
const total = sections.reduce((sum, s) => sum + s.count, 0);
|
|
640
652
|
this.filters.appendChild(this.createPill(this.allMsg, total, null));
|
|
641
653
|
for (const section of sections) {
|
|
642
|
-
this.
|
|
643
|
-
|
|
654
|
+
const pill = this.createPill(
|
|
655
|
+
section.label,
|
|
656
|
+
section.count,
|
|
657
|
+
section.label
|
|
644
658
|
);
|
|
659
|
+
this.filters.appendChild(pill);
|
|
660
|
+
if (section.label === this.activeSection) {
|
|
661
|
+
this.activePill = pill;
|
|
662
|
+
}
|
|
645
663
|
}
|
|
646
664
|
}
|
|
647
665
|
|
|
@@ -649,6 +667,9 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
649
667
|
const button = document.createElement("button");
|
|
650
668
|
button.type = "button";
|
|
651
669
|
const active = this.activeSection === value;
|
|
670
|
+
// A filter chip is a toggle whose only visual state is color; the
|
|
671
|
+
// pressed state gives assistive tech the same information.
|
|
672
|
+
button.setAttribute("aria-pressed", String(active));
|
|
652
673
|
button.className = `${PILL_BASE} ${active ? PILL_ON : PILL_OFF}`;
|
|
653
674
|
button.innerHTML = `${escapeHtml(label)} <span class="opacity-60">${count}</span>`;
|
|
654
675
|
button.addEventListener("click", () => {
|
|
@@ -769,6 +790,17 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
769
790
|
this.input.removeAttribute("aria-activedescendant");
|
|
770
791
|
this.clearPreview();
|
|
771
792
|
}
|
|
793
|
+
// The filter row is height-capped and scrolls. Pills sit in the pool's
|
|
794
|
+
// first-seen order and sections drop out as the query narrows, so the
|
|
795
|
+
// active pill can move rows between renders — and the row's scrollTop
|
|
796
|
+
// survives replaceChildren — so without this the reader can be looking
|
|
797
|
+
// at filtered results with no highlighted pill in view. Only a section
|
|
798
|
+
// pill: with "All" selected every render (each keystroke, each resolved
|
|
799
|
+
// search) would otherwise snap a row the reader scrolled back to the
|
|
800
|
+
// top, moving the pill they were about to click. Issued here, after
|
|
801
|
+
// the results are in the DOM, so it shares one layout flush with
|
|
802
|
+
// selectIndex instead of forcing a second one mid-render.
|
|
803
|
+
this.activePill?.scrollIntoView({ block: "nearest" });
|
|
772
804
|
}
|
|
773
805
|
|
|
774
806
|
selectIndex(index: number) {
|
|
@@ -31,7 +31,10 @@ const { headings, title, variant } = Astro.props;
|
|
|
31
31
|
<blume-toc class="block">
|
|
32
32
|
<ul class="m-0 list-none border-border border-t p-2">
|
|
33
33
|
{headings.map((heading) => (
|
|
34
|
-
<li
|
|
34
|
+
<li
|
|
35
|
+
data-depth={heading.depth}
|
|
36
|
+
style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}
|
|
37
|
+
>
|
|
35
38
|
<a
|
|
36
39
|
class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
|
|
37
40
|
href={`#${heading.slug}`}
|
|
@@ -52,7 +55,10 @@ const { headings, title, variant } = Astro.props;
|
|
|
52
55
|
<blume-toc class="block">
|
|
53
56
|
<ul class="m-0 list-none p-0">
|
|
54
57
|
{headings.map((heading) => (
|
|
55
|
-
<li
|
|
58
|
+
<li
|
|
59
|
+
data-depth={heading.depth}
|
|
60
|
+
style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}
|
|
61
|
+
>
|
|
56
62
|
<a
|
|
57
63
|
class="block py-1.5 text-muted-foreground transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
|
|
58
64
|
href={`#${heading.slug}`}
|
|
@@ -29,9 +29,38 @@
|
|
|
29
29
|
* and again after every client-router swap, which resets `<html>` attributes to
|
|
30
30
|
* the incoming page's server-rendered (theme-less) set.
|
|
31
31
|
*
|
|
32
|
+
* Re-applying on `astro:after-swap` alone is too late for CSS transitions: the
|
|
33
|
+
* router's `swapRootAttributes` drops `data-theme`, then its scroll restoration
|
|
34
|
+
* (`scrollTo`) forces a style flush before `after-swap` fires, so every
|
|
35
|
+
* `transition-colors` element in the new body gets a light-theme computed
|
|
36
|
+
* style and animates to dark once the attribute returns. `astro:before-swap`
|
|
37
|
+
* therefore stamps the current theme onto the incoming document's root, so the
|
|
38
|
+
* attribute swap carries it over and the theme never drops in the first place;
|
|
39
|
+
* the `after-swap` re-apply stays as the fallback that also picks up a
|
|
40
|
+
* preference changed in another tab.
|
|
41
|
+
*
|
|
32
42
|
* Reads `data-mode` — `"system" | "light" | "dark"`.
|
|
33
43
|
*/
|
|
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);})();`;
|
|
44
|
+
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:before-swap",(e)=>{const t=document.documentElement.dataset.theme;if(t){e.newDocument.documentElement.dataset.theme=t;}});document.addEventListener("astro:after-swap",apply);})();`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Keep an embedded Scalar reference on Blume's theme. Scalar picks its own
|
|
48
|
+
* color mode — its \`forceDarkModeState\` option, else the \`colorMode\` key its
|
|
49
|
+
* own toggle writes to localStorage, else the OS setting — and never looks at
|
|
50
|
+
* \`data-theme\`. A reader whose Blume preference disagrees with their OS (a
|
|
51
|
+
* browser that reports dark to \`prefers-color-scheme\` while the docs are set
|
|
52
|
+
* to light, say) got a light navbar over a dark reference.
|
|
53
|
+
*
|
|
54
|
+
* Runs inline right after the reference's mount container, before Scalar's
|
|
55
|
+
* client module executes: it rewrites the container's \`data-configuration\` to
|
|
56
|
+
* force the current theme, which also hides Scalar's own toggle. Scalar reads
|
|
57
|
+
* that override once at mount and keys its styles off \`dark-mode\`/\`light-mode\`
|
|
58
|
+
* classes on \`<body>\`, so later flips of Blume's toggle are mirrored onto
|
|
59
|
+
* those classes directly. A container whose author already set
|
|
60
|
+
* \`forceDarkModeState\` or \`darkMode\` through the \`scalar\` escape hatch is left
|
|
61
|
+
* to Scalar, per the contract that author options win.
|
|
62
|
+
*/
|
|
63
|
+
export const SCALAR_THEME_INIT_SCRIPT = `(()=>{const d=document.documentElement;const mode=()=>d.dataset.theme==="dark"?"dark":"light";let pinned=0;for(const el of document.querySelectorAll("[data-scalar-client]")){let c;try{c=JSON.parse(el.dataset.configuration||"{}");}catch{continue;}if("forceDarkModeState" in c||"darkMode" in c){continue;}c.forceDarkModeState=mode();el.dataset.configuration=JSON.stringify(c);pinned+=1;}if(!pinned){return;}new MutationObserver(()=>{const dark=mode()==="dark";document.body.classList.toggle("dark-mode",dark);document.body.classList.toggle("light-mode",!dark);}).observe(d,{attributes:true,attributeFilter:["data-theme"]});})();`;
|
|
35
64
|
|
|
36
65
|
/**
|
|
37
66
|
* Hide a previously-dismissed banner before it can flash in — and again after
|
|
@@ -41,6 +70,29 @@ export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.
|
|
|
41
70
|
*/
|
|
42
71
|
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);})();`;
|
|
43
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Keep the page styled across client-router swaps. Astro hoists the CSS of a
|
|
75
|
+
* component rendered after the head has streamed (the page's MDX content, the
|
|
76
|
+
* WebMcp island) into the **body** as `<link rel="stylesheet">` tags — and the
|
|
77
|
+
* client router only preloads and persists stylesheets it finds in the head.
|
|
78
|
+
* A swapped-in body `<link>` applies asynchronously, so every navigation to a
|
|
79
|
+
* page with body CSS painted one or two completely unstyled frames (giant raw
|
|
80
|
+
* SVG logo, default link colors) before the sheet kicked in — even when the
|
|
81
|
+
* same sheet was already loaded on the outgoing page, because the swap throws
|
|
82
|
+
* the old body (and its link element) away.
|
|
83
|
+
*
|
|
84
|
+
* Two listeners close the gap. `astro:before-preparation` wraps the router's
|
|
85
|
+
* loader: after the next document is fetched, any of its body stylesheets not
|
|
86
|
+
* already in the live head are appended there and awaited, so their rules
|
|
87
|
+
* apply before the swap. `astro:before-swap` then moves the incoming
|
|
88
|
+
* document's body stylesheets into its head, where the router's head diff
|
|
89
|
+
* keeps the already-loaded copy (matched by `href`) instead of re-inserting a
|
|
90
|
+
* fresh, not-yet-applied link — and drops it again on a later navigation to a
|
|
91
|
+
* page that doesn't use it. A sheet that fails to load resolves rather than
|
|
92
|
+
* wedging the navigation; the page renders as it would have without this.
|
|
93
|
+
*/
|
|
94
|
+
export const SWAP_STYLESHEET_INIT_SCRIPT = `(()=>{const sel='body link[rel="stylesheet"]';document.addEventListener("astro:before-preparation",(e)=>{const load=e.loader;e.loader=async()=>{await load();const links=[...e.newDocument.querySelectorAll(sel)].filter((l)=>!document.head.querySelector('link[rel="stylesheet"][href="'+l.getAttribute("href")+'"]'));await Promise.all(links.map((l)=>new Promise((done)=>{const c=document.createElement("link");for(const a of l.attributes){c.setAttribute(a.name,a.value);}c.onload=done;c.onerror=done;document.head.append(c);})));};});document.addEventListener("astro:before-swap",(e)=>{for(const l of e.newDocument.querySelectorAll(sel)){e.newDocument.head.append(l);}});})();`;
|
|
95
|
+
|
|
44
96
|
/**
|
|
45
97
|
* Keep the sidebar's scroll useful across page changes. The sidebar is its own
|
|
46
98
|
* scroll container, reborn scrolled to the top whenever its markup is rebuilt —
|
|
@@ -15,9 +15,14 @@ const { source } = Astro.props;
|
|
|
15
15
|
const spec = specs[source];
|
|
16
16
|
|
|
17
17
|
// OpenAPI declares `servers` as an array of URLs; AsyncAPI as a named map of
|
|
18
|
-
// host/protocol/pathname
|
|
18
|
+
// host/protocol/pathname; a GraphQL schema names no server, so its configured
|
|
19
|
+
// live endpoint stands in. All flatten into one list of address chips.
|
|
19
20
|
const addresses: string[] = [];
|
|
20
|
-
if (spec?.kind === "
|
|
21
|
+
if (spec?.kind === "graphql") {
|
|
22
|
+
if (spec.endpoint) {
|
|
23
|
+
addresses.push(spec.endpoint);
|
|
24
|
+
}
|
|
25
|
+
} else if (spec?.kind === "asyncapi") {
|
|
21
26
|
const servers = (spec.document as AsyncApiDocument).servers ?? {};
|
|
22
27
|
for (const server of Object.values(servers)) {
|
|
23
28
|
if (server?.host) {
|
|
@@ -38,7 +43,12 @@ if (spec?.kind === "asyncapi") {
|
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
|
-
|
|
46
|
+
let addressLabel = "Base URL";
|
|
47
|
+
if (spec?.kind === "asyncapi") {
|
|
48
|
+
addressLabel = "Servers";
|
|
49
|
+
} else if (spec?.kind === "graphql") {
|
|
50
|
+
addressLabel = "Endpoint";
|
|
51
|
+
}
|
|
42
52
|
---
|
|
43
53
|
|
|
44
54
|
{
|
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
schemaOf,
|
|
18
18
|
} from "./async.ts";
|
|
19
19
|
import type { MessageSample } from "./async-snippets.ts";
|
|
20
|
+
import { DEPRECATED_LABEL_CLASS } from "../colors.ts";
|
|
20
21
|
import { asyncSampleLanguages } from "./async-snippets.ts";
|
|
22
|
+
import { languageSamplePanels } from "./sample-panels.ts";
|
|
21
23
|
import { buildMessage, defaultMessageValues } from "./message.ts";
|
|
22
24
|
import { messageModel } from "./message-model.ts";
|
|
23
25
|
import MessageComposer from "./MessageComposer.astro";
|
|
@@ -30,6 +32,7 @@ import ParametersTable from "./ParametersTable.astro";
|
|
|
30
32
|
import SchemaTable from "./SchemaTable.astro";
|
|
31
33
|
import type { SecuritySchemeLike } from "./security.ts";
|
|
32
34
|
import { resolveAsyncApiSecurity } from "./security.ts";
|
|
35
|
+
import OperationPanel from "./OperationPanel.astro";
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* The AsyncAPI front-end of the operation page: message payloads instead of
|
|
@@ -107,17 +110,7 @@ const sample: MessageSample | null = model
|
|
|
107
110
|
: null;
|
|
108
111
|
const languages = asyncSampleLanguages(spec?.codeSamples ?? [], protocol);
|
|
109
112
|
const samplePanels = sample
|
|
110
|
-
? await
|
|
111
|
-
languages.map(async (language) => ({
|
|
112
|
-
html: await highlightCode(language.build(sample), language.lang, {
|
|
113
|
-
icons: false,
|
|
114
|
-
themes: data.config.codeThemes,
|
|
115
|
-
}),
|
|
116
|
-
key: language.id,
|
|
117
|
-
label: language.label,
|
|
118
|
-
lang: language.id,
|
|
119
|
-
}))
|
|
120
|
-
)
|
|
113
|
+
? await languageSamplePanels(languages, sample, data.config.codeThemes)
|
|
121
114
|
: [];
|
|
122
115
|
|
|
123
116
|
const operationBindings = bindingGroups(operation?.bindings);
|
|
@@ -135,7 +128,7 @@ const channelBindings = bindingGroups(channel?.bindings);
|
|
|
135
128
|
{ref.path}
|
|
136
129
|
</code>
|
|
137
130
|
{ref.deprecated && (
|
|
138
|
-
<span class=
|
|
131
|
+
<span class={DEPRECATED_LABEL_CLASS}>
|
|
139
132
|
deprecated
|
|
140
133
|
</span>
|
|
141
134
|
)}
|
|
@@ -238,13 +231,13 @@ const channelBindings = bindingGroups(channel?.bindings);
|
|
|
238
231
|
{(spec.playground.enabled ||
|
|
239
232
|
samplePanels.length > 0 ||
|
|
240
233
|
messagePanels.length > 0) && (
|
|
241
|
-
<
|
|
234
|
+
<OperationPanel>
|
|
242
235
|
{spec.playground.enabled && model && <MessageComposer model={model} />}
|
|
243
236
|
<div class="not-prose flex flex-col gap-6">
|
|
244
237
|
<PanelTabs copy heading="Example" panels={samplePanels} />
|
|
245
238
|
<PanelTabs heading="Message" panels={messagePanels} />
|
|
246
239
|
</div>
|
|
247
|
-
</
|
|
240
|
+
</OperationPanel>
|
|
248
241
|
)}
|
|
249
242
|
</div>
|
|
250
243
|
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { withBase } from "../islands/base-path.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A schema name (or full type display like `[Pet!]!`) rendered as a link when
|
|
6
|
+
* the named type has a reference page, plain muted text otherwise — the one
|
|
7
|
+
* chip every GraphQL component uses for type and operation names. `class`
|
|
8
|
+
* carries sizing (`text-xs`, `text-sm`); some containers set the size
|
|
9
|
+
* themselves and pass none.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
name: string;
|
|
13
|
+
route?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { name, route, class: className } = Astro.props;
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
route ? (
|
|
22
|
+
<a
|
|
23
|
+
class:list={["font-mono text-accent hover:underline", className]}
|
|
24
|
+
href={withBase(route)}
|
|
25
|
+
>
|
|
26
|
+
{name}
|
|
27
|
+
</a>
|
|
28
|
+
) : (
|
|
29
|
+
<span class:list={["font-mono text-muted-foreground", className]}>
|
|
30
|
+
{name}
|
|
31
|
+
</span>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { GraphqlFieldRow } from "./graphql-helpers.ts";
|
|
3
|
+
import { isOutputField } from "./graphql-helpers.ts";
|
|
4
|
+
import GraphqlChip from "./GraphqlChip.astro";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The row list shared by every GraphQL member table: a type page's fields or
|
|
8
|
+
* input fields, and an operation page's arguments (enum values render their
|
|
9
|
+
* own simpler list in `GraphqlType.astro` — they carry no type). Types link
|
|
10
|
+
* to their pages via `routes`; a name with no page (a built-in scalar)
|
|
11
|
+
* renders as plain text.
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
title: string;
|
|
15
|
+
rows: GraphqlFieldRow[];
|
|
16
|
+
routes: Map<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { title, rows, routes } = Astro.props;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether a value must be supplied by the caller: an argument or input field
|
|
23
|
+
* in a non-null position with no default. Output fields never take the badge
|
|
24
|
+
* — a response field isn't something the reader provides, and its `!` already
|
|
25
|
+
* shows in the type display — and a defaulted non-null argument is omittable
|
|
26
|
+
* per the GraphQL spec.
|
|
27
|
+
*/
|
|
28
|
+
const isRequired = (row: GraphqlFieldRow): boolean =>
|
|
29
|
+
!isOutputField(row) &&
|
|
30
|
+
row.type.display.endsWith("!") &&
|
|
31
|
+
row.default === undefined;
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
rows.length > 0 && (
|
|
36
|
+
<section class="mt-6">
|
|
37
|
+
<div
|
|
38
|
+
aria-level="2"
|
|
39
|
+
class="mb-2 font-semibold text-foreground text-sm"
|
|
40
|
+
role="heading"
|
|
41
|
+
>
|
|
42
|
+
{title}
|
|
43
|
+
</div>
|
|
44
|
+
<div class="not-prose rounded-blume border border-border px-4">
|
|
45
|
+
{rows.map((row) => {
|
|
46
|
+
const route = routes.get(row.type.name);
|
|
47
|
+
const args = isOutputField(row) ? row.args : [];
|
|
48
|
+
return (
|
|
49
|
+
<div class="border-border border-t py-3 first:border-t-0">
|
|
50
|
+
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
51
|
+
<code class="font-mono text-foreground text-sm">
|
|
52
|
+
{row.name}
|
|
53
|
+
</code>
|
|
54
|
+
<GraphqlChip
|
|
55
|
+
class="text-xs"
|
|
56
|
+
name={row.type.display}
|
|
57
|
+
route={route}
|
|
58
|
+
/>
|
|
59
|
+
{isRequired(row) && (
|
|
60
|
+
<span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
|
|
61
|
+
required
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
{row.deprecationReason !== undefined && (
|
|
65
|
+
<span class="font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide line-through">
|
|
66
|
+
deprecated
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
{row.description && (
|
|
71
|
+
<div
|
|
72
|
+
class="mt-1 text-muted-foreground text-sm"
|
|
73
|
+
set:text={row.description}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
{"default" in row && row.default !== undefined && (
|
|
77
|
+
<div class="mt-1 text-muted-foreground text-xs">
|
|
78
|
+
Default:{" "}
|
|
79
|
+
<code class="rounded bg-muted px-1 py-0.5 text-foreground">
|
|
80
|
+
{row.default}
|
|
81
|
+
</code>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
{row.deprecationReason && (
|
|
85
|
+
<div class="mt-1 text-muted-foreground text-xs">
|
|
86
|
+
Deprecated: <span set:text={row.deprecationReason} />
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{args.length > 0 && (
|
|
90
|
+
<div class="mt-2 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-xs">
|
|
91
|
+
<span class="text-muted-foreground">Arguments:</span>
|
|
92
|
+
{args.map((arg) => (
|
|
93
|
+
<span class="inline-flex items-baseline gap-1">
|
|
94
|
+
<code class="rounded bg-muted px-1 py-0.5 text-foreground">
|
|
95
|
+
{arg.name}
|
|
96
|
+
</code>
|
|
97
|
+
<GraphqlChip
|
|
98
|
+
name={arg.type.display}
|
|
99
|
+
route={routes.get(arg.type.name)}
|
|
100
|
+
/>
|
|
101
|
+
</span>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
</section>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
import data from "blume:data";
|
|
3
|
+
import specs from "blume:openapi";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
GraphqlDocument,
|
|
7
|
+
GraphqlOperationKind,
|
|
8
|
+
} from "../../openapi/graphql.ts";
|
|
9
|
+
import {
|
|
10
|
+
graphqlRootField,
|
|
11
|
+
isGraphqlOperationKind,
|
|
12
|
+
} from "../../openapi/graphql.ts";
|
|
13
|
+
import { highlightCode } from "../../markdown/index.ts";
|
|
14
|
+
import { DEPRECATED_LABEL_CLASS } from "../colors.ts";
|
|
15
|
+
import {
|
|
16
|
+
exampleQuery,
|
|
17
|
+
exampleResponse,
|
|
18
|
+
exampleVariables,
|
|
19
|
+
graphqlPlaygroundModel,
|
|
20
|
+
graphqlRoutes,
|
|
21
|
+
} from "./graphql-helpers.ts";
|
|
22
|
+
import { buildRequest, defaultValues } from "./request.ts";
|
|
23
|
+
import { languageSamplePanels } from "./sample-panels.ts";
|
|
24
|
+
import { sampleLanguages } from "./snippets.ts";
|
|
25
|
+
import GraphqlChip from "./GraphqlChip.astro";
|
|
26
|
+
import GraphqlFieldsTable from "./GraphqlFieldsTable.astro";
|
|
27
|
+
import GraphqlType from "./GraphqlType.astro";
|
|
28
|
+
import MethodBadge from "./MethodBadge.astro";
|
|
29
|
+
import PanelTabs from "./PanelTabs.astro";
|
|
30
|
+
import Playground from "./Playground.astro";
|
|
31
|
+
import OperationPanel from "./OperationPanel.astro";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The GraphQL front-end of the reference page, dispatched from
|
|
35
|
+
* `Operation.astro`: root fields render as operations — arguments, return
|
|
36
|
+
* type, a generated example query, live code samples, and the "Try it"
|
|
37
|
+
* playground (the OpenAPI panel reused verbatim: a GraphQL call is a plain
|
|
38
|
+
* POST whose JSON body carries the query and variables) — while named types
|
|
39
|
+
* defer to `GraphqlType.astro`. Subscriptions are the exception: they don't
|
|
40
|
+
* run over a plain POST, so their pages show the generated operation instead
|
|
41
|
+
* of the playground and HTTP samples.
|
|
42
|
+
*/
|
|
43
|
+
interface Props {
|
|
44
|
+
source: string;
|
|
45
|
+
id: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { source, id } = Astro.props;
|
|
49
|
+
const spec = specs[source];
|
|
50
|
+
const ref = spec?.operations[id];
|
|
51
|
+
const document = (spec?.document ?? { roots: {}, types: {} }) as GraphqlDocument;
|
|
52
|
+
const isOperation = ref !== undefined && isGraphqlOperationKind(ref.method);
|
|
53
|
+
const field = ref && isOperation ? graphqlRootField(document, ref) : undefined;
|
|
54
|
+
const routes = spec ? graphqlRoutes(spec) : new Map<string, string>();
|
|
55
|
+
|
|
56
|
+
const query =
|
|
57
|
+
field && isOperation
|
|
58
|
+
? exampleQuery(document, field, ref.method as GraphqlOperationKind)
|
|
59
|
+
: "";
|
|
60
|
+
const variables = field ? exampleVariables(document, field) : undefined;
|
|
61
|
+
// Subscriptions run over a stateful transport (WebSocket/SSE) that a standard
|
|
62
|
+
// GraphQL server does not serve from a single-response POST — so, like
|
|
63
|
+
// AsyncAPI's streaming operations, their pages get no playground and no HTTP
|
|
64
|
+
// code samples; the generated operation itself is shown instead.
|
|
65
|
+
const isSubscription = ref?.method === "subscription";
|
|
66
|
+
const model =
|
|
67
|
+
spec && field && !isSubscription
|
|
68
|
+
? graphqlPlaygroundModel(spec, query, variables)
|
|
69
|
+
: null;
|
|
70
|
+
const sample = model ? buildRequest(model, defaultValues(model)) : null;
|
|
71
|
+
const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
72
|
+
|
|
73
|
+
const highlight = (code: string, lang: string) =>
|
|
74
|
+
highlightCode(code, lang, { icons: false, themes: data.config.codeThemes });
|
|
75
|
+
const requestPanels = sample
|
|
76
|
+
? await languageSamplePanels(languages, sample, data.config.codeThemes)
|
|
77
|
+
: [];
|
|
78
|
+
const operationPanels =
|
|
79
|
+
field && isSubscription
|
|
80
|
+
? [
|
|
81
|
+
{ html: await highlight(query, "graphql"), key: "operation", label: "Operation" },
|
|
82
|
+
...(variables
|
|
83
|
+
? [
|
|
84
|
+
{
|
|
85
|
+
html: await highlight(JSON.stringify(variables, null, 2), "json"),
|
|
86
|
+
key: "variables",
|
|
87
|
+
label: "Variables",
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
: []),
|
|
91
|
+
]
|
|
92
|
+
: [];
|
|
93
|
+
const responsePanels =
|
|
94
|
+
field && spec
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
html: await highlight(
|
|
98
|
+
JSON.stringify(exampleResponse(document, field), null, 2),
|
|
99
|
+
"json"
|
|
100
|
+
),
|
|
101
|
+
key: "example",
|
|
102
|
+
label: "Example",
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
: [];
|
|
106
|
+
|
|
107
|
+
const returnRoute = field ? routes.get(field.type.name) : undefined;
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
ref && !isOperation ? (
|
|
112
|
+
spec && <GraphqlType refOp={ref} spec={spec} />
|
|
113
|
+
) : !(spec && ref && field) ? (
|
|
114
|
+
<div class="text-muted-foreground">This API operation could not be found.</div>
|
|
115
|
+
) : (
|
|
116
|
+
<div class="not-prose">
|
|
117
|
+
<div class="mb-6 flex flex-wrap items-center gap-3">
|
|
118
|
+
<MethodBadge method={ref.method} />
|
|
119
|
+
<code class="break-all font-mono text-foreground text-sm">
|
|
120
|
+
{field.name}
|
|
121
|
+
</code>
|
|
122
|
+
{field.deprecationReason !== undefined && (
|
|
123
|
+
<span class={DEPRECATED_LABEL_CLASS}>
|
|
124
|
+
deprecated
|
|
125
|
+
</span>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
<div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
|
|
129
|
+
<div>
|
|
130
|
+
{field.deprecationReason && (
|
|
131
|
+
<p class="mb-4 text-muted-foreground text-sm">
|
|
132
|
+
Deprecated: <span set:text={field.deprecationReason} />
|
|
133
|
+
</p>
|
|
134
|
+
)}
|
|
135
|
+
<GraphqlFieldsTable
|
|
136
|
+
routes={routes}
|
|
137
|
+
rows={field.args}
|
|
138
|
+
title="Arguments"
|
|
139
|
+
/>
|
|
140
|
+
<section class="mt-6">
|
|
141
|
+
<div
|
|
142
|
+
aria-level="2"
|
|
143
|
+
class="mb-2 font-semibold text-foreground text-sm"
|
|
144
|
+
role="heading"
|
|
145
|
+
>
|
|
146
|
+
Returns
|
|
147
|
+
</div>
|
|
148
|
+
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
149
|
+
<GraphqlChip
|
|
150
|
+
class="text-sm"
|
|
151
|
+
name={field.type.display}
|
|
152
|
+
route={returnRoute}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
{document.types[field.type.name]?.description && (
|
|
156
|
+
<p
|
|
157
|
+
class="mt-1 text-muted-foreground text-sm"
|
|
158
|
+
set:text={document.types[field.type.name]?.description}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
</section>
|
|
162
|
+
</div>
|
|
163
|
+
{((sample && model) || isSubscription) && (
|
|
164
|
+
<OperationPanel>
|
|
165
|
+
{sample && model && spec.playground.enabled && (
|
|
166
|
+
<Playground
|
|
167
|
+
model={model}
|
|
168
|
+
operation={id}
|
|
169
|
+
proxy={spec.playground.proxy}
|
|
170
|
+
slug={spec.slug}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
<div class="not-prose flex flex-col gap-6">
|
|
174
|
+
{isSubscription ? (
|
|
175
|
+
<PanelTabs copy heading="Operation" panels={operationPanels} />
|
|
176
|
+
) : (
|
|
177
|
+
<PanelTabs copy heading="Request" panels={requestPanels} />
|
|
178
|
+
)}
|
|
179
|
+
<PanelTabs heading="Response" panels={responsePanels} />
|
|
180
|
+
</div>
|
|
181
|
+
</OperationPanel>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|