blume 0.3.0 → 0.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 (128) hide show
  1. package/dist/cli/index.js +1631 -940
  2. package/dist/cli/index.js.map +62 -50
  3. package/dist/types/core/data.d.ts +2 -0
  4. package/dist/types/core/project.d.ts +12 -2
  5. package/dist/types/core/schema.d.ts +442 -292
  6. package/dist/types/core/types.d.ts +7 -0
  7. package/dist/types/migrate/mintlify/assets.d.ts +8 -0
  8. package/docs/01-quickstart.mdx +5 -16
  9. package/docs/02-deployment.mdx +21 -54
  10. package/docs/advanced/api-reference.mdx +34 -51
  11. package/docs/advanced/blog.mdx +9 -25
  12. package/docs/advanced/bridge.mdx +74 -0
  13. package/docs/advanced/changelog.mdx +10 -33
  14. package/docs/advanced/custom-pages.mdx +21 -78
  15. package/docs/advanced/meta.ts +8 -1
  16. package/docs/advanced/migrate.mdx +119 -0
  17. package/docs/configuration/ai.mdx +42 -103
  18. package/docs/configuration/analytics.mdx +20 -38
  19. package/docs/configuration/customization.mdx +40 -73
  20. package/docs/configuration/export.mdx +9 -34
  21. package/docs/configuration/index.mdx +67 -87
  22. package/docs/configuration/search.mdx +17 -54
  23. package/docs/configuration/seo.mdx +17 -48
  24. package/docs/configuration/theming.mdx +20 -42
  25. package/docs/content/components.mdx +95 -101
  26. package/docs/content/i18n.mdx +21 -72
  27. package/docs/content/index.mdx +18 -48
  28. package/docs/content/islands.mdx +25 -52
  29. package/docs/content/meta.mdx +23 -50
  30. package/docs/content/navigation.mdx +23 -62
  31. package/docs/content/sources.mdx +20 -83
  32. package/docs/content/syntax.mdx +37 -105
  33. package/docs/index.mdx +12 -41
  34. package/docs/reference/cli.mdx +47 -30
  35. package/docs/reference/frontmatter.mdx +7 -5
  36. package/package.json +11 -1
  37. package/src/astro/generate.ts +18 -8
  38. package/src/astro/integration.ts +26 -3
  39. package/src/astro/islands.ts +6 -2
  40. package/src/astro/markdown-negotiation.ts +17 -3
  41. package/src/astro/pages.ts +6 -1
  42. package/src/astro/static-assets.ts +117 -0
  43. package/src/astro/templates.ts +76 -30
  44. package/src/cli/args.ts +23 -0
  45. package/src/cli/commands/build.ts +129 -62
  46. package/src/cli/commands/check.ts +20 -0
  47. package/src/cli/commands/dev.ts +11 -2
  48. package/src/cli/commands/doctor.ts +10 -1
  49. package/src/cli/commands/eject.ts +3 -1
  50. package/src/cli/commands/init.ts +21 -1
  51. package/src/cli/commands/preview.ts +2 -1
  52. package/src/cli/commands/validate.ts +12 -1
  53. package/src/cli/dev-lock.ts +92 -0
  54. package/src/cli/log.ts +11 -0
  55. package/src/cli/prepare.ts +3 -0
  56. package/src/components/BlumePage.astro +8 -0
  57. package/src/components/Icon.astro +13 -10
  58. package/src/components/content/ApiField.astro +75 -0
  59. package/src/components/content/ParamField.astro +39 -0
  60. package/src/components/content/RequestField.astro +23 -0
  61. package/src/components/content/ResponseField.astro +23 -0
  62. package/src/components/content/Step.astro +1 -1
  63. package/src/components/content/YouTube.astro +35 -0
  64. package/src/components/content/youtube.ts +46 -0
  65. package/src/components/islands/ask-ai.tsx +14 -14
  66. package/src/components/layout/Breadcrumbs.astro +7 -2
  67. package/src/components/layout/NavTree.astro +24 -8
  68. package/src/components/layout/RootLayout.astro +56 -34
  69. package/src/components/layout/Search.astro +1 -1
  70. package/src/components/openapi/ApiOverview.astro +84 -0
  71. package/src/components/openapi/MethodBadge.astro +28 -0
  72. package/src/components/openapi/Operation.astro +140 -0
  73. package/src/components/openapi/ParametersTable.astro +97 -0
  74. package/src/components/openapi/RequestBody.astro +58 -0
  75. package/src/components/openapi/RequestPanel.astro +169 -0
  76. package/src/components/openapi/Responses.astro +91 -0
  77. package/src/components/openapi/SchemaProperty.astro +118 -0
  78. package/src/components/openapi/SchemaTable.astro +86 -0
  79. package/src/components/openapi/helpers.ts +238 -0
  80. package/src/components/openapi/panel.ts +59 -0
  81. package/src/components/openapi/snippets.ts +201 -0
  82. package/src/components/props.ts +3 -0
  83. package/src/core/assets.ts +31 -0
  84. package/src/core/bridge.ts +10 -0
  85. package/src/core/builtin-tags.ts +6 -0
  86. package/src/core/data.ts +2 -0
  87. package/src/core/diagnostics.ts +6 -1
  88. package/src/core/gitignore.ts +30 -0
  89. package/src/core/links.ts +60 -19
  90. package/src/core/project-graph.ts +5 -1
  91. package/src/core/project.ts +25 -3
  92. package/src/core/schema.ts +54 -6
  93. package/src/core/sources/mdx-remote.ts +54 -8
  94. package/src/core/sources/mintlify.ts +1 -1
  95. package/src/core/sources/normalize.ts +6 -1
  96. package/src/core/sources/notion.ts +49 -5
  97. package/src/core/sources/resolve.ts +28 -6
  98. package/src/core/sources/sanity.ts +5 -1
  99. package/src/core/types.ts +7 -0
  100. package/src/deploy/rss.ts +1 -8
  101. package/src/deploy/sitemap.ts +20 -1
  102. package/src/deploy/xml.ts +8 -0
  103. package/src/markdown/directives.ts +15 -7
  104. package/src/markdown/package-commands.ts +26 -4
  105. package/src/migrate/fumadocs/content.ts +14 -1
  106. package/src/migrate/fumadocs/groups.ts +7 -0
  107. package/src/migrate/fumadocs/index.ts +5 -2
  108. package/src/migrate/mintlify/assets.ts +46 -0
  109. package/src/migrate/mintlify/config.ts +153 -1
  110. package/src/migrate/mintlify/content.ts +8 -2
  111. package/src/migrate/mintlify/index.ts +111 -46
  112. package/src/migrate/shared.ts +12 -27
  113. package/src/og/card.ts +14 -2
  114. package/src/openapi/model.ts +174 -0
  115. package/src/openapi/parse.ts +48 -0
  116. package/src/openapi/references.ts +164 -0
  117. package/src/openapi/render-mdx.ts +76 -0
  118. package/src/openapi/scalar.ts +15 -103
  119. package/src/openapi/source.ts +140 -0
  120. package/src/registry/eject.ts +28 -5
  121. package/src/registry/registry.ts +6 -0
  122. package/src/registry/rewrite-imports.ts +31 -19
  123. package/src/search/documents.ts +23 -5
  124. package/src/search/sync/algolia.ts +5 -1
  125. package/src/search/sync/typesense.ts +24 -16
  126. package/src/theme/chrome-icons.ts +22 -0
  127. package/src/theme/icons.ts +151 -161
  128. package/src/theme/palette.ts +26 -7
