blume 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/index.js +1621 -576
  3. package/dist/cli/index.js.map +109 -104
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/config-input.d.ts +79 -27
  6. package/dist/types/core/config.d.ts +2 -1
  7. package/dist/types/core/data.d.ts +16 -1
  8. package/dist/types/core/diagnostics.d.ts +5 -1
  9. package/dist/types/core/i18n-ui.d.ts +12 -0
  10. package/dist/types/core/schema.d.ts +112 -15
  11. package/dist/types/core/sources/types.d.ts +3 -1
  12. package/dist/types/core/standard-schema.d.ts +7 -3
  13. package/dist/types/core/types.d.ts +43 -2
  14. package/dist/types/core/ui-packs/index.d.ts +9 -1
  15. package/dist/types/openapi/references.d.ts +6 -5
  16. package/dist/types/seo/x-handle.d.ts +3 -2
  17. package/docs/advanced/api-reference.mdx +8 -6
  18. package/docs/configuration/search.mdx +2 -0
  19. package/docs/configuration/seo.mdx +1 -1
  20. package/docs/content/i18n.mdx +1 -1
  21. package/docs/content/meta.mdx +2 -1
  22. package/docs/content/meta.ts +1 -0
  23. package/docs/content/navigation.mdx +35 -1
  24. package/docs/content/versioning.mdx +106 -0
  25. package/docs/reference/cli.mdx +1 -0
  26. package/docs/reference/frontmatter.mdx +3 -0
  27. package/package.json +3 -1
  28. package/skills/blume-migrate/SKILL.md +2 -2
  29. package/skills/blume-migrate/references/docusaurus.md +1 -1
  30. package/skills/blume-migrate/references/fumadocs.md +1 -1
  31. package/skills/blume-migrate/references/mintlify.md +1 -1
  32. package/src/ai/agent-readability.ts +37 -10
  33. package/src/ai/ask-context.ts +5 -1
  34. package/src/ai/ask.ts +10 -1
  35. package/src/ai/component-markdown.ts +80 -43
  36. package/src/ai/llms.ts +40 -16
  37. package/src/ai/mcp/data.ts +48 -12
  38. package/src/ai/mcp/discovery.ts +28 -11
  39. package/src/ai/mcp/server.ts +183 -38
  40. package/src/ai/mcp/tools.ts +3 -3
  41. package/src/ai/skills.ts +32 -9
  42. package/src/ai/visibility.ts +2 -2
  43. package/src/astro/component-slots.ts +2 -0
  44. package/src/astro/examples.ts +6 -2
  45. package/src/astro/generate.ts +54 -29
  46. package/src/astro/integration.ts +13 -2
  47. package/src/astro/islands.ts +16 -9
  48. package/src/astro/templates.ts +152 -33
  49. package/src/audit/agent.ts +2 -2
  50. package/src/audit/checks/content.ts +26 -11
  51. package/src/audit/checks/dns-aid.ts +3 -0
  52. package/src/audit/checks/indexability.ts +24 -6
  53. package/src/audit/checks/llms.ts +9 -4
  54. package/src/audit/checks/network.ts +2 -0
  55. package/src/audit/checks/social.ts +18 -10
  56. package/src/audit/crawl.ts +37 -9
  57. package/src/audit/report.ts +20 -19
  58. package/src/audit/run.ts +5 -2
  59. package/src/audit/snapshot.ts +2 -4
  60. package/src/audit/types.ts +25 -3
  61. package/src/blume-modules.d.ts +5 -1
  62. package/src/cli/commands/audit.ts +9 -4
  63. package/src/cli/commands/build.ts +15 -9
  64. package/src/cli/commands/dev.ts +2 -0
  65. package/src/cli/commands/doctor.ts +2 -0
  66. package/src/cli/commands/eval.ts +7 -3
  67. package/src/cli/commands/init.ts +9 -9
  68. package/src/cli/commands/mcp-stdio.ts +3 -0
  69. package/src/cli/commands/translate.ts +14 -3
  70. package/src/cli/commands/version.ts +85 -0
  71. package/src/cli/dev-lock.ts +31 -10
  72. package/src/cli/eject-scripts.ts +17 -2
  73. package/src/cli/index.ts +2 -0
  74. package/src/cli/init/questions.ts +1 -1
  75. package/src/cli/init/scaffold.ts +22 -15
  76. package/src/cli/internal-error.ts +1 -0
  77. package/src/components/content/auto-type-table.ts +3 -0
  78. package/src/components/content/diff.ts +9 -5
  79. package/src/components/content/github-info.ts +2 -0
  80. package/src/components/islands/ask-ai.tsx +33 -25
  81. package/src/components/islands/hooks.ts +5 -1
  82. package/src/components/islands/webmcp.ts +49 -12
  83. package/src/components/layout/Header.astro +25 -1
  84. package/src/components/layout/NavSelector.astro +11 -2
  85. package/src/components/layout/NavTree.astro +4 -2
  86. package/src/components/layout/RootLayout.astro +18 -0
  87. package/src/components/layout/Search.astro +77 -13
  88. package/src/components/layout/VersionBanner.astro +39 -0
  89. package/src/components/layout/analytics-client.ts +8 -5
  90. package/src/components/layout/hydration-hint.ts +1 -1
  91. package/src/components/layout/nav-utils.ts +1 -4
  92. package/src/components/layout/overrides.ts +25 -12
  93. package/src/components/layout/search/algolia.ts +18 -5
  94. package/src/components/layout/search/endpoint.ts +3 -0
  95. package/src/components/layout/search/flexsearch.ts +23 -7
  96. package/src/components/layout/search/orama-cloud.ts +1 -1
  97. package/src/components/layout/search/orama.ts +4 -1
  98. package/src/components/layout/search/pagefind.ts +2 -0
  99. package/src/components/layout/search/types.ts +13 -1
  100. package/src/components/layout/search/typesense.ts +19 -3
  101. package/src/components/openapi/ApiOverview.astro +32 -6
  102. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  103. package/src/components/openapi/Bindings.astro +89 -0
  104. package/src/components/openapi/MethodBadge.astro +3 -0
  105. package/src/components/openapi/Operation.astro +7 -2
  106. package/src/components/openapi/PanelTabs.astro +131 -0
  107. package/src/components/openapi/ParametersTable.astro +2 -0
  108. package/src/components/openapi/RequestPanel.astro +12 -119
  109. package/src/components/openapi/async-snippets.ts +174 -0
  110. package/src/components/openapi/async.ts +348 -0
  111. package/src/components/openapi/helpers.ts +52 -20
  112. package/src/components/openapi/security.ts +102 -29
  113. package/src/components/openapi/snippets.ts +11 -11
  114. package/src/core/component-overrides.ts +28 -23
  115. package/src/core/config-input.ts +88 -27
  116. package/src/core/config.ts +20 -7
  117. package/src/core/content.ts +3 -1
  118. package/src/core/data.ts +16 -1
  119. package/src/core/define-components.ts +5 -0
  120. package/src/core/diagnostics.ts +46 -38
  121. package/src/core/frontmatter.ts +33 -7
  122. package/src/core/graph.ts +137 -53
  123. package/src/core/i18n-ui.ts +15 -0
  124. package/src/core/i18n.ts +16 -8
  125. package/src/core/load-module.ts +1 -0
  126. package/src/core/manifest.ts +92 -3
  127. package/src/core/meta.ts +44 -14
  128. package/src/core/nav-diagnostics.ts +3 -3
  129. package/src/core/navigation.ts +247 -67
  130. package/src/core/project-graph.ts +15 -3
  131. package/src/core/schema.ts +213 -67
  132. package/src/core/sources/assets.ts +2 -0
  133. package/src/core/sources/cache.ts +6 -0
  134. package/src/core/sources/github-releases.ts +39 -31
  135. package/src/core/sources/mdx-remote.ts +4 -0
  136. package/src/core/sources/normalize.ts +67 -20
  137. package/src/core/sources/notion.ts +49 -17
  138. package/src/core/sources/portable-text.ts +32 -11
  139. package/src/core/sources/sanity.ts +68 -14
  140. package/src/core/sources/types.ts +4 -0
  141. package/src/core/sources/watch.ts +1 -1
  142. package/src/core/standard-schema.ts +9 -3
  143. package/src/core/text-width.ts +26 -0
  144. package/src/core/tsconfig-aliases.ts +9 -5
  145. package/src/core/types.ts +45 -2
  146. package/src/core/ui-packs/index.ts +9 -1
  147. package/src/core/version-cut.ts +301 -0
  148. package/src/core/version.ts +2 -0
  149. package/src/core/versions.ts +170 -0
  150. package/src/deploy/adapter-output.ts +5 -2
  151. package/src/deploy/cloudflare-negotiation.ts +25 -10
  152. package/src/deploy/sitemap.ts +33 -1
  153. package/src/deploy/vercel-negotiation.ts +11 -4
  154. package/src/eval/report.ts +4 -4
  155. package/src/eval/run.ts +2 -2
  156. package/src/eval/schema.ts +1 -1
  157. package/src/markdown/base-links.ts +6 -6
  158. package/src/markdown/directives.ts +7 -1
  159. package/src/markdown/heading-anchors.ts +17 -6
  160. package/src/markdown/index.ts +73 -24
  161. package/src/markdown/inline-code.ts +14 -2
  162. package/src/markdown/language-icon.ts +6 -2
  163. package/src/markdown/mdast.ts +18 -4
  164. package/src/markdown/package-commands.ts +6 -8
  165. package/src/markdown/table-wrap.ts +4 -1
  166. package/src/markdown/twoslash.ts +2 -0
  167. package/src/og/card.ts +30 -11
  168. package/src/og/derive.ts +43 -27
  169. package/src/openapi/asyncapi.ts +366 -0
  170. package/src/openapi/model.ts +126 -57
  171. package/src/openapi/parse.ts +97 -5
  172. package/src/openapi/references.ts +12 -10
  173. package/src/openapi/render-mdx.ts +73 -34
  174. package/src/openapi/scalar.ts +6 -8
  175. package/src/openapi/source.ts +98 -28
  176. package/src/registry/eject.ts +7 -2
  177. package/src/search/documents.ts +25 -5
  178. package/src/search/facets.ts +7 -5
  179. package/src/search/orama-index.ts +66 -20
  180. package/src/search/popular.ts +10 -5
  181. package/src/search/providers.ts +2 -2
  182. package/src/search/sync/index.ts +2 -0
  183. package/src/search/sync/typesense.ts +4 -2
  184. package/src/seo/jsonld.ts +24 -6
  185. package/src/seo/x-handle.ts +8 -3
  186. package/src/theme/chrome-icons.ts +7 -2
  187. package/src/theme/fonts.ts +8 -4
  188. package/src/theme/icons.ts +4 -2
  189. package/src/theme/palette.ts +22 -14
  190. package/src/translate/meta.ts +15 -6
  191. package/src/translate/report.ts +9 -5
  192. package/src/translate/run.ts +10 -4
  193. package/src/translate/validate.ts +52 -17
  194. package/src/translate/work-list.ts +0 -0
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  import specs from "blume:openapi";
3
3
 
