blume 1.5.3 → 1.6.1

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 (209) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/index.js +3949 -1403
  3. package/dist/cli/index.js.map +111 -96
  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 +33 -2
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +10 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +122 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +29 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +26 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/07-faq.mdx +9 -9
  21. package/docs/_snippets/include-demo.mdx +7 -0
  22. package/docs/advanced/api-reference.mdx +13 -4
  23. package/docs/advanced/custom-pages.mdx +4 -2
  24. package/docs/advanced/graphql.mdx +84 -0
  25. package/docs/advanced/meta.ts +8 -1
  26. package/docs/configuration/ai.mdx +25 -3
  27. package/docs/configuration/index.mdx +24 -0
  28. package/docs/configuration/search.mdx +13 -1
  29. package/docs/configuration/seo.mdx +30 -3
  30. package/docs/configuration/theming.mdx +23 -0
  31. package/docs/content/components.mdx +15 -1
  32. package/docs/content/includes.mdx +68 -0
  33. package/docs/content/meta.ts +1 -0
  34. package/docs/content/navigation.mdx +25 -0
  35. package/docs/content/sources.mdx +42 -1
  36. package/docs/content/syntax.mdx +69 -1
  37. package/docs/content/versioning.mdx +15 -9
  38. package/docs/reference/cli.mdx +2 -1
  39. package/package.json +66 -57
  40. package/skills/blume-migrate/SKILL.md +16 -7
  41. package/skills/blume-migrate/references/docusaurus.md +5 -3
  42. package/skills/blume-migrate/references/fumadocs.md +10 -2
  43. package/skills/blume-migrate/references/mintlify.md +3 -2
  44. package/skills/blume-migrate/references/nextra.md +2 -2
  45. package/skills/blume-migrate/references/starlight.md +1 -1
  46. package/src/ai/agent-readability.ts +2 -1
  47. package/src/ai/ask-data.ts +2 -1
  48. package/src/ai/component-markdown.ts +199 -36
  49. package/src/ai/llms.ts +93 -6
  50. package/src/ai/markdown.ts +2 -2
  51. package/src/ai/mcp/discovery.ts +10 -2
  52. package/src/ai/mcp/server.ts +74 -2
  53. package/src/astro/examples.ts +29 -2
  54. package/src/astro/generate.ts +282 -177
  55. package/src/astro/include-hmr.ts +81 -0
  56. package/src/astro/include-refresh.ts +0 -0
  57. package/src/astro/index.ts +10 -5
  58. package/src/astro/markdown-negotiation.ts +1 -1
  59. package/src/astro/runtime-modules.ts +196 -0
  60. package/src/astro/templates.ts +365 -113
  61. package/src/cli/commands/build.ts +91 -16
  62. package/src/cli/commands/dev.ts +6 -3
  63. package/src/cli/host-args.ts +18 -0
  64. package/src/cli/index.ts +2 -1
  65. package/src/cli/init/questions.ts +1 -0
  66. package/src/cli/init/scaffold.ts +27 -4
  67. package/src/components/colors.ts +142 -0
  68. package/src/components/content/Badge.astro +5 -12
  69. package/src/components/content/Callout.astro +19 -36
  70. package/src/components/content/Card.astro +15 -21
  71. package/src/components/content/Component.astro +10 -1
  72. package/src/components/content/GithubInfo.astro +28 -9
  73. package/src/components/content/Tabs.astro +27 -5
  74. package/src/components/content/github-info.ts +20 -5
  75. package/src/components/copy-feedback.ts +93 -9
  76. package/src/components/dropdown-dismiss.ts +122 -0
  77. package/src/components/islands/ask-ai.tsx +4 -1
  78. package/src/components/islands/hooks.ts +3 -1
  79. package/src/components/layout/Fonts.astro +15 -8
  80. package/src/components/layout/Header.astro +44 -0
  81. package/src/components/layout/LanguageSwitcher.astro +9 -1
  82. package/src/components/layout/NavSelector.astro +12 -3
  83. package/src/components/layout/NavTree.astro +6 -18
  84. package/src/components/layout/PageActions.astro +54 -22
  85. package/src/components/layout/PageLayout.astro +2 -0
  86. package/src/components/layout/ReferenceLayout.astro +6 -1
  87. package/src/components/layout/RootLayout.astro +42 -15
  88. package/src/components/layout/Search.astro +36 -4
  89. package/src/components/layout/TableOfContents.astro +8 -2
  90. package/src/components/layout/head-scripts.ts +30 -1
  91. package/src/components/openapi/ApiOverview.astro +13 -3
  92. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  93. package/src/components/openapi/GraphqlChip.astro +33 -0
  94. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  95. package/src/components/openapi/GraphqlOperation.astro +186 -0
  96. package/src/components/openapi/GraphqlType.astro +154 -0
  97. package/src/components/openapi/MethodBadge.astro +3 -14
  98. package/src/components/openapi/Operation.astro +12 -5
  99. package/src/components/openapi/OperationPanel.astro +43 -0
  100. package/src/components/openapi/RequestPanel.astro +5 -10
  101. package/src/components/openapi/Responses.astro +1 -16
  102. package/src/components/openapi/graphql-helpers.ts +466 -0
  103. package/src/components/openapi/playground-client.ts +15 -0
  104. package/src/components/openapi/sample-panels.ts +45 -0
  105. package/src/components/openapi/snippets.ts +13 -35
  106. package/src/core/base-path.ts +11 -0
  107. package/src/core/config-input.ts +209 -2
  108. package/src/core/config.ts +6 -4
  109. package/src/core/content-assets.ts +15 -4
  110. package/src/core/data.ts +28 -3
  111. package/src/core/define-components.ts +2 -0
  112. package/src/core/diagnostics.ts +8 -0
  113. package/src/core/frontmatter.ts +20 -8
  114. package/src/core/github.ts +71 -0
  115. package/src/core/graph.ts +22 -8
  116. package/src/core/heading-markers.ts +96 -0
  117. package/src/core/i18n-ui.ts +12 -0
  118. package/src/core/includes.ts +633 -0
  119. package/src/core/last-modified.ts +36 -11
  120. package/src/core/links.ts +79 -13
  121. package/src/core/manifest.ts +10 -0
  122. package/src/core/meta.ts +2 -1
  123. package/src/core/nav-diagnostics.ts +11 -2
  124. package/src/core/navigation.ts +27 -6
  125. package/src/core/project-graph.ts +61 -9
  126. package/src/core/schema.ts +235 -36
  127. package/src/core/server-features.ts +5 -9
  128. package/src/core/sources/github-releases.ts +2 -2
  129. package/src/core/sources/normalize.ts +502 -115
  130. package/src/core/sources/notion.ts +43 -8
  131. package/src/core/sources/obsidian.ts +1038 -0
  132. package/src/core/sources/read.ts +36 -1
  133. package/src/core/sources/resolve.ts +34 -1
  134. package/src/core/sources/types.ts +28 -6
  135. package/src/core/sources/watch.ts +12 -8
  136. package/src/core/tsconfig-aliases.ts +48 -35
  137. package/src/core/types.ts +31 -2
  138. package/src/core/ui-packs/ar.ts +2 -0
  139. package/src/core/ui-packs/bg.ts +3 -0
  140. package/src/core/ui-packs/bn.ts +2 -0
  141. package/src/core/ui-packs/ca.ts +3 -0
  142. package/src/core/ui-packs/cs.ts +2 -0
  143. package/src/core/ui-packs/da.ts +2 -0
  144. package/src/core/ui-packs/de.ts +3 -0
  145. package/src/core/ui-packs/el.ts +3 -0
  146. package/src/core/ui-packs/es.ts +3 -0
  147. package/src/core/ui-packs/fa.ts +2 -0
  148. package/src/core/ui-packs/fi.ts +2 -0
  149. package/src/core/ui-packs/fr.ts +3 -0
  150. package/src/core/ui-packs/he.ts +2 -0
  151. package/src/core/ui-packs/hi.ts +2 -0
  152. package/src/core/ui-packs/hr.ts +3 -0
  153. package/src/core/ui-packs/hu.ts +3 -0
  154. package/src/core/ui-packs/id.ts +3 -0
  155. package/src/core/ui-packs/it.ts +2 -0
  156. package/src/core/ui-packs/ja.ts +3 -0
  157. package/src/core/ui-packs/ko.ts +3 -0
  158. package/src/core/ui-packs/nl.ts +3 -0
  159. package/src/core/ui-packs/no.ts +3 -0
  160. package/src/core/ui-packs/pl.ts +3 -0
  161. package/src/core/ui-packs/pt-br.ts +3 -0
  162. package/src/core/ui-packs/pt.ts +3 -0
  163. package/src/core/ui-packs/ro.ts +3 -0
  164. package/src/core/ui-packs/ru.ts +3 -0
  165. package/src/core/ui-packs/sk.ts +2 -0
  166. package/src/core/ui-packs/sr.ts +2 -0
  167. package/src/core/ui-packs/sv.ts +3 -0
  168. package/src/core/ui-packs/th.ts +2 -0
  169. package/src/core/ui-packs/tr.ts +3 -0
  170. package/src/core/ui-packs/uk.ts +3 -0
  171. package/src/core/ui-packs/vi.ts +2 -0
  172. package/src/core/ui-packs/zh-tw.ts +2 -0
  173. package/src/core/ui-packs/zh.ts +2 -0
  174. package/src/core/version-cut.ts +26 -6
  175. package/src/core/yaml.ts +26 -0
  176. package/src/deploy/function-bundle.ts +251 -0
  177. package/src/deploy/vercel-negotiation.ts +49 -6
  178. package/src/eval/schema.ts +3 -1
  179. package/src/markdown/code-title.ts +22 -16
  180. package/src/markdown/features.ts +21 -0
  181. package/src/markdown/fence-meta.ts +50 -0
  182. package/src/markdown/heading-anchors.ts +198 -37
  183. package/src/markdown/include.ts +247 -0
  184. package/src/markdown/index.ts +43 -34
  185. package/src/markdown/language-icon.ts +2 -2
  186. package/src/markdown/mdast.ts +7 -3
  187. package/src/markdown/ts2js.ts +264 -0
  188. package/src/og/card.ts +1 -1
  189. package/src/openapi/asyncapi.ts +4 -1
  190. package/src/openapi/graphql-build.ts +293 -0
  191. package/src/openapi/graphql.ts +212 -0
  192. package/src/openapi/model.ts +38 -5
  193. package/src/openapi/parse.ts +34 -0
  194. package/src/openapi/proxy.ts +30 -5
  195. package/src/openapi/references.ts +97 -13
  196. package/src/openapi/render-mdx.ts +66 -12
  197. package/src/openapi/scalar.ts +5 -16
  198. package/src/openapi/source.ts +91 -23
  199. package/src/registry/eject.ts +47 -17
  200. package/src/search/documents.ts +229 -37
  201. package/src/search/orama-index.ts +9 -5
  202. package/src/seo/jsonld.ts +293 -51
  203. package/src/theme/code-block-padding.ts +16 -0
  204. package/src/theme/entry.ts +67 -13
  205. package/src/theme/fonts.ts +189 -16
  206. package/src/theme/sources.ts +49 -0
  207. package/src/translate/prompts.ts +2 -0
  208. package/src/translate/run.ts +7 -0
  209. package/src/translate/work-list.ts +0 -0