@@ -31,8 +31,24 @@ const {
31
31
  root = depth === 0,
32
32
  } = Astro.props;
33
33
 
34
- const badgeClass =
35
- "shrink-0 rounded-full bg-muted px-1.5 py-0.5 font-medium text-[0.65rem] text-muted-foreground leading-none";
34
+ const badgeBase =
35
+ "shrink-0 rounded-full px-1.5 py-0.5 font-medium text-[0.65rem] leading-none";
36
+ const badgeClass = `${badgeBase} bg-muted text-muted-foreground`;
37
+ // HTTP-method badges (from an OpenAPI reference's sidebar) are colour-coded;
38
+ // every other badge keeps the neutral style.
39
+ const METHOD_BADGE: Record<string, string> = {
40
+ DELETE: "bg-red-500/15 text-red-700 dark:text-red-300",
41
+ GET: "bg-green-500/15 text-green-700 dark:text-green-300",
42
+ HEAD: "bg-muted text-muted-foreground",
43
+ OPTIONS: "bg-muted text-muted-foreground",
44
+ PATCH: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
45
+ POST: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
46
+ PUT: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
47
+ };
48
+ const badgeClassFor = (badge: string): string => {
49
+ const method = METHOD_BADGE[badge.toUpperCase()];
50
+ return method ? `${badgeBase} ${method}` : badgeClass;
51
+ };
36
52
  const deprecatedClass =