4
+ import type { AsyncApiDocument } from "../../openapi/asyncapi.ts";
5
+
4
6
  // The spec-level metadata block (version + base URLs) at the top of an API
5
7
  // overview page. The tag sections that follow are emitted by `overviewMdx` as
6
8
  // markdown headings plus `<ApiTagOperations>` lists, so they land in the
@@ -11,8 +13,32 @@ interface Props {
11
13
 
12
14
  const { source } = Astro.props;
13
15
  const spec = specs[source];
14
- const servers =
15
- ((spec?.document ?? {}) as { servers?: { url?: string }[] }).servers ?? [];
16
+
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.
19
+ const addresses: string[] = [];
20
+ if (spec?.kind === "asyncapi") {
21
+ const servers = (spec.document as AsyncApiDocument).servers ?? {};
22
+ for (const server of Object.values(servers)) {
23
+ if (server?.host) {
24
+ addresses.push(
25
+ `${server.protocol ? `${server.protocol}://` : ""}${server.host}${server.pathname ?? ""}`
26
+ );
27
+ }
28
+ }
29
+ } else {
30
+ // Hand-written specs sometimes declare `servers` as a bare object; degrade
31
+ // to no address chips instead of throwing mid-build.
32
+ const declared = ((spec?.document ?? {}) as { servers?: { url?: string }[] })
33
+ .servers;
34
+ const servers = Array.isArray(declared) ? declared : [];
35
+ for (const server of servers) {
36
+ if (server.url) {
37
+ addresses.push(server.url);
38
+ }
39
+ }
40
+ }
41
+ const addressLabel = spec?.kind === "asyncapi" ? "Servers" : "Base URL";
16
42
  ---
17
43
 
18
44
  {
@@ -23,12 +49,12 @@ const servers =
23
49
  Version {spec.version}
24
50
  </div>
25
51
  )}
