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.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/cli/index.js +3639 -1377
  3. package/dist/cli/index.js.map +103 -91
  4. package/dist/types/ai/component-markdown.d.ts +79 -0
  5. package/dist/types/components/layout/nav-utils.d.ts +60 -0
  6. package/dist/types/core/base-path.d.ts +9 -0
  7. package/dist/types/core/config-input.d.ts +206 -4
  8. package/dist/types/core/config.d.ts +6 -4
  9. package/dist/types/core/data.d.ts +23 -1
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +8 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +117 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +23 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +21 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/_snippets/include-demo.mdx +7 -0
  21. package/docs/advanced/api-reference.mdx +3 -3
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/graphql.mdx +84 -0
  24. package/docs/advanced/meta.ts +8 -1
  25. package/docs/configuration/ai.mdx +21 -3
  26. package/docs/configuration/index.mdx +24 -0
  27. package/docs/configuration/search.mdx +13 -1
  28. package/docs/configuration/seo.mdx +27 -0
  29. package/docs/configuration/theming.mdx +17 -0
  30. package/docs/content/components.mdx +7 -0
  31. package/docs/content/includes.mdx +68 -0
  32. package/docs/content/meta.ts +1 -0
  33. package/docs/content/navigation.mdx +25 -0
  34. package/docs/content/sources.mdx +42 -1
  35. package/docs/content/syntax.mdx +69 -1
  36. package/docs/content/versioning.mdx +15 -9
  37. package/docs/reference/cli.mdx +2 -1
  38. package/package.json +23 -14
  39. package/skills/blume-migrate/SKILL.md +16 -7
  40. package/skills/blume-migrate/references/docusaurus.md +5 -3
  41. package/skills/blume-migrate/references/fumadocs.md +10 -2
  42. package/skills/blume-migrate/references/mintlify.md +3 -2
  43. package/skills/blume-migrate/references/nextra.md +2 -2
  44. package/skills/blume-migrate/references/starlight.md +1 -1
  45. package/src/ai/agent-readability.ts +2 -1
  46. package/src/ai/ask-data.ts +2 -1
  47. package/src/ai/component-markdown.ts +199 -36
  48. package/src/ai/llms.ts +93 -6
  49. package/src/ai/markdown.ts +2 -2
  50. package/src/ai/mcp/discovery.ts +10 -2
  51. package/src/ai/mcp/server.ts +74 -2
  52. package/src/astro/generate.ts +183 -116
  53. package/src/astro/include-hmr.ts +81 -0
  54. package/src/astro/include-refresh.ts +0 -0
  55. package/src/astro/index.ts +3 -5
  56. package/src/astro/templates.ts +125 -76
  57. package/src/cli/commands/build.ts +84 -15
  58. package/src/cli/init/questions.ts +1 -0
  59. package/src/cli/init/scaffold.ts +27 -4
  60. package/src/components/colors.ts +142 -0
  61. package/src/components/content/Badge.astro +5 -12
  62. package/src/components/content/Callout.astro +19 -36
  63. package/src/components/content/Card.astro +15 -21
  64. package/src/components/content/Component.astro +10 -1
  65. package/src/components/content/GithubInfo.astro +28 -9
  66. package/src/components/content/Tabs.astro +27 -5
  67. package/src/components/content/github-info.ts +20 -5
  68. package/src/components/dropdown-dismiss.ts +122 -0
  69. package/src/components/layout/Fonts.astro +15 -8
  70. package/src/components/layout/Header.astro +44 -0
  71. package/src/components/layout/LanguageSwitcher.astro +9 -1
  72. package/src/components/layout/NavSelector.astro +12 -3
  73. package/src/components/layout/NavTree.astro +6 -18
  74. package/src/components/layout/PageActions.astro +29 -8
  75. package/src/components/layout/PageLayout.astro +10 -1
  76. package/src/components/layout/ReferenceLayout.astro +6 -1
  77. package/src/components/layout/RootLayout.astro +46 -15
  78. package/src/components/layout/Search.astro +36 -4
  79. package/src/components/layout/TableOfContents.astro +8 -2
  80. package/src/components/layout/head-scripts.ts +53 -1
  81. package/src/components/openapi/ApiOverview.astro +13 -3
  82. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  83. package/src/components/openapi/GraphqlChip.astro +33 -0
  84. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  85. package/src/components/openapi/GraphqlOperation.astro +186 -0
  86. package/src/components/openapi/GraphqlType.astro +154 -0
  87. package/src/components/openapi/MethodBadge.astro +3 -14
  88. package/src/components/openapi/Operation.astro +12 -5
  89. package/src/components/openapi/OperationPanel.astro +43 -0
  90. package/src/components/openapi/RequestPanel.astro +5 -10
  91. package/src/components/openapi/Responses.astro +1 -16
  92. package/src/components/openapi/graphql-helpers.ts +466 -0
  93. package/src/components/openapi/playground-client.ts +15 -0
  94. package/src/components/openapi/sample-panels.ts +45 -0
  95. package/src/components/openapi/snippets.ts +13 -35
  96. package/src/core/base-path.ts +11 -0
  97. package/src/core/config-input.ts +209 -2
  98. package/src/core/config.ts +6 -4
  99. package/src/core/content-assets.ts +15 -4
  100. package/src/core/data.ts +18 -2
  101. package/src/core/diagnostics.ts +8 -0
  102. package/src/core/frontmatter.ts +20 -8
  103. package/src/core/github.ts +71 -0
  104. package/src/core/graph.ts +22 -8
  105. package/src/core/heading-markers.ts +96 -0
  106. package/src/core/i18n-ui.ts +11 -0
  107. package/src/core/includes.ts +632 -0
  108. package/src/core/last-modified.ts +36 -11
  109. package/src/core/links.ts +79 -13
  110. package/src/core/meta.ts +2 -1
  111. package/src/core/nav-diagnostics.ts +11 -2
  112. package/src/core/navigation.ts +27 -6
  113. package/src/core/project-graph.ts +61 -9
  114. package/src/core/schema.ts +226 -35
  115. package/src/core/server-features.ts +5 -9
  116. package/src/core/sources/github-releases.ts +2 -2
  117. package/src/core/sources/normalize.ts +502 -115
  118. package/src/core/sources/notion.ts +43 -8
  119. package/src/core/sources/obsidian.ts +1038 -0
  120. package/src/core/sources/read.ts +36 -1
  121. package/src/core/sources/resolve.ts +34 -1
  122. package/src/core/sources/types.ts +28 -6
  123. package/src/core/sources/watch.ts +12 -8
  124. package/src/core/tsconfig-aliases.ts +48 -35
  125. package/src/core/types.ts +25 -2
  126. package/src/core/ui-packs/ar.ts +1 -0
  127. package/src/core/ui-packs/bg.ts +2 -0
  128. package/src/core/ui-packs/bn.ts +1 -0
  129. package/src/core/ui-packs/ca.ts +2 -0
  130. package/src/core/ui-packs/cs.ts +1 -0
  131. package/src/core/ui-packs/da.ts +1 -0
  132. package/src/core/ui-packs/de.ts +2 -0
  133. package/src/core/ui-packs/el.ts +2 -0
  134. package/src/core/ui-packs/es.ts +2 -0
  135. package/src/core/ui-packs/fa.ts +1 -0
  136. package/src/core/ui-packs/fi.ts +1 -0
  137. package/src/core/ui-packs/fr.ts +2 -0
  138. package/src/core/ui-packs/he.ts +1 -0
  139. package/src/core/ui-packs/hi.ts +1 -0
  140. package/src/core/ui-packs/hr.ts +2 -0
  141. package/src/core/ui-packs/hu.ts +2 -0
  142. package/src/core/ui-packs/id.ts +2 -0
  143. package/src/core/ui-packs/it.ts +1 -0
  144. package/src/core/ui-packs/ja.ts +2 -0
  145. package/src/core/ui-packs/ko.ts +2 -0
  146. package/src/core/ui-packs/nl.ts +2 -0
  147. package/src/core/ui-packs/no.ts +2 -0
  148. package/src/core/ui-packs/pl.ts +2 -0
  149. package/src/core/ui-packs/pt-br.ts +2 -0
  150. package/src/core/ui-packs/pt.ts +2 -0
  151. package/src/core/ui-packs/ro.ts +2 -0
  152. package/src/core/ui-packs/ru.ts +2 -0
  153. package/src/core/ui-packs/sk.ts +1 -0
  154. package/src/core/ui-packs/sr.ts +1 -0
  155. package/src/core/ui-packs/sv.ts +2 -0
  156. package/src/core/ui-packs/th.ts +1 -0
  157. package/src/core/ui-packs/tr.ts +2 -0
  158. package/src/core/ui-packs/uk.ts +2 -0
  159. package/src/core/ui-packs/vi.ts +1 -0
  160. package/src/core/ui-packs/zh-tw.ts +1 -0
  161. package/src/core/ui-packs/zh.ts +1 -0
  162. package/src/core/version-cut.ts +21 -3
  163. package/src/core/yaml.ts +26 -0
  164. package/src/deploy/function-bundle.ts +251 -0
  165. package/src/eval/schema.ts +3 -1
  166. package/src/markdown/code-title.ts +22 -16
  167. package/src/markdown/features.ts +21 -0
  168. package/src/markdown/fence-meta.ts +50 -0
  169. package/src/markdown/heading-anchors.ts +198 -37
  170. package/src/markdown/include.ts +247 -0
  171. package/src/markdown/index.ts +43 -34
  172. package/src/markdown/language-icon.ts +2 -2
  173. package/src/markdown/mdast.ts +7 -3
  174. package/src/markdown/ts2js.ts +264 -0
  175. package/src/openapi/asyncapi.ts +4 -1
  176. package/src/openapi/graphql-build.ts +293 -0
  177. package/src/openapi/graphql.ts +212 -0
  178. package/src/openapi/model.ts +38 -5
  179. package/src/openapi/parse.ts +34 -0
  180. package/src/openapi/proxy.ts +30 -5
  181. package/src/openapi/references.ts +89 -13
  182. package/src/openapi/render-mdx.ts +48 -8
  183. package/src/openapi/scalar.ts +5 -12
  184. package/src/openapi/source.ts +91 -23
  185. package/src/registry/eject.ts +11 -0
  186. package/src/search/documents.ts +229 -37
  187. package/src/search/orama-index.ts +9 -5
  188. package/src/seo/jsonld.ts +293 -51
  189. package/src/theme/code-block-padding.ts +16 -0
  190. package/src/theme/entry.ts +65 -11
  191. package/src/theme/fonts.ts +189 -16
  192. package/src/translate/prompts.ts +2 -0
  193. package/src/translate/run.ts +7 -0
  194. 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.filters.appendChild(
643
- this.createPill(section.label, section.count, section.label)
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 style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}>
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 style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}>
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. Both flatten into one list of address chips.
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 === "asyncapi") {
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
- const addressLabel = spec?.kind === "asyncapi" ? "Servers" : "Base URL";
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 Promise.all(
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="font-medium text-[0.625rem] text-orange-600 uppercase tracking-wide dark:text-orange-400">
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
- <div class="xl:sticky xl:top-24 xl:self-start" data-operation-panel>
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
- </div>
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
+ }