37
53
  "shrink-0 rounded-full bg-amber-500/10 px-1.5 py-0.5 font-medium text-[0.65rem] text-amber-700 leading-none dark:text-amber-300";
38
54
 
@@ -154,7 +170,7 @@ const initialId =
154
170
  />
155
171
  )}
156
172
  <span class="min-w-0 flex-1 truncate">{item.label}</span>
157
- {item.badge && <span class={badgeClass}>{item.badge}</span>}
173
+ {item.badge && <span class={badgeClassFor(item.badge)}>{item.badge}</span>}
158
174
  {item.deprecated && (
159
175
  <span class={deprecatedClass}>deprecated</span>
160
176
  )}
@@ -188,7 +204,7 @@ const initialId =
188
204
  />
189
205
  )}
190
206
  <span class="min-w-0 flex-1 truncate">{item.label}</span>
191
- {item.badge && <span class={badgeClass}>{item.badge}</span>}
207
+ {item.badge && <span class={badgeClassFor(item.badge)}>{item.badge}</span>}
192
208
  <Icon
193
209
  class="shrink-0 text-muted-foreground"
194
210
  name="chevron-right"
@@ -224,7 +240,7 @@ const initialId =
224
240
  )}
225
241
  <span class="min-w-0 flex-1 truncate">{item.label}</span>
226
242
  {item.badge && (
227
- <span class={badgeClass}>{item.badge}</span>
243
+ <span class={badgeClassFor(item.badge)}>{item.badge}</span>
228
244
  )}
229
245
  </a>
230
246
  ) : (
@@ -238,7 +254,7 @@ const initialId =
238
254
  )}
239
255
  <span class="min-w-0 flex-1 truncate">{item.label}</span>
240
256
  {item.badge && (
241
- <span class={badgeClass}>{item.badge}</span>
257
+ <span class={badgeClassFor(item.badge)}>{item.badge}</span>
242
258
  )}
243
259
  </>
244
260
  )}
@@ -279,7 +295,7 @@ const initialId =
279
295
  )}
280
296
  <span class="min-w-0 truncate">{item.label}</span>
281
297
  {item.badge && (
282
- <span class={`ml-auto ${badgeClass}`}>{item.badge}</span>
298
+ <span class={`ml-auto ${badgeClassFor(item.badge)}`}>{item.badge}</span>
283
299
  )}
284
300
  </a>
285
301
  ) : (
@@ -293,7 +309,7 @@ const initialId =
293
309
  )}
294
310
  <span class="min-w-0 truncate">{item.label}</span>
295
311
  {item.badge && (
296
- <span class={`ml-auto ${badgeClass}`}>{item.badge}</span>
312
+ <span class={`ml-auto ${badgeClassFor(item.badge)}`}>{item.badge}</span>
297
313
  )}
298
314
  </>
299
315
  )}
@@ -200,9 +200,19 @@ const searchLocale =
200
200
  localeSwitch && localeSwitch.length > 1 ? locale : undefined;
201
201
  // TOC entries: the configured heading range, or none when the TOC is disabled
202
202
  // (an empty list makes TableOfContents render nothing).