@@ -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
+ }
@@ -0,0 +1,154 @@
1
+ ---
2
+ import type { GraphqlDocument } from "../../openapi/graphql.ts";
3
+ import { graphqlTypeDef } from "../../openapi/graphql.ts";
4
+ import type { ApiOperationRef, ApiSpecData } from "../../openapi/model.ts";
5
+ import {
6
+ graphqlOperationRoutes,
7
+ graphqlRoutes,
8
+ graphqlUsage,
9
+ } from "./graphql-helpers.ts";
10
+ import GraphqlChip from "./GraphqlChip.astro";
11
+ import GraphqlFieldsTable from "./GraphqlFieldsTable.astro";
12
+ import MethodBadge from "./MethodBadge.astro";
13
+
14
+ /**
15
+ * A named-type reference page: the type's fields (or input fields, enum
16
+ * values, union members), what it implements or is implemented by, and usage
17
+ * backlinks — which operations return or accept it and which types reference
18
+ * it. The description prose is already in the MDX body above this component.
19
+ */
20
+ interface Props {
21
+ spec: ApiSpecData;
22
+ refOp: ApiOperationRef;
23
+ }
24
+
25
+ const { spec, refOp } = Astro.props;
26
+ const document = spec.document as GraphqlDocument;
27
+ const type = graphqlTypeDef(document, refOp);
28
+ const routes = graphqlRoutes(spec);
29
+ const operationRoutes = graphqlOperationRoutes(spec);
30
+
31
+ const usage = type ? graphqlUsage(document, type.name) : { operations: [], types: [] };
32
+
33
+ // The chip sections a kind can render, empty ones dropped: `possibleTypes`
34
+ // carries a union's members or an interface's implementers, so its heading
35
+ // follows the kind.
36
+ const sections = type
37
+ ? [
38
+ {
39
+ names: type.kind === "union" ? (type.possibleTypes ?? []) : [],
40
+ title: "Member types",
41
+ },
42
+ {
43
+ names: type.kind === "interface" ? (type.possibleTypes ?? []) : [],
44
+ title: "Implemented by",
45
+ },
46
+ { names: type.interfaces ?? [], title: "Implements" },
47
+ ].filter((section) => section.names.length > 0)
48
+ : [];
49
+
50
+ const SECTION_HEADING = "mb-2 font-semibold text-foreground text-sm";
51
+ ---
52
+
53
+ {
54
+ !(type) ? (
55
+ <div class="text-muted-foreground">This GraphQL type could not be found.</div>
56
+ ) : (
57
+ <div class="not-prose">
58
+ <div class="mb-6 flex flex-wrap items-center gap-3">
59
+ <MethodBadge method={refOp.method} />
60
+ <code class="break-all font-mono text-foreground text-sm">
61
+ {type.name}
62
+ </code>
63
+ </div>
64
+ {type.specifiedByUrl && (
65
+ <div class="mb-4 text-muted-foreground text-sm">
66
+ Specified by{" "}
67
+ <a class="text-accent hover:underline" href={type.specifiedByUrl}>
68
+ {type.specifiedByUrl}
69
+ </a>
70
+ </div>
71
+ )}
72
+ <GraphqlFieldsTable
73
+ routes={routes}
74
+ rows={type.fields ?? []}
75
+ title="Fields"
76
+ />
77
+ <GraphqlFieldsTable
78
+ routes={routes}
79
+ rows={type.inputFields ?? []}
80
+ title="Input fields"
81
+ />
82
+ {(type.enumValues ?? []).length > 0 && (
83
+ <section class="mt-6">
84
+ <div aria-level="2" class={SECTION_HEADING} role="heading">
85
+ Values
86
+ </div>
87
+ <div class="rounded-blume border border-border px-4">
88
+ {(type.enumValues ?? []).map((value) => (
89
+ <div class="border-border border-t py-3 first:border-t-0">
90
+ <div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
91
+ <code class="font-mono text-foreground text-sm">
92
+ {value.name}
93
+ </code>
94
+ {value.deprecationReason !== undefined && (
95
+ <span class="font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide line-through">
96
+ deprecated
97
+ </span>
98
+ )}
99
+ </div>
100
+ {value.description && (
101
+ <div
102
+ class="mt-1 text-muted-foreground text-sm"
103
+ set:text={value.description}
104
+ />
105
+ )}
106
+ {value.deprecationReason && (
107
+ <div class="mt-1 text-muted-foreground text-xs">
108
+ Deprecated: <span set:text={value.deprecationReason} />
109
+ </div>
110
+ )}
111
+ </div>
112
+ ))}
113
+ </div>
114
+ </section>
115
+ )}
116
+ {sections.map((section) => (
117
+ <section class="mt-6">
118
+ <div aria-level="2" class={SECTION_HEADING} role="heading">
119
+ {section.title}
120
+ </div>
121
+ <div class="flex flex-wrap gap-2">
122
+ {section.names.map((name) => (
123
+ <GraphqlChip class="text-xs" name={name} route={routes.get(name)} />
124
+ ))}
125
+ </div>
126
+ </section>
127
+ ))}
128
+ {(usage.operations.length > 0 || usage.types.length > 0) && (
129
+ <section class="mt-6">
130
+ <div aria-level="2" class={SECTION_HEADING} role="heading">
131
+ Used by
132
+ </div>
133
+ <div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
134
+ {usage.operations.map((operation) => (
135
+ <span class="inline-flex items-baseline gap-1 text-xs">
136
+ <GraphqlChip
137
+ class="text-xs"
138
+ name={operation.name}
139
+ route={operationRoutes.get(
140
+ `${operation.kind}:${operation.name}`
141
+ )}
142
+ />
143
+ <span class="text-muted-foreground">{operation.kind}</span>
144
+ </span>
145
+ ))}
146
+ {usage.types.map((name) => (
147
+ <GraphqlChip class="text-xs" name={name} route={routes.get(name)} />
148
+ ))}
149
+ </div>
150
+ </section>
151
+ )}
152
+ </div>
153
+ )
154
+ }
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { methodColor } from "../colors.ts";
3
+
2
4
  interface Props {
3
5
  method: string;
4
6
  class?: string;
@@ -6,20 +8,7 @@ interface Props {
6
8
 
7
9
  const { method, class: className } = Astro.props;
8
10
  const upper = method.toUpperCase();
9
-
10
- const COLORS: Record<string, string> = {
11
- DELETE: "bg-red-500/15 text-red-700 dark:text-red-300",
12
- GET: "bg-green-500/15 text-green-700 dark:text-green-300",
13
- HEAD: "bg-muted text-muted-foreground",
14
- OPTIONS: "bg-muted text-muted-foreground",
15
- PATCH: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
16
- POST: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
17
- PUT: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
18
- // AsyncAPI actions.
19
- RECEIVE: "bg-teal-500/15 text-teal-700 dark:text-teal-300",
20
- SEND: "bg-violet-500/15 text-violet-700 dark:text-violet-300",
21
- };
22
- const color = COLORS[upper] ?? "bg-muted text-muted-foreground";
11
+ const color = methodColor(upper);
23
12
  ---
24
13
 
25
14
  <span
@@ -14,8 +14,10 @@ import {
14
14
  type SecurityRequirementLike,
15
15
  type SecuritySchemeLike,
16
16
  } from "./security.ts";
17
+ import { DEPRECATED_LABEL_CLASS } from "../colors.ts";
17
18
  import { sampleLanguages } from "./snippets.ts";
18
19
  import AsyncApiOperation from "./AsyncApiOperation.astro";
20
+ import GraphqlOperation from "./GraphqlOperation.astro";
19
21
  import Authorization from "./Authorization.astro";
20
22
  import MethodBadge from "./MethodBadge.astro";
21
23
  import ParametersTable from "./ParametersTable.astro";
@@ -23,6 +25,7 @@ import Playground from "./Playground.astro";
23
25
  import RequestBody from "./RequestBody.astro";
24
26
  import RequestPanel from "./RequestPanel.astro";
25
27
  import Responses from "./Responses.astro";
28
+ import OperationPanel from "./OperationPanel.astro";
26
29
 
27
30
  interface Props {
28
31
  source: string;
@@ -60,9 +63,11 @@ interface FullOperation {
60
63
  const { source, id } = Astro.props;
61
64
  const spec = specs[source];
62
65
  const ref = spec?.operations[id];
63
- // The AsyncAPI front-end renders its own body; the lookups below are
64
- // OpenAPI-shaped (paths, request/response) and resolve to nothing for it.
66
+ // The AsyncAPI and GraphQL front-ends render their own bodies; the lookups
67
+ // below are OpenAPI-shaped (paths, request/response) and resolve to nothing
68
+ // for them.
65
69
  const isAsyncApi = spec?.kind === "asyncapi";
70
+ const isGraphql = spec?.kind === "graphql";
66
71
 
67
72
  const doc = (spec?.document ?? {}) as {
68
73
  paths?: Record<
@@ -126,6 +131,8 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
126
131
  {
127
132
  isAsyncApi ? (
128
133
  <AsyncApiOperation id={id} source={source} />
134
+ ) : isGraphql ? (
135
+ <GraphqlOperation id={id} source={source} />
129
136
  ) : !(spec && ref && operation) ? (
130
137
  <div class="text-muted-foreground">This API operation could not be found.</div>
131
138
  ) : (
@@ -136,7 +143,7 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
136
143
  {ref.path}
137
144
  </code>
138
145
  {operation.deprecated && (
139
- <span class="font-medium text-[0.625rem] text-orange-600 uppercase tracking-wide dark:text-orange-400">
146
+ <span class={DEPRECATED_LABEL_CLASS}>
140
147
  deprecated
141
148
  </span>
142
149
  )}
@@ -161,7 +168,7 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
161
168
  )}
162
169
  </div>
163
170
  {sample && model && (
164
- <div class="xl:sticky xl:top-24 xl:self-start" data-operation-panel>
171
+ <OperationPanel>
165
172
  {spec.playground.enabled && (
166
173
  <Playground
167
174
  model={model}
@@ -176,7 +183,7 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
176
183
  sample={sample}
177
184
  schemas={schemas}
178
185
  />
179
- </div>
186
+ </OperationPanel>
180
187
  )}
181
188
  </div>
182
189
  </div>
@@ -0,0 +1,43 @@
1
+ ---
2
+ /**
3
+ * The API reference's sample panel: the right-hand column of an operation
4
+ * page holding the playground and the request/response examples. OpenAPI,
5
+ * AsyncAPI and GraphQL operation pages share it, so the layout lives here
6
+ * once. It stays utility classes rather than a theme rule on purpose: an
7
+ * ejected site freezes its generated app.css but keeps pulling components from
8
+ * node_modules through its `@source` glob, so utilities survive a blume
9
+ * upgrade there and a theme rule would not.
10
+ *
11
+ * At xl the panel sits beside the docs column and sticks below the 4rem header
12
+ * with the same 2rem gap the content keeps. It is bounded to the viewport with
13
+ * its own scroll region: a grid item's sticky containing block is its row, and
14
+ * the row is as tall as its tallest item, so a long request schema on the left
15
+ * would otherwise pin the panel with everything below the fold unreachable
16
+ * until the left column ended. The tradeoff runs the other way when the left
17
+ * column is the short one — the row is then only as tall as the capped panel
18
+ * and the overhang is reached by scrolling the panel, not the page. No
19
+ * overscroll containment, like the sidebar and table of contents: once the
20
+ * panel reaches its end, wheel input chains to the page. The scrollbar matches
21
+ * theirs, the end padding keeps it off the Request card's edge, and the stable
22
+ * gutter stops a bar appearing at the cap ("Try it" opening, a tab switch)
23
+ * from narrowing the content on classic-scrollbar platforms.
24
+ *
25
+ * The 0.25rem inset (`pt-1 ps-1 pb-1`) keeps the global focus ring — 2px
26
+ * outline, 2px offset — on the first child, the playground's `<summary>` at
27
+ * the scroller's origin, inside the padding box where it can paint; the
28
+ * negative margins pull the box back so the content still aligns with the left
29
+ * column, and the sticky offset (`top-24` less the inset) and the viewport cap
30
+ * (`7.5rem` less the inset) are adjusted by the same amount. The end side
31
+ * uses the same trick at 1rem (`-me-4 pe-4`): the box spills into the page
32
+ * gutter (`<main>` keeps `xl:px-10`), so the scrollbar and the reserved gutter
33
+ * live there, off the Request card's edge, and with overlay scrollbars the
34
+ * cards keep the column's full 28rem; a classic bar costs its own width.
35
+ */
36
+ ---
37
+
38
+ <div
39
+ class="xl:-ms-1 xl:-mt-1 xl:-me-4 xl:sticky xl:top-[5.75rem] xl:max-h-[calc(100dvh-7.25rem)] xl:overflow-y-auto xl:scrollbar-thin xl:scrollbar-thumb-border xl:scrollbar-track-transparent xl:[scrollbar-gutter:stable] xl:pt-1 xl:ps-1 xl:pb-1 xl:pe-4"
40
+ data-operation-panel
41
+ >
42
+ <slot />
43
+ </div>