26
- {servers.length > 0 && (
52
+ {addresses.length > 0 && (
27
53
  <div class="not-prose mb-8 flex flex-wrap items-center gap-2">
28
- <span class="text-muted-foreground text-xs">Base URL</span>
29
- {servers.map((server) => (
54
+ <span class="text-muted-foreground text-xs">{addressLabel}</span>
55
+ {addresses.map((address) => (
30
56
  <code class="rounded bg-muted px-2 py-0.5 text-foreground text-xs">
31
- {server.url}
57
+ {address}
32
58
  </code>
33
59
  ))}
34
60
  </div>
@@ -0,0 +1,237 @@
1
+ ---
2
+ import data from "blume:data";
3
+ import specs from "blume:openapi";
4
+
5
+ import type { AsyncApiDocument } from "../../openapi/asyncapi.ts";
6
+ import { asyncApiOperationObject } from "../../openapi/asyncapi.ts";
7
+ import { highlightCode } from "../../markdown/index.ts";
8
+ import {
9
+ asyncApiSecurityEntries,
10
+ bindingGroups,
11
+ channelParameters,
12
+ channelServers,
13
+ messageLabel,
14
+ operationMessages,
15
+ payloadSchema,
16
+ protocolOf,
17
+ schemaOf,
18
+ } from "./async.ts";
19
+ import type { MessageSample } from "./async-snippets.ts";
20
+ import { asyncSampleLanguages } from "./async-snippets.ts";
21
+ import Authorization from "./Authorization.astro";
22
+ import Bindings from "./Bindings.astro";
23
+ import { exampleValue, type SchemaLike, toJson } from "./helpers.ts";
24
+ import MethodBadge from "./MethodBadge.astro";
25
+ import PanelTabs from "./PanelTabs.astro";
26
+ import ParametersTable from "./ParametersTable.astro";
27
+ import SchemaTable from "./SchemaTable.astro";
28
+ import type { SecuritySchemeLike } from "./security.ts";
29
+ import { resolveAsyncApiSecurity } from "./security.ts";
30
+
31
+ /**
32
+ * The AsyncAPI front-end of the operation page: message payloads instead of
33
+ * request/response, channel parameters instead of path/query, protocol
34
+ * bindings, and binding-aware samples in the right rail. Everything below the
35
+ * dispatch — schema tables, parameter rows, authorization, the tabbed panels —
36
+ * is the same component set the OpenAPI operation renders.
37
+ */
38
+ interface Props {
39
+ source: string;
40
+ id: string;
41
+ }
42
+
43
+ const { source, id } = Astro.props;
44
+ const spec = specs[source];
45
+ const ref = spec?.operations[id];
46
+ const document = (spec?.document ?? {}) as AsyncApiDocument;
47
+ const operation = ref ? asyncApiOperationObject(document, ref) : undefined;
48
+ const channel = ref?.channelId
49
+ ? document.channels?.[ref.channelId]
50
+ : undefined;
51
+
52
+ const schemas = (document.components?.schemas ?? {}) as Record<
53
+ string,
54
+ SchemaLike
55
+ >;
56
+ const servers = channelServers(channel, document);
57
+ const security = resolveAsyncApiSecurity(
58
+ asyncApiSecurityEntries(operation, servers),
59
+ document.components?.securitySchemes as
60
+ | Record<string, SecuritySchemeLike>
61
+ | undefined
62
+ );
63
+ const parameters = channelParameters(channel, document);
64
+ const messages = operation ? operationMessages(operation, channel, document) : [];
65
+ const protocol = protocolOf(operation, channel, servers);
66
+
67
+ /** Declared example first, else a sampled value from the payload schema. */
68
+ const exampleOf = (message: (typeof messages)[number]["message"]): unknown =>
69
+ message.examples?.[0]?.payload ?? exampleValue(payloadSchema(message), schemas);
70
+
71
+ const messagePanels = await Promise.all(
72
+ messages.map(async (named, index) => {
73
+ const example = exampleOf(named.message);
74
+ return {
75
+ html:
76
+ example === undefined || example === null
77
+ ? null
78
+ : await highlightCode(toJson(example), "json", {
79
+ icons: false,
80
+ themes: data.config.codeThemes,
81
+ }),
82
+ key: `message-${index}`,
83
+ label: messageLabel(named),
84
+ text: named.message.summary || "No example payload.",
85
+ };
86
+ })
87
+ );
88
+
89
+ const sample: MessageSample | null = ref
90
+ ? {
91
+ action: ref.method === "send" ? "send" : "receive",
92
+ address: ref.path,
93
+ payload: messages.length > 0 ? exampleOf(messages[0].message) : undefined,
94
+ server: servers[0],
95
+ }
96
+ : null;
97
+ const languages = asyncSampleLanguages(spec?.codeSamples ?? [], protocol);
98
+ const samplePanels = sample
99
+ ? await Promise.all(
100
+ languages.map(async (language) => ({
101
+ html: await highlightCode(language.build(sample), language.lang, {
102
+ icons: false,
103
+ themes: data.config.codeThemes,
104
+ }),
105
+ key: language.id,
106
+ label: language.label,
107
+ }))
108
+ )
109
+ : [];
110
+
111
+ const operationBindings = bindingGroups(operation?.bindings);
112
+ const channelBindings = bindingGroups(channel?.bindings);
113
+ ---
114
+
115
+ {
116
+ !(spec && ref && operation) ? (
117
+ <div class="text-muted-foreground">This API operation could not be found.</div>
118
+ ) : (
119
+ <div class="not-prose">
120
+ <div class="mb-6 flex flex-wrap items-center gap-3">
121
+ <MethodBadge method={ref.method} />
122
+ <code class="break-all font-mono text-foreground text-sm">
123
+ {ref.path}
124
+ </code>
125
+ {ref.deprecated && (
126
+ <span class="font-medium text-[0.625rem] text-orange-600 uppercase tracking-wide dark:text-orange-400">
127
+ deprecated
128
+ </span>
129
+ )}
130
+ </div>
131
+ <div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
132
+ <div>
133
+ <Authorization security={security} />
134
+ <ParametersTable parameters={parameters} schemas={schemas} />
135
+ {messages.map((named) => {
136
+ const payload = payloadSchema(named.message);
137
+ // Headers may be multi-format too; undefined with `headers`
138
+ // present means an inline-unrenderable schema format.
139
+ const headers = schemaOf(named.message.headers);
140
+ const messageBindings = bindingGroups(named.message.bindings);
141
+ return (
142
+ <section class="mt-6">
143
+ <div
144
+ aria-level="2"
145
+ class="mb-2 font-semibold text-foreground text-sm"
146
+ role="heading"
147
+ >
148
+ {messages.length > 1
149
+ ? `Message: ${messageLabel(named)}`
150
+ : "Message"}
151
+ </div>
152
+ {named.message.description && (
153
+ <p class="mb-2 text-muted-foreground text-sm">
154
+ {named.message.description}
155
+ </p>
156
+ )}
157
+ {named.message.contentType && (
158
+ <div class="mb-2 text-muted-foreground text-xs">
159
+ <code class="rounded bg-muted px-1 py-0.5">
160
+ {named.message.contentType}
161
+ </code>
162
+ </div>
163
+ )}
164
+ {payload && (
165
+ <div class="not-prose rounded-blume border border-border px-4 py-3">
166
+ <SchemaTable
167
+ expandAll={spec.expandSchemas}
168
+ schema={payload}
169
+ schemas={schemas}
170
+ />
171
+ </div>
172
+ )}
173
+ {/* Payload is optional on a message — only a payload that
174
+ failed to unwrap earns the can't-render note. */}
175
+ {named.message.payload !== undefined && !payload && (
176
+ <p class="text-muted-foreground text-sm">
177
+ The payload uses a schema format this reference can't
178
+ render inline.
179
+ </p>
180
+ )}
181
+ {named.message.headers && (
182
+ <div class="mt-3">
183
+ <div class="mb-1 font-medium text-muted-foreground text-xs uppercase tracking-wide">
184
+ Headers
185
+ </div>
186
+ {headers ? (
187
+ <div class="not-prose rounded-blume border border-border px-4 py-3">
188
+ <SchemaTable
189
+ expandAll={spec.expandSchemas}
190
+ schema={headers}
191
+ schemas={schemas}
192
+ />
193
+ </div>
194
+ ) : (
195
+ <p class="text-muted-foreground text-sm">
196
+ The headers use a schema format this reference can't
197
+ render inline.
198
+ </p>
199
+ )}
200
+ </div>
201
+ )}
202
+ {messageBindings.length > 0 && (
203
+ <Bindings
204
+ expandAll={spec.expandSchemas}
205
+ groups={messageBindings}
206
+ schemas={schemas}
207
+ title="Message bindings"
208
+ />
209
+ )}
210
+ </section>
211
+ );
212
+ })}
213
+ <Bindings
214
+ expandAll={spec.expandSchemas}
215
+ groups={operationBindings}
216
+ schemas={schemas}
217
+ title="Operation bindings"
218
+ />
219
+ <Bindings
220
+ expandAll={spec.expandSchemas}
221
+ groups={channelBindings}
222
+ schemas={schemas}
223
+ title="Channel bindings"
224
+ />
225
+ </div>
226
+ {(samplePanels.length > 0 || messagePanels.length > 0) && (
227
+ <div class="xl:sticky xl:top-24 xl:self-start">
228
+ <div class="not-prose flex flex-col gap-6">
229
+ <PanelTabs copy heading="Example" panels={samplePanels} />
230
+ <PanelTabs heading="Message" panels={messagePanels} />
231
+ </div>
232
+ </div>
233
+ )}
234
+ </div>
235
+ </div>
236
+ )
237
+ }
@@ -0,0 +1,89 @@
1
+ ---
2
+ import type { BindingGroup } from "./async.ts";
3
+ import type { SchemaLike } from "./helpers.ts";
4
+ import SchemaTable from "./SchemaTable.astro";
5
+
6
+ /**
7
+ * Protocol binding fields as a key/value list, one group per protocol
8
+ * (`kafka`, `ws`, `mqtt`, …). Schema-shaped values — the ws binding's `query`
9
+ * and `headers` — render as nested schema tables; everything else as code.
10
+ */
11
+ interface Props {
12
+ title: string;
13
+ groups: BindingGroup[];
14
+ schemas: Record<string, SchemaLike>;
15
+ expandAll?: boolean;
16
+ }
17
+
18
+ const { title, groups, schemas, expandAll = false } = Astro.props;
19
+
20
+ // A bare `type` key is not enough — binding sub-objects carry protocol enums
21
+ // there (the AMQP exchange's `type: "topic"`), so only a JSON-Schema type
22
+ // value counts.
23
+ const SCHEMA_TYPES = new Set([
24
+ "array",
25
+ "boolean",
26
+ "integer",
27
+ "null",
28
+ "number",
29
+ "object",
30
+ "string",
31
+ ]);
32
+
33
+ const isSchemaish = (value: unknown): value is SchemaLike => {
34
+ if (typeof value !== "object" || value === null) {
35
+ return false;
36
+ }
37
+ if ("properties" in value || "$ref" in value) {
38
+ return true;
39
+ }
40
+ const { type } = value as { type?: unknown };
41
+ return typeof type === "string" && SCHEMA_TYPES.has(type);
42
+ };
43
+ ---
44
+
45
+ {
46
+ groups.length > 0 && (
47
+ <section class="mt-6">
48
+ <div
49
+ aria-level="2"
50
+ class="mb-2 font-semibold text-foreground text-sm"
51
+ role="heading"
52
+ >
53
+ {title}
54
+ </div>
55
+ {groups.map((group) => (
56
+ <div class="not-prose mb-3 rounded-blume border border-border px-4 last:mb-0">
57
+ <div class="flex items-baseline gap-2 border-border py-3">
58
+ <code class="font-mono font-semibold text-foreground text-sm">
59
+ {group.protocol}
60
+ </code>
61
+ </div>
62
+ {group.rows.map((row) => (
63
+ <div class="border-border border-t py-3">
64
+ <div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
65
+ <code class="font-mono text-foreground text-sm">{row.name}</code>
66
+ {!isSchemaish(row.value) && (
67
+ <code class="rounded bg-muted px-1 py-0.5 text-foreground text-xs">
68
+ {typeof row.value === "string"
69
+ ? row.value
70
+ : JSON.stringify(row.value)}
71
+ </code>
72
+ )}
73
+ </div>
74
+ {isSchemaish(row.value) && (
75
+ <div class="mt-2">
76
+ <SchemaTable
77
+ expandAll={expandAll}
78
+ schema={row.value}
79
+ schemas={schemas}
80
+ />
81
+ </div>
82
+ )}
83
+ </div>
84
+ ))}
85
+ </div>
86
+ ))}
87
+ </section>
88
+ )
89
+ }
@@ -15,6 +15,9 @@ const COLORS: Record<string, string> = {
15
15
  PATCH: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
16
16
  POST: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
17
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",
18
21
  };
19
22
  const color = COLORS[upper] ?? "bg-muted text-muted-foreground";
20
23
  ---
@@ -14,6 +14,7 @@ import {
14
14
  type SecuritySchemeLike,
15
15
  } from "./security.ts";
16
16
  import { buildRequestSample, sampleLanguages } from "./snippets.ts";
17
+ import AsyncApiOperation from "./AsyncApiOperation.astro";
17
18
  import Authorization from "./Authorization.astro";
18
19
  import MethodBadge from "./MethodBadge.astro";
19
20
  import ParametersTable from "./ParametersTable.astro";
@@ -57,6 +58,9 @@ interface FullOperation {
57
58
  const { source, id } = Astro.props;
58
59
  const spec = specs[source];
59
60
  const ref = spec?.operations[id];
61
+ // The AsyncAPI front-end renders its own body; the lookups below are
62
+ // OpenAPI-shaped (paths, request/response) and resolve to nothing for it.
63
+ const isAsyncApi = spec?.kind === "asyncapi";
60
64
 
61
65
  const doc = (spec?.document ?? {}) as {
62
66
  paths?: Record<
@@ -113,9 +117,10 @@ const sample =
113
117
  : null;
114
118
  const languages = sampleLanguages(spec?.codeSamples ?? []);
115
119
  ---
116
-
117
120
  {
118
- !(spec && ref && operation) ? (
121
+ isAsyncApi ? (
122
+ <AsyncApiOperation id={id} source={source} />
123
+ ) : !(spec && ref && operation) ? (
119
124
  <div class="text-muted-foreground">This API operation could not be found.</div>
120
125
  ) : (
121
126
  <div class="not-prose">
@@ -0,0 +1,131 @@
1
+ ---
2
+ /**
3
+ * The tabbed code-panel chrome shared by the request/response rail (OpenAPI)
4
+ * and the sample/message rail (AsyncAPI): a heading, a tab per panel, an
5
+ * optional copy button, and one visible panel at a time. Panels carry either
6
+ * highlighted `html` or plain fallback `text`. Behavior comes from the
7
+ * `<blume-panel-tabs>` custom element in `panel.ts`.
8
+ */
9
+ interface Panel {
10
+ key: string;
11
+ label: string;
12
+ /** Highlighted code HTML; null/undefined renders the `text` fallback. */
13
+ html?: string | null;
14
+ /** Plain-text fallback when there is nothing to highlight. */
15
+ text?: string;
16
+ }
17
+
18
+ interface Props {
19
+ heading: string;
20
+ panels: Panel[];
21
+ /** Show a copy button that copies the active panel's text. */
22
+ copy?: boolean;
23
+ /** Render tab labels in a monospace face (status codes). */
24
+ mono?: boolean;
25
+ }
26
+
27
+ const { heading, panels, copy = false, mono = false } = Astro.props;
28
+
29
+ // A stable per-instance id prefix so tab/tabpanel pairs can reference each
30
+ // other without colliding across the multiple PanelTabs on one page (the
31
+ // heading is unique per rail group; panel keys are unique within a group).
32
+ const NON_ID = /[^a-z0-9]+/gu;
33
+ const idFor = (kind: "tab" | "panel", key: string): string =>
34
+ ["panel-tabs", heading, kind, key]
35
+ .map((part) => part.toLowerCase().replace(NON_ID, "-"))
36
+ .join("-");
37
+
38
+ // A `.prose` wrapper gives Shiki its scoped token colors; the global style at
39
+ // the foot of this file strips the standalone code block's own box (border,
40
+ // injected copy button, language label) so the code sits flush inside the one
41
+ // panel border.
42
+ const CODE_WRAP = "prose max-w-none text-xs";
43
+ const TAB_CLASS =
44
+ "-mb-px cursor-pointer border-transparent border-b-2 bg-transparent py-2 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground aria-[selected=true]:border-accent aria-[selected=true]:text-foreground";
45
+ ---
46
+
47
+ {
48
+ panels.length > 0 && (
49
+ <div>
50
+ <div
51
+ aria-level="3"
52
+ class="mb-2 font-semibold text-foreground text-sm"
53
+ role="heading"
54
+ >
55
+ {heading}
56
+ </div>
57
+ <blume-panel-tabs class="block overflow-hidden rounded-blume border border-border bg-background">
58
+ <div class="flex items-center justify-between gap-2 border-border border-b px-3">
59
+ <div class="flex flex-wrap gap-4" role="tablist">
60
+ {panels.map((panel, index) => (
61
+ <button
62
+ aria-controls={idFor("panel", panel.key)}
63
+ aria-selected={index === 0 ? "true" : "false"}
64
+ class:list={[TAB_CLASS, mono && "font-mono"]}
65
+ data-panel-tab={panel.key}
66
+ id={idFor("tab", panel.key)}
67
+ role="tab"
68
+ type="button"
69
+ >
70
+ {panel.label}
71
+ </button>
72
+ ))}
73
+ </div>
74
+ {copy && (
75
+ <button
76
+ aria-label={`Copy ${heading.toLowerCase()}`}
77
+ class="group shrink-0 cursor-pointer rounded px-1.5 py-1 text-muted-foreground text-xs hover:text-foreground"
78
+ data-panel-copy
79
+ type="button"
80
+ >
81
+ <span class="group-data-[copied]:hidden">Copy</span>
82
+ <span class="hidden group-data-[copied]:inline">Copied</span>
83
+ </button>
84
+ )}
85
+ </div>
86
+ {panels.map((panel, index) => (
87
+ <div
88
+ aria-labelledby={idFor("tab", panel.key)}
89
+ class:list={[index === 0 ? "" : "hidden"]}
90
+ data-panel={panel.key}
91
+ id={idFor("panel", panel.key)}
92
+ role="tabpanel"
93
+ >
94
+ {panel.html ? (
95
+ <div class={CODE_WRAP}>
96
+ <Fragment set:html={panel.html} />
97
+ </div>
98
+ ) : (
99
+ <div class="px-3 py-4 text-muted-foreground text-xs">
100
+ {panel.text ?? ""}
101
+ </div>
102
+ )}
103
+ </div>
104
+ ))}
105
+ </blume-panel-tabs>
106
+ </div>
107
+ )
108
+ }
109
+
110
+ <script>
111
+ import "./panel.ts";
112
+ </script>
113
+
114
+ <style is:global>
115
+ /* Strip the standalone code block's own chrome inside a panel: the border,
116
+ margin, radius, and the copy button + language label the prose code theme
117
+ adds — the panel supplies a single border and its own copy button. */
118
+ blume-panel-tabs pre.astro-code {
119
+ margin: 0 !important;
120
+ border: 0 !important;
121
+ border-radius: 0 !important;
122
+ background: transparent !important;
123
+ padding: 0.75rem 1rem !important;
124
+ }
125
+ blume-panel-tabs pre.astro-code::before {
126
+ content: none !important;
127
+ }
128
+ blume-panel-tabs [data-blume-copy] {
129
+ display: none !important;
130
+ }
131
+ </style>
@@ -27,6 +27,8 @@ const SECTIONS: { in: string; title: string }[] = [
27
27
  { in: "query", title: "Query parameters" },
28
28
  { in: "header", title: "Header parameters" },
29
29
  { in: "cookie", title: "Cookie parameters" },
30
+ // AsyncAPI channel address parameters.
31
+ { in: "channel", title: "Channel parameters" },
30
32
  ];
31
33
 
32
34
  const groups = SECTIONS.map((section) => ({