203
- const tocHeadings = toc.enabled
204
- ? headings.filter((h) => h.depth >= toc.minLevel && h.depth <= toc.maxLevel)
205
- : [];
203
+ // API operation pages own a two-column body (docs + request panel) and their own
204
+ // right rail, so they drop the table of contents and widen the content column.
205
+ const isApiOperation = pageType === "openapi-operation";
206
+ const tocHeadings =
207
+ toc.enabled && !isApiOperation
208
+ ? headings.filter((h) => h.depth >= toc.minLevel && h.depth <= toc.maxLevel)
209
+ : [];
210
+ const gridClass = isApiOperation
211
+ ? "lg:grid-cols-[17.5rem_minmax(0,1fr)]"
212
+ : "lg:grid-cols-[17.5rem_minmax(0,1fr)] xl:grid-cols-[17.5rem_minmax(0,1fr)_17.5rem]";
213
+ const articleClass = isApiOperation
214
+ ? "prose max-w-none"
215
+ : "prose mx-auto max-w-[42rem]";
206
216
  const pageTitle = page.title ? `${page.title} - ${site.title}` : site.title;
207
217
  const description = page.description ?? site.description;
208
218
 
@@ -331,7 +341,7 @@ const bannerScript = banner?.dismissible
331
341
  >
332
342
  <slot name="ask" slot="ask" />
333
343
  </HeaderSlot>
334
- <div class="mx-auto grid grid-cols-1 items-start lg:grid-cols-[17.5rem_minmax(0,1fr)] xl:grid-cols-[17.5rem_minmax(0,1fr)_17.5rem]">
344
+ <div class:list={["mx-auto grid grid-cols-1 items-start", gridClass]}>
335
345
  <aside
336
346
  aria-label="Primary"
337
347
  class="fixed top-16 start-0 z-[35] h-[calc(100dvh-4rem)] w-64 max-w-[80vw] -translate-x-[105%] overflow-y-auto border-border border-e bg-background px-5 pt-4 pb-6 transition-transform rtl:translate-x-[105%] [:where([data-blume-nav-open])_&]:translate-x-0! lg:sticky lg:z-auto lg:w-auto lg:max-w-none lg:translate-x-0! lg:border-e-0 lg:bg-transparent lg:px-4"
@@ -354,47 +364,59 @@ const bannerScript = banner?.dismissible
354
364
  </nav>
355
365
  </aside>
356
366
  <main class="min-w-0 px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
357
- <BreadcrumbsSlot crumbs={crumbs} />
367
+ <BreadcrumbsSlot crumbs={crumbs} wide={isApiOperation} />
358
368
  <TableOfContentsSlot
359
369
  headings={tocHeadings}
360
370
  title={strings.toc.title}
361
371
  variant="mobile"
362
372
  />
363
373
  <PageHeaderSlot headings={tocHeadings} page={page} route={page.route} />
364
- <article class="prose mx-auto max-w-[42rem]" dir={contentDir}>
374
+ <article class:list={[articleClass]} dir={contentDir}>
365
375
  <slot />
366
376
  </article>
367
- <PageFooterSlot headings={tocHeadings} page={page} route={page.route} />
368
377
  {
369
- formattedLastModified && (
370
- <p class="mx-auto mt-10 max-w-[42rem] text-muted-foreground text-sm">
371
- {strings.page.lastUpdated} {formattedLastModified}
372
- </p>
378
+ !isApiOperation && (
379
+ <>
380
+ <PageFooterSlot
381
+ headings={tocHeadings}
382
+ page={page}
383
+ route={page.route}
384
+ />
385
+ {formattedLastModified && (
386
+ <p class="mx-auto mt-10 max-w-[42rem] text-muted-foreground text-sm">
387
+ {strings.page.lastUpdated} {formattedLastModified}
388
+ </p>
389
+ )}
390
+ {feedback && <FeedbackSlot strings={strings.feedback} />}
391
+ <PaginationSlot next={next} prev={prev} strings={strings.page} />
392
+ </>
373
393
  )
374
394
  }
375
- {feedback && <FeedbackSlot strings={strings.feedback} />}
376
- <PaginationSlot next={next} prev={prev} strings={strings.page} />
377
395
  </main>
378
- <aside
379
- aria-label={strings.toc.title}
380
- class="sticky top-16 hidden h-[calc(100dvh-4rem)] overflow-y-auto px-4 pt-6 pb-10 text-sm xl:block"
381
- >
382
- <TableOfContentsSlot
383
- headings={tocHeadings}
384
- title={strings.toc.title}
385
- variant="desktop"
386
- />
387
- <PageActions
388
- askEnabled={askEnabled}
389
- editUrl={editUrl}
390
- exportEpub={exportEpub}
391
- exportPdf={exportPdf}
392
- mcpName={mcp?.name}
393
- mcpUrl={mcpUrl}
394
- route={page.route}
395
- strings={strings.actions}
396
- />
397
- </aside>
396
+ {
397
+ !isApiOperation && (
398
+ <aside
399
+ aria-label={strings.toc.title}
400
+ class="sticky top-16 hidden h-[calc(100dvh-4rem)] overflow-y-auto px-4 pt-6 pb-10 text-sm xl:block"
401
+ >
402
+ <TableOfContentsSlot
403
+ headings={tocHeadings}
404
+ title={strings.toc.title}
405
+ variant="desktop"
406
+ />
407
+ <PageActions
408
+ askEnabled={askEnabled}
409
+ editUrl={editUrl}
410
+ exportEpub={exportEpub}
411
+ exportPdf={exportPdf}
412
+ mcpName={mcp?.name}
413
+ mcpUrl={mcpUrl}
414
+ route={page.route}
415
+ strings={strings.actions}
416
+ />
417
+ </aside>
418
+ )
419
+ }
398
420
  </div>
399
421
  <FooterSlot navigation={navigation} site={site} ui={strings} />
400
422
  {
@@ -414,7 +436,7 @@ const bannerScript = banner?.dismissible
414
436
  type="button"
415
437
  ></button>
416
438
  <script>
417
- import { icons } from "../../theme/icons.ts";
439
+ import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
418
440
  // Registers the <blume-mermaid> custom element (emitted by ```mermaid
419
441
  // fences). Mermaid itself is lazy-loaded only on pages that use a diagram.
420
442
  import "../content/mermaid-element.ts";
@@ -138,7 +138,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
138
138
  />
139
139
 
140
140
  <script>
141
- import { icons } from "../../theme/icons.ts";
141
+ import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
142
142
  import {
143
143
  escapeHtml,
144
144
  highlight,
@@ -0,0 +1,84 @@
1
+ ---
2
+ import specs from "blume:openapi";
3
+ import MethodBadge from "./MethodBadge.astro";
4
+
5
+ interface Props {
6
+ source: string;
7
+ }
8
+
9
+ const { source } = Astro.props;
10
+ const spec = specs[source];
11
+ const operations = Object.values(spec?.operations ?? {});
12
+ const servers =
13
+ ((spec?.document ?? {}) as { servers?: { url?: string }[] }).servers ?? [];
14
+
15
+ // Tags in declared order, then any operation tag not listed under `tags`.
16
+ const declaredSlugs = new Set(spec?.tags.map((tag) => tag.slug));
17
+ const extraSlugs = new Set<string>();
18
+ const extraTags: { slug: string; name: string; description: string }[] = [];
19
+ for (const operation of operations) {
20
+ if (!(declaredSlugs.has(operation.tagSlug) || extraSlugs.has(operation.tagSlug))) {
21
+ extraSlugs.add(operation.tagSlug);
22
+ extraTags.push({ description: "", name: operation.tag, slug: operation.tagSlug });
23
+ }
24
+ }
25
+ const sections = [...(spec?.tags ?? []), ...extraTags];
26
+ ---
27
+
28
+ {
29
+ spec && (
30
+ <div>
31
+ {spec.version && (
32
+ <div class="not-prose mb-4 text-muted-foreground text-sm">
33
+ Version {spec.version}
34
+ </div>
35
+ )}
36
+ {servers.length > 0 && (
37
+ <div class="not-prose mb-8 flex flex-wrap items-center gap-2">
38
+ <span class="text-muted-foreground text-xs">Base URL</span>
39
+ {servers.map((server) => (
40
+ <code class="rounded bg-muted px-2 py-0.5 text-foreground text-xs">
41
+ {server.url}
42
+ </code>
43
+ ))}
44
+ </div>
45
+ )}
46
+ {sections.map((tag) => {
47
+ const ops = operations.filter(
48
+ (operation) => operation.tagSlug === tag.slug
49
+ );
50
+ if (ops.length === 0) {
51
+ return null;
52
+ }
53
+ return (
54
+ <section class="mb-8">
55
+ <h2 class="mb-1 font-semibold text-foreground text-xl">
56
+ {tag.name}
57
+ </h2>
58
+ {tag.description && (
59
+ <div class="mb-3 text-muted-foreground text-sm" set:text={tag.description} />
60
+ )}
61
+ <ul class="not-prose flex list-none flex-col gap-2 p-0">
62
+ {ops.map((operation) => (
63
+ <li>
64
+ <a
65
+ class="flex items-center gap-3 rounded-blume border border-border p-3 text-inherit no-underline! transition-colors hover:border-accent hover:bg-muted hover:no-underline!"
66
+ href={operation.route}
67
+ >
68
+ <MethodBadge method={operation.method} />
69
+ <span class="font-medium text-foreground text-sm">
70
+ {operation.summary || operation.path}
71
+ </span>
72
+ <code class="ml-auto hidden text-muted-foreground text-xs sm:inline">
73
+ {operation.path}
74
+ </code>
75
+ </a>
76
+ </li>
77
+ ))}
78
+ </ul>
79
+ </section>
80
+ );
81
+ })}
82
+ </div>
83
+ )
84
+ }
@@ -0,0 +1,28 @@
1
+ ---
2
+ interface Props {
3
+ method: string;
4
+ class?: string;
5
+ }
6
+
7
+ const { method, class: className } = Astro.props;
8
+ 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
+ };
19
+ const color = COLORS[upper] ?? "bg-muted text-muted-foreground";
20
+ ---
21
+
22
+ <span
23
+ class:list={[
24
+ "not-prose inline-flex items-center rounded-md px-2 py-0.5 font-mono font-semibold text-xs uppercase tracking-wide",
25
+ color,
26
+ className,
27
+ ]}>{upper}</span
28
+ >
@@ -0,0 +1,140 @@
1
+ ---
2
+ import specs from "blume:openapi";
3
+ import type { SchemaLike } from "./helpers.ts";
4
+ import { buildRequestSample, sampleLanguages } from "./snippets.ts";
5
+ import MethodBadge from "./MethodBadge.astro";
6
+ import ParametersTable from "./ParametersTable.astro";
7
+ import RequestBody from "./RequestBody.astro";
8
+ import RequestPanel from "./RequestPanel.astro";
9
+ import Responses from "./Responses.astro";
10
+
11
+ interface Props {
12
+ source: string;
13
+ id: string;
14
+ }
15
+
16
+ interface ParamLike {
17
+ $ref?: string;
18
+ name?: string;
19
+ in?: string;
20
+ description?: string;
21
+ required?: boolean;
22
+ deprecated?: boolean;
23
+ schema?: SchemaLike;
24
+ example?: unknown;
25
+ }
26
+
27
+ interface MediaTypeLike {
28
+ schema?: SchemaLike;
29
+ example?: unknown;
30
+ }
31
+
32
+ interface FullOperation {
33
+ summary?: string;
34
+ description?: string;
35
+ deprecated?: boolean;
36
+ parameters?: ParamLike[];
37
+ requestBody?: {
38
+ description?: string;
39
+ required?: boolean;
40
+ content?: Record<string, MediaTypeLike>;
41
+ };
42
+ responses?: Record<
43
+ string,
44
+ { description?: string; content?: Record<string, MediaTypeLike> }
45
+ >;
46
+ }
47
+
48
+ const { source, id } = Astro.props;
49
+ const spec = specs[source];
50
+ const ref = spec?.operations[id];
51
+
52
+ const doc = (spec?.document ?? {}) as {
53
+ paths?: Record<string, Record<string, unknown> & { parameters?: ParamLike[] }>;
54
+ components?: {
55
+ schemas?: Record<string, SchemaLike>;
56
+ parameters?: Record<string, ParamLike>;
57
+ };
58
+ servers?: { url?: string }[];
59
+ };
60
+ const pathItem = ref ? doc.paths?.[ref.path] : undefined;
61
+ const operation = (
62
+ pathItem && ref ? pathItem[ref.method] : undefined
63
+ ) as FullOperation | undefined;
64
+
65
+ const schemas = doc.components?.schemas ?? {};
66
+ const componentParams = doc.components?.parameters ?? {};
67
+ const PARAM_REF = /#\/components\/parameters\/(?<name>[^/]+)$/u;
68
+ const resolveParam = (param: ParamLike): ParamLike => {
69
+ if (typeof param.$ref === "string") {
70
+ const name = PARAM_REF.exec(param.$ref)?.groups?.name;
71
+ return (name && componentParams[name]) || param;
72
+ }
73
+ return param;
74
+ };
75
+ const params = [
76
+ ...(pathItem?.parameters ?? []),
77
+ ...(operation?.parameters ?? []),
78
+ ].map(resolveParam);
79
+
80
+ const sample =
81
+ ref && operation
82
+ ? buildRequestSample(
83
+ { parameters: params, requestBody: operation.requestBody },
84
+ ref.method,
85
+ ref.path,
86
+ doc.servers ?? [],
87
+ schemas
88
+ )
89
+ : null;
90
+ const languages = sampleLanguages(spec?.codeSamples ?? []);
91
+ ---
92
+
93
+ {
94
+ !(spec && ref && operation) ? (
95
+ <div class="text-muted-foreground">This API operation could not be found.</div>
96
+ ) : (
97
+ <div class="not-prose">
98
+ <div class="mb-6 flex flex-wrap items-center gap-3">
99
+ <MethodBadge method={ref.method} />
100
+ <code class="break-all font-mono text-foreground text-sm">
101
+ {ref.path}
102
+ </code>
103
+ {operation.deprecated && (
104
+ <span class="font-medium text-[0.625rem] text-orange-600 uppercase tracking-wide dark:text-orange-400">
105
+ deprecated
106
+ </span>
107
+ )}
108
+ </div>
109
+ <div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
110
+ <div>
111
+ <ParametersTable parameters={params} schemas={schemas} />
112
+ {operation.requestBody && (
113
+ <RequestBody
114
+ expandAll={spec.expandSchemas}
115
+ requestBody={operation.requestBody}
116
+ schemas={schemas}
117
+ />
118
+ )}
119
+ {operation.responses && (
120
+ <Responses
121
+ expandAll={spec.expandSchemas}
122
+ responses={operation.responses}
123
+ schemas={schemas}
124
+ />
125
+ )}
126
+ </div>
127
+ {sample && (
128
+ <div class="xl:sticky xl:top-24 xl:self-start">
129
+ <RequestPanel
130
+ languages={languages}
131
+ responses={operation.responses ?? {}}
132
+ sample={sample}
133
+ schemas={schemas}
134
+ />
135
+ </div>
136
+ )}
137
+ </div>
138
+ </div>
139
+ )
140
+ }
@@ -0,0 +1,97 @@
1
+ ---
2
+ import {
3
+ constraints,
4
+ resolveSchema,
5
+ type SchemaLike,
6
+ typeLabel,
7
+ } from "./helpers.ts";
8
+
9
+ interface ParamLike {
10
+ name?: string;
11
+ in?: string;
12
+ description?: string;
13
+ required?: boolean;
14
+ deprecated?: boolean;
15
+ schema?: SchemaLike;
16
+ }
17
+
18
+ interface Props {
19
+ parameters: ParamLike[];
20
+ schemas: Record<string, SchemaLike>;
21
+ }
22
+
23
+ const { parameters, schemas } = Astro.props;
24
+
25
+ const SECTIONS: { in: string; title: string }[] = [
26
+ { in: "path", title: "Path parameters" },
27
+ { in: "query", title: "Query parameters" },
28
+ { in: "header", title: "Header parameters" },
29
+ { in: "cookie", title: "Cookie parameters" },
30
+ ];
31
+
32
+ const groups = SECTIONS.map((section) => ({
33
+ items: parameters.filter((param) => param.in === section.in),
34
+ title: section.title,
35
+ })).filter((group) => group.items.length > 0);
36
+ ---
37
+
38
+ {
39
+ groups.map((group) => (
40
+ <section class="mt-6">
41
+ <div
42
+ aria-level="3"
43
+ class="mb-2 font-semibold text-foreground text-sm"
44
+ role="heading"
45
+ >
46
+ {group.title}
47
+ </div>
48
+ <div class="not-prose rounded-blume border border-border px-4">
49
+ {group.items.map((param) => {
50
+ const resolved = resolveSchema(schemas, param.schema ?? {});
51
+ const type = param.schema ? typeLabel(param.schema, schemas) : "string";
52
+ const limits = constraints(resolved);
53
+ const enumValues = Array.isArray(resolved.enum) ? resolved.enum : null;
54
+ return (
55
+ <div class="border-border border-t py-3 first:border-t-0">
56
+ <div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
57
+ <code class="font-mono text-foreground text-sm">{param.name}</code>
58
+ <span class="text-muted-foreground text-xs">{type}</span>
59
+ {param.required && (
60
+ <span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
61
+ required
62
+ </span>
63
+ )}
64
+ {param.deprecated && (
65
+ <span class="font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide line-through">
66
+ deprecated
67
+ </span>
68
+ )}
69
+ </div>
70
+ {param.description && (
71
+ <div
72
+ class="mt-1 text-muted-foreground text-sm"
73
+ set:text={param.description}
74
+ />
75
+ )}
76
+ {limits.length > 0 && (
77
+ <div class="mt-1 text-muted-foreground text-xs">
78
+ {limits.join(" · ")}
79
+ </div>
80
+ )}
81
+ {enumValues && (
82
+ <div class="mt-1 flex flex-wrap items-center gap-1 text-xs">
83
+ <span class="text-muted-foreground">Allowed:</span>
84
+ {enumValues.map((value) => (
85
+ <code class="rounded bg-muted px-1 py-0.5 text-foreground">
86
+ {String(value)}
87
+ </code>
88
+ ))}
89
+ </div>
90
+ )}
91
+ </div>
92
+ );
93
+ })}
94
+ </div>
95
+ </section>
96
+ ))
97
+ }
@@ -0,0 +1,58 @@
1
+ ---
2
+ import type { SchemaLike } from "./helpers.ts";
3
+ import SchemaTable from "./SchemaTable.astro";
4
+
5
+ interface MediaTypeLike {
6
+ schema?: SchemaLike;
7
+ }
8
+
9
+ interface RequestBodyLike {
10
+ description?: string;
11
+ required?: boolean;
12
+ content?: Record<string, MediaTypeLike>;
13
+ }
14
+
15
+ interface Props {
16
+ requestBody: RequestBodyLike;
17
+ schemas: Record<string, SchemaLike>;
18
+ expandAll?: boolean;
19
+ }
20
+
21
+ const { requestBody, schemas, expandAll = false } = Astro.props;
22
+
23
+ const entries = Object.entries(requestBody.content ?? {});
24
+ const chosen = entries.find(([type]) => type.includes("json")) ?? entries[0];
25
+ const contentType = chosen?.[0] ?? "application/json";
26
+ const schema = chosen?.[1]?.schema ?? {};
27
+ ---
28
+
29
+ <section class="mt-8">
30
+ <div class="mb-2 flex flex-wrap items-center gap-2">
31
+ <div
32
+ aria-level="2"
33
+ class="font-semibold text-foreground text-lg"
34
+ role="heading"
35
+ >
36
+ Request body
37
+ </div>
38
+ {
39
+ requestBody.required && (
40
+ <span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
41
+ required
42
+ </span>
43
+ )
44
+ }
45
+ <code class="text-muted-foreground text-xs">{contentType}</code>
46
+ </div>
47
+ {
48
+ requestBody.description && (
49
+ <div
50
+ class="text-muted-foreground text-sm"
51
+ set:text={requestBody.description}
52
+ />
53
+ )
54
+ }
55
+ <div class="mt-3">
56
+ <SchemaTable expandAll={expandAll} schema={schema} schemas={schemas} />
57
+ </div>
58
+ </section>