blume 0.5.3 → 0.5.4

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 (113) hide show
  1. package/dist/cli/index.js +2985 -6883
  2. package/dist/cli/index.js.map +27 -58
  3. package/dist/types/core/config.d.ts +0 -8
  4. package/dist/types/core/data.d.ts +2 -2
  5. package/dist/types/core/schema.d.ts +58 -453
  6. package/dist/types/core/types.d.ts +1 -7
  7. package/docs/advanced/meta.ts +1 -8
  8. package/docs/advanced/skills.mdx +28 -0
  9. package/docs/configuration/ai.mdx +2 -0
  10. package/docs/configuration/index.mdx +13 -17
  11. package/docs/configuration/theming.mdx +2 -10
  12. package/docs/content/components.mdx +2 -53
  13. package/docs/content/i18n.mdx +0 -4
  14. package/docs/content/syntax.mdx +1 -1
  15. package/docs/index.mdx +0 -1
  16. package/docs/reference/cli.mdx +12 -13
  17. package/package.json +4 -6
  18. package/skills/blume/SKILL.md +71 -0
  19. package/skills/blume-update-docs/SKILL.md +52 -0
  20. package/skills/blume-update-docs/references/audit-checklist.md +46 -0
  21. package/src/astro/generate.ts +15 -12
  22. package/src/astro/integration.ts +0 -21
  23. package/src/astro/templates.ts +9 -16
  24. package/src/cli/commands/dev.ts +0 -6
  25. package/src/cli/commands/validate.ts +0 -2
  26. package/src/cli/index.ts +0 -2
  27. package/src/components/BlumePage.astro +0 -6
  28. package/src/components/Icon.astro +1 -12
  29. package/src/components/content/AccordionItem.astro +3 -6
  30. package/src/components/content/Badge.astro +1 -3
  31. package/src/components/content/Callout.astro +3 -9
  32. package/src/components/content/Card.astro +2 -3
  33. package/src/components/content/ColorItem.astro +2 -2
  34. package/src/components/content/Column.astro +1 -1
  35. package/src/components/content/Prompt.astro +1 -1
  36. package/src/components/content/Step.astro +3 -4
  37. package/src/components/content/Tab.astro +2 -3
  38. package/src/components/content/TypeTable.astro +5 -5
  39. package/src/components/content/Update.astro +1 -1
  40. package/src/components/layout/Header.astro +26 -3
  41. package/src/components/layout/Logo.astro +5 -1
  42. package/src/components/layout/NavSelector.astro +1 -1
  43. package/src/components/layout/NavTree.astro +14 -14
  44. package/src/components/layout/PageLayout.astro +42 -0
  45. package/src/components/layout/ReferenceLayout.astro +1 -0
  46. package/src/components/layout/RootLayout.astro +31 -2
  47. package/src/components/layout/Search.astro +5 -5
  48. package/src/components/layout/nav-utils.ts +9 -4
  49. package/src/components/openapi/ApiOverview.astro +4 -50
  50. package/src/components/openapi/ApiTagOperations.astro +42 -0
  51. package/src/core/builtin-tags.ts +1 -3
  52. package/src/core/config.ts +5 -28
  53. package/src/core/data.ts +2 -2
  54. package/src/core/graph.ts +1 -4
  55. package/src/core/links.ts +5 -19
  56. package/src/core/meta.ts +1 -1
  57. package/src/core/navigation.ts +0 -5
  58. package/src/core/project-graph.ts +0 -5
  59. package/src/core/schema.ts +13 -90
  60. package/src/core/sources/resolve.ts +0 -13
  61. package/src/core/sources/watch.ts +3 -2
  62. package/src/core/types.ts +1 -8
  63. package/src/openapi/parse.ts +197 -14
  64. package/src/openapi/render-mdx.ts +44 -10
  65. package/src/openapi/source.ts +19 -2
  66. package/src/theme/entry.ts +45 -17
  67. package/src/theme/icons.ts +18 -109
  68. package/src/theme/palette.ts +4 -37
  69. package/src/theme/twoslash.ts +6 -1
  70. package/dist/types/core/bridge.d.ts +0 -24
  71. package/dist/types/core/package-json.d.ts +0 -12
  72. package/dist/types/migrate/mintlify/assets.d.ts +0 -8
  73. package/dist/types/migrate/mintlify/config.d.ts +0 -16
  74. package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
  75. package/dist/types/migrate/shared.d.ts +0 -153
  76. package/docs/advanced/bridge.mdx +0 -76
  77. package/docs/advanced/migrate.mdx +0 -124
  78. package/src/astro/static-assets.ts +0 -124
  79. package/src/cli/commands/migrate.ts +0 -39
  80. package/src/components/content/ApiField.astro +0 -75
  81. package/src/components/content/ParamField.astro +0 -39
  82. package/src/components/content/RequestField.astro +0 -23
  83. package/src/components/content/ResponseField.astro +0 -23
  84. package/src/components/content/Warning.astro +0 -9
  85. package/src/core/assets.ts +0 -31
  86. package/src/core/bridge.ts +0 -102
  87. package/src/core/sources/mintlify.ts +0 -190
  88. package/src/migrate/fumadocs/config.ts +0 -155
  89. package/src/migrate/fumadocs/content.ts +0 -376
  90. package/src/migrate/fumadocs/frontmatter.ts +0 -18
  91. package/src/migrate/fumadocs/groups.ts +0 -237
  92. package/src/migrate/fumadocs/index.ts +0 -355
  93. package/src/migrate/fumadocs/meta.ts +0 -244
  94. package/src/migrate/migrate.ts +0 -53
  95. package/src/migrate/mintlify/assets.ts +0 -46
  96. package/src/migrate/mintlify/config.ts +0 -954
  97. package/src/migrate/mintlify/content.ts +0 -120
  98. package/src/migrate/mintlify/frontmatter.ts +0 -126
  99. package/src/migrate/mintlify/i18n.ts +0 -51
  100. package/src/migrate/mintlify/icons.ts +0 -128
  101. package/src/migrate/mintlify/index.ts +0 -459
  102. package/src/migrate/mintlify/snippets.ts +0 -315
  103. package/src/migrate/mintlify/transform.ts +0 -82
  104. package/src/migrate/nextra/content.ts +0 -46
  105. package/src/migrate/nextra/frontmatter.ts +0 -40
  106. package/src/migrate/nextra/index.ts +0 -389
  107. package/src/migrate/nextra/meta.ts +0 -266
  108. package/src/migrate/shared.ts +0 -801
  109. package/src/migrate/starlight/config.ts +0 -455
  110. package/src/migrate/starlight/content.ts +0 -75
  111. package/src/migrate/starlight/frontmatter.ts +0 -111
  112. package/src/migrate/starlight/i18n.ts +0 -54
  113. package/src/migrate/starlight/index.ts +0 -131
@@ -129,13 +129,13 @@ const initialId =
129
129
  {panel.route ? (
130
130
  <a
131
131
  aria-current={panel.route === currentRoute ? "page" : undefined}
132
- class="min-w-0 flex-1 truncate font-semibold text-foreground text-sm hover:underline"
132
+ class="flex-1 truncate font-semibold text-foreground text-sm hover:underline"
133
133
  href={panel.route}
134
134
  >
135
135
  {panel.label}
136
136
  </a>
137
137
  ) : (
138
- <span class="min-w-0 flex-1 truncate font-semibold text-foreground text-sm">
138
+ <span class="flex-1 truncate font-semibold text-foreground text-sm">
139
139
  {panel.label}
140
140
  </span>
141
141
  )}
@@ -161,7 +161,7 @@ const initialId =
161
161
  class="block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground"
162
162
  href={item.route}
163
163
  >
164
- <span class="flex min-w-0 items-center gap-2">
164
+ <span class="flex items-center gap-2">
165
165
  {item.icon && (
166
166
  <Icon
167
167
  class="shrink-0 text-muted-foreground"
@@ -169,7 +169,7 @@ const initialId =
169
169
  size={14}
170
170
  />
171
171
  )}
172
- <span class="min-w-0 flex-1 truncate">{item.label}</span>
172
+ <span class="flex-1 truncate">{item.label}</span>
173
173
  {item.badge && <span class={badgeClassFor(item.badge)}>{item.badge}</span>}
174
174
  {item.deprecated && (
175
175
  <span class={deprecatedClass}>deprecated</span>
@@ -192,7 +192,7 @@ const initialId =
192
192
  return (
193
193
  <li class={spacing}>
194
194
  <button
195
- class="flex w-full min-w-0 items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-left font-medium text-foreground text-sm transition-colors hover:bg-muted"
195
+ class="flex w-full items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-left font-medium text-foreground text-sm transition-colors hover:bg-muted"
196
196
  data-nav-to={id}
197
197
  type="button"
198
198
  >
@@ -203,7 +203,7 @@ const initialId =
203
203
  size={14}
204
204
  />
205
205
  )}
206
- <span class="min-w-0 flex-1 truncate">{item.label}</span>
206
+ <span class="flex-1 truncate">{item.label}</span>
207
207
  {item.badge && <span class={badgeClassFor(item.badge)}>{item.badge}</span>}
208
208
  <Icon
209
209
  class="shrink-0 text-muted-foreground"
@@ -222,13 +222,13 @@ const initialId =
222
222
  return (
223
223
  <li class={spacing}>
224
224
  <details class="group" open={open}>
225
- <summary class="mb-1 flex cursor-pointer list-none items-center gap-1.5 rounded-[0.65rem] px-2.5 py-1.5 font-medium text-foreground text-sm transition-colors hover:bg-muted [&::-webkit-details-marker]:hidden">
225
+ <summary class="flex cursor-pointer list-none items-center gap-1.5 rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden">
226
226
  {item.route ? (
227
227
  <a
228
228
  aria-current={
229
229
  item.route === currentRoute ? "page" : undefined
230
230
  }
231
- class="-my-1 flex min-w-0 flex-1 items-center gap-1.5 rounded py-1 text-foreground transition-colors aria-[current=page]:font-semibold"
231
+ class="-my-1 flex flex-1 items-center gap-1.5 rounded py-1 transition-colors aria-[current=page]:text-foreground aria-[current=page]:font-semibold"
232
232
  href={item.route}
233
233
  >
234
234
  {item.icon && (
@@ -238,7 +238,7 @@ const initialId =
238
238
  size={14}
239
239
  />
240
240
  )}
241
- <span class="min-w-0 flex-1 truncate">{item.label}</span>
241
+ <span class="flex-1 truncate">{item.label}</span>
242
242
  {item.badge && (
243
243
  <span class={badgeClassFor(item.badge)}>{item.badge}</span>
244
244
  )}
@@ -252,7 +252,7 @@ const initialId =
252
252
  size={14}
253
253
  />
254
254
  )}
255
- <span class="min-w-0 flex-1 truncate">{item.label}</span>
255
+ <span class="flex-1 truncate">{item.label}</span>
256
256
  {item.badge && (
257
257
  <span class={badgeClassFor(item.badge)}>{item.badge}</span>
258
258
  )}
@@ -279,11 +279,11 @@ const initialId =
279
279
  // `flat` (default): a non-collapsible header with its items beneath.
280
280
  return (
281
281
  <li class={spacing}>
282
- <p class="mb-2 flex min-w-0 items-center gap-1.5 px-2.5 font-medium text-foreground text-sm">
282
+ <p class="mb-2 flex items-center gap-1.5 px-2.5 font-medium text-foreground text-sm">
283
283
  {item.route ? (
284
284
  <a
285
285
  aria-current={item.route === currentRoute ? "page" : undefined}
286
- class="-ml-1 flex min-w-0 flex-1 items-center gap-1.5 rounded px-1 py-0.5 text-foreground transition-colors hover:bg-muted aria-[current=page]:bg-muted"
286
+ class="-ml-1 flex flex-1 items-center gap-1.5 rounded px-1 py-0.5 text-foreground transition-colors hover:bg-muted aria-[current=page]:bg-muted"
287
287
  href={item.route}
288
288
  >
289
289
  {item.icon && (
@@ -293,7 +293,7 @@ const initialId =
293
293
  size={14}
294
294
  />
295
295
  )}
296
- <span class="min-w-0 truncate">{item.label}</span>
296
+ <span class="truncate">{item.label}</span>
297
297
  {item.badge && (
298
298
  <span class={`ml-auto ${badgeClassFor(item.badge)}`}>{item.badge}</span>
299
299
  )}
@@ -307,7 +307,7 @@ const initialId =
307
307
  size={14}
308
308
  />
309
309
  )}
310
- <span class="min-w-0 truncate">{item.label}</span>
310
+ <span class="truncate">{item.label}</span>
311
311
  {item.badge && (
312
312
  <span class={`ml-auto ${badgeClassFor(item.badge)}`}>{item.badge}</span>
313
313
  )}
@@ -190,6 +190,48 @@ const bannerScript = banner?.dismissible
190
190
  </Header>
191
191
  <main id="blume-content"><slot /></main>
192
192
  <slot name="footer" />
193
+ {
194
+ // Mobile nav for chrome-only pages: the header's inline tab bar is hidden
195
+ // below `lg` and there's no sidebar drawer, so surface the tabs in a
196
+ // slide-in drawer instead. It reuses the header's `data-blume-nav-toggle`
197
+ // machinery (drawer top, scroll lock, resize-close) — the hamburger button
198
+ // lives in the header; this is the panel it opens.
199
+ navigation.tabs.length > 0 && (
200
+ <>
201
+ <aside
202
+ aria-label="Navigation"
203
+ class="fixed top-[var(--blume-drawer-top,4rem)] start-0 z-[35] h-[calc(100dvh-var(--blume-drawer-top,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:hidden"
204
+ >
205
+ <nav aria-label="Sections">
206
+ <ul class="m-0 list-none p-0">
207
+ {navigation.tabs.map((tab) => (
208
+ <li>
209
+ <a
210
+ aria-current={
211
+ route === tab.path ||
212
+ (tab.path !== "/" && route.startsWith(tab.path))
213
+ ? "page"
214
+ : undefined
215
+ }
216
+ class="block rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
217
+ href={tab.path}
218
+ >
219
+ {tab.label}
220
+ </a>
221
+ </li>
222
+ ))}
223
+ </ul>
224
+ </nav>
225
+ </aside>
226
+ <button
227
+ aria-label="Close navigation"
228
+ class="fixed inset-0 z-[30] hidden cursor-pointer border-0 bg-black/40 [:where([data-blume-nav-open])_&]:block lg:hidden"
229
+ data-blume-nav-toggle
230
+ type="button"
231
+ />
232
+ </>
233
+ )
234
+ }
193
235
  {
194
236
  clientDataJson && (
195
237
  <script
@@ -18,6 +18,7 @@ interface Props {
18
18
  dark?: string;
19
19
  alt: string;
20
20
  href: string;
21
+ text?: string;
21
22
  } | null;
22
23
  favicon?: {
23
24
  href: string;
@@ -39,6 +39,7 @@ interface Props {
39
39
  dark?: string;
40
40
  alt: string;
41
41
  href: string;
42
+ text?: string;
42
43
  } | null;
43
44
  favicon?: {
44
45
  href: string;
@@ -344,8 +345,36 @@ const bannerScript = banner?.dismissible
344
345
  <div class:list={["mx-auto grid grid-cols-1 items-start", gridClass]}>
345
346
  <aside
346
347
  aria-label="Primary"
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"
348
+ class="fixed top-[var(--blume-drawer-top,4rem)] start-0 z-[35] h-[calc(100dvh-var(--blume-drawer-top,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:top-16 lg:z-auto lg:h-[calc(100dvh-4rem)] lg:w-auto lg:max-w-none lg:translate-x-0! lg:border-e-0 lg:bg-transparent lg:px-4"
348
349
  >
350
+ {
351
+ // The header's tab bar is hidden below `md`, so the drawer is the only
352
+ // way to move between sections on mobile: list the tabs above the
353
+ // page tree, separated by a divider. Hidden from `md` up, where the
354
+ // header tabs take over.
355
+ navigation.tabs.length > 0 && (
356
+ <nav aria-label="Sections" class="mb-4 border-border border-b pb-4 md:hidden">
357
+ <ul class="m-0 list-none p-0">
358
+ {navigation.tabs.map((tab) => (
359
+ <li>
360
+ <a
361
+ aria-current={
362
+ page.route === tab.path ||
363
+ (tab.path !== "/" && page.route.startsWith(tab.path))
364
+ ? "page"
365
+ : undefined
366
+ }
367
+ class="block rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
368
+ href={tab.path}
369
+ >
370
+ {tab.label}
371
+ </a>
372
+ </li>
373
+ ))}
374
+ </ul>
375
+ </nav>
376
+ )
377
+ }
349
378
  <nav>
350
379
  {
351
380
  MobileNavSlot ? (
@@ -363,7 +392,7 @@ const bannerScript = banner?.dismissible
363
392
  }
364
393
  </nav>
365
394
  </aside>
366
- <main class="min-w-0 px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
395
+ <main class="px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
367
396
  <BreadcrumbsSlot crumbs={crumbs} wide={isApiOperation} />
368
397
  <TableOfContentsSlot
369
398
  headings={tocHeadings}
@@ -36,12 +36,12 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
36
36
  >
37
37
  <button
38
38
  aria-label={s.button}
39
- class="inline-flex h-9 min-w-0 cursor-pointer items-center gap-2 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground sm:min-w-48"
39
+ class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground sm:min-w-48"
40
40
  data-blume-search-open
41
41
  type="button"
42
42
  >
43
43
  <Icon name="search" size={16} />
44
- <span class="min-w-0 flex-1 text-start max-sm:hidden">{s.button}</span>
44
+ <span class="flex-1 text-start max-sm:hidden">{s.button}</span>
45
45
  <kbd class="font-mono text-[0.7rem] max-sm:hidden" data-blume-search-kbd
46
46
  >⌘K</kbd
47
47
  >
@@ -459,7 +459,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
459
459
  : "Ask AI";
460
460
  el.innerHTML = `
461
461
  <span class="mt-0.5 shrink-0 text-accent">${svg("sparkles")}</span>
462
- <span class="min-w-0 flex-1">
462
+ <span class="flex-1">
463
463
  <span class="block truncate font-normal text-foreground text-sm">${title}</span>
464
464
  <span class="block truncate text-muted-foreground text-sm">Get an instant answer from AI</span>
465
465
  </span>`;
@@ -479,7 +479,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
479
479
  : "";
480
480
  el.innerHTML = `
481
481
  <span class="mt-0.5 shrink-0 text-muted-foreground">${svg("file")}</span>
482
- <span class="min-w-0 flex-1">
482
+ <span class="flex-1">
483
483
  <span class="block truncate font-normal text-foreground text-sm">${hit.title}</span>
484
484
  ${excerpt}
485
485
  </span>`;
@@ -494,7 +494,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
494
494
  el.className = ROW_CLASS;
495
495
  el.innerHTML = `
496
496
  <span class="mt-0.5 shrink-0 text-muted-foreground">${svg("file")}</span>
497
- <span class="min-w-0 flex-1">
497
+ <span class="flex-1">
498
498
  <span class="block truncate font-normal text-foreground text-sm">${escapeHtml(label)}</span>
499
499
  </span>`;
500
500
  const item: Selectable = { el, kind: "link", url };
@@ -161,9 +161,14 @@ const withoutTabSections = (nodes: NavNode[], tabs: NavTab[]): NavNode[] => {
161
161
  * Adapters / API / AI tabs) drills each tab into its own pages instead of one
162
162
  * global tree, the way Fumadocs' root folders do. On a route under no tab (or
163
163
  * the root `/` tab), the tab-owned groups are hidden so the root sidebar shows
164
- * only pages that don't belong to a tab. Falls back to the full sidebar when a
165
- * matched tab maps to no group, or when hiding the tab sections would blank the
166
- * sidebar, so a route is never left empty.
164
+ * only pages that don't belong to a tab.
165
+ *
166
+ * When a matched tab owns no sidebar group a standalone page like the
167
+ * generated changelog timeline (`/changelog`), or a tab whose source produced
168
+ * no pages — the sidebar is empty. It must not fall back to the full tree: that
169
+ * would leak every *other* tab's section (e.g. the OpenAPI operations) onto the
170
+ * page. On a route under no tab, hiding the tab sections falls back to the full
171
+ * sidebar only when it would otherwise blank, so an un-tabbed route stays full.
167
172
  */
168
173
  export const sidebarForRoute = (
169
174
  sidebar: NavNode[],
@@ -172,7 +177,7 @@ export const sidebarForRoute = (
172
177
  ): NavNode[] => {
173
178
  const tab = activeTab(tabs, route);
174
179
  if (tab) {
175
- return sectionChildren(sidebar, tab.path) ?? sidebar;
180
+ return sectionChildren(sidebar, tab.path) ?? [];
176
181
  }
177
182
  const scoped = withoutTabSections(sidebar, tabs);
178
183
  return scoped.length > 0 ? scoped : sidebar;
@@ -1,28 +1,18 @@
1
1
  ---
2
2
  import specs from "blume:openapi";
3
- import MethodBadge from "./MethodBadge.astro";
4
3
 
4
+ // The spec-level metadata block (version + base URLs) at the top of an API
5
+ // overview page. The tag sections that follow are emitted by `overviewMdx` as
6
+ // markdown headings plus `<ApiTagOperations>` lists, so they land in the
7
+ // table of contents.
5
8
  interface Props {
6
9
  source: string;
7
10
  }
8
11
 
9
12
  const { source } = Astro.props;
10
13
  const spec = specs[source];
11
- const operations = Object.values(spec?.operations ?? {});
12
14
  const servers =
13
15
  ((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
16
  ---
27
17
 
28
18
  {
@@ -43,42 +33,6 @@ const sections = [...(spec?.tags ?? []), ...extraTags];
43
33
  ))}
44
34
  </div>
45
35
  )}
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
36
  </div>
83
37
  )
84
38
  }
@@ -0,0 +1,42 @@
1
+ ---
2
+ import specs from "blume:openapi";
3
+ import MethodBadge from "./MethodBadge.astro";
4
+
5
+ // The operation-link list for one tag section of an API overview page. The
6
+ // section heading itself is emitted by `overviewMdx` as a markdown `##` — so it
7
+ // flows into the table of contents — and this component renders only the list
8
+ // of operations beneath it.
9
+ interface Props {
10
+ source: string;
11
+ /** Tag slug (`ApiOperationRef.tagSlug`) selecting this section's operations. */
12
+ tag: string;
13
+ }
14
+
15
+ const { source, tag } = Astro.props;
16
+ const operations = Object.values(specs[source]?.operations ?? {}).filter(
17
+ (operation) => operation.tagSlug === tag
18
+ );
19
+ ---
20
+
21
+ {
22
+ operations.length > 0 && (
23
+ <ul class="not-prose my-4 flex list-none flex-col gap-2 p-0">
24
+ {operations.map((operation) => (
25
+ <li>
26
+ <a
27
+ 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!"
28
+ href={operation.route}
29
+ >
30
+ <MethodBadge method={operation.method} />
31
+ <span class="font-medium text-foreground text-sm">
32
+ {operation.summary || operation.path}
33
+ </span>
34
+ <code class="ml-auto hidden text-muted-foreground text-xs sm:inline">
35
+ {operation.path}
36
+ </code>
37
+ </a>
38
+ </li>
39
+ ))}
40
+ </ul>
41
+ )
42
+ }
@@ -8,6 +8,7 @@ export const BUILTIN_MDX_TAGS = new Set<string>([
8
8
  "Accordion",
9
9
  "AccordionItem",
10
10
  "ApiOverview",
11
+ "ApiTagOperations",
11
12
  "AutoTypeTable",
12
13
  "Badge",
13
14
  "Callout",
@@ -28,10 +29,7 @@ export const BUILTIN_MDX_TAGS = new Set<string>([
28
29
  "Math",
29
30
  "Operation",
30
31
  "Panel",
31
- "ParamField",
32
32
  "Prompt",
33
- "RequestField",
34
- "ResponseField",
35
33
  "Step",
36
34
  "Steps",
37
35
  "Tab",
@@ -1,7 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
 
3
- import { detectMintlifyBridge } from "./bridge.ts";
4
- import type { BridgeDetection } from "./bridge.ts";
5
3
  import { applyDeploymentEnv } from "./deployment-env.ts";
6
4
  import { BlumeError, diagnosticsFromZod } from "./diagnostics.ts";
7
5
  import { createModuleLoader } from "./load-module.ts";
@@ -16,20 +14,11 @@ import type { Diagnostic } from "./types.ts";
16
14
  */
17
15
  export const defineConfig = (config: BlumeConfig): BlumeConfig => config;
18
16
 
19
- /** Bridge mode info: a foreign docs tool Blume is serving without migrating. */
20
- export interface ConfigBridge {
21
- tool: "mintlify";
22
- /** Absolute path of the foreign config file (`docs.json`/`mint.json`). */
23
- configFile: string;
24
- }
25
-
26
17
  /** Result of loading + validating a project config. */
27
18
  export interface ConfigLoadResult {
28
19
  config: ResolvedConfig;
29
20
  /** Absolute path of the config file used, or null when defaults were used. */
30
21
  configFile: string | null;
31
- /** Set when a foreign docs config (e.g. Mintlify) is being bridged. */
32
- bridge: ConfigBridge | null;
33
22
  diagnostics: Diagnostic[];
34
23
  }
35
24
 
@@ -50,9 +39,6 @@ export const loadConfig = async (
50
39
  ): Promise<ConfigLoadResult> => {
51
40
  const configFile = findConfigFile(root);
52
41
 
53
- // With no Blume config, a Mintlify `docs.json` activates bridge mode: serve
54
- // the unconverted project by synthesizing config + a `mintlify` content source.
55
- let bridge: BridgeDetection | null = null;
56
42
  let raw: unknown = {};
57
43
  if (configFile) {
58
44
  try {
@@ -65,30 +51,24 @@ export const loadConfig = async (
65
51
  severity: "error",
66
52
  });
67
53
  }
68
- } else {
69
- bridge = await detectMintlifyBridge(root);
70
- if (bridge) {
71
- ({ raw } = bridge);
72
- }
73
54
  }
74
55
 
75
- const sourceFile = bridge?.configFile ?? configFile;
76
56
  const parsed = blumeConfigSchema.safeParse(raw ?? {});
77
57
  if (!parsed.success) {
78
58
  // Read the raw config text (when on disk) so errors carry a line/column.
79
59
  const source =
80
- sourceFile && existsSync(sourceFile)
81
- ? readFileSync(sourceFile, "utf-8")
60
+ configFile && existsSync(configFile)
61
+ ? readFileSync(configFile, "utf-8")
82
62
  : undefined;
83
63
  const diagnostics = diagnosticsFromZod(parsed.error, {
84
64
  code: "BLUME_CONFIG_INVALID",
85
- file: sourceFile ?? undefined,
65
+ file: configFile ?? undefined,
86
66
  source,
87
67
  });
88
68
  const [first, ...rest] = diagnostics;
89
69
  const primary = first ?? {
90
70
  code: "BLUME_CONFIG_INVALID",
91
- file: sourceFile ?? undefined,
71
+ file: configFile ?? undefined,
92
72
  message: "Invalid Blume config.",
93
73
  severity: "error" as const,
94
74
  };
@@ -115,15 +95,12 @@ export const loadConfig = async (
115
95
  const ogEnabled = config.seo.og.enabled ?? Boolean(site);
116
96
 
117
97
  return {
118
- bridge: bridge
119
- ? { configFile: bridge.configFile, tool: bridge.tool }
120
- : null,
121
98
  config: {
122
99
  ...config,
123
100
  deployment: { ...config.deployment, site },
124
101
  seo: { ...config.seo, og: { ...config.seo.og, enabled: ogEnabled } },
125
102
  },
126
- configFile: sourceFile,
103
+ configFile,
127
104
  diagnostics: [],
128
105
  };
129
106
  };
package/src/core/data.ts CHANGED
@@ -17,6 +17,8 @@ export interface BlumeLogo {
17
17
  dark?: string;
18
18
  alt: string;
19
19
  href: string;
20
+ /** Wordmark text beside the mark; `undefined` falls back to the site title. */
21
+ text?: string;
20
22
  }
21
23
 
22
24
  /** A favicon or apple-touch-icon: a link href plus an optional MIME type. */
@@ -93,8 +95,6 @@ export interface BlumeDataConfig {
93
95
  favicon: BlumeFavicon;
94
96
  feedback: boolean;
95
97
  i18n: BlumeDataI18n | null;
96
- /** Default icon library for bare `icon` names. */
97
- icons: ResolvedConfig["icons"];
98
98
  /** `markdown.imageZoom`: click-to-zoom content images. */
99
99
  imageZoom: boolean;
100
100
  logo: BlumeLogo | null;
package/src/core/graph.ts CHANGED
@@ -60,7 +60,7 @@ export const buildContentGraph = (
60
60
  }
61
61
 
62
62
  // Each locale gets an independent tree from its own pages and folder meta,
63
- // so navigation may diverge per language (Mintlify-style).
63
+ // so navigation may diverge per language.
64
64
  for (const { code } of i18n.locales) {
65
65
  // Localize internal tab paths so a header tab points to its in-locale
66
66
  // route (e.g. `/docs` -> `/fr/docs`); external paths pass through.
@@ -89,7 +89,6 @@ export const buildContentGraph = (
89
89
  }
90
90
 
91
91
  navigationByLocale[code] = buildNavigation(localePages, {
92
- chromeVariants: options.navigation.chromeVariants,
93
92
  folderMeta: options.folderMeta,
94
93
  // Meta files live in locale directories only under the `dir` parser
95
94
  // (`fr/guides/meta.ts` -> key `fr/guides`). Under `dot`, translations
@@ -105,14 +104,12 @@ export const buildContentGraph = (
105
104
  });
106
105
  }
107
106
  navigation = navigationByLocale[i18n.defaultLocale] ?? {
108
- chromeVariants: [],
109
107
  selectors: [],
110
108
  sidebar: [],
111
109
  tabs: [],
112
110
  };
113
111
  } else {
114
112
  navigation = buildNavigation(pages, {
115
- chromeVariants: options.navigation.chromeVariants,
116
113
  folderMeta: options.folderMeta,
117
114
  selectors: options.navigation.selectors,
118
115
  sharedFolderMeta: options.sharedFolderMeta,
package/src/core/links.ts CHANGED
@@ -2,7 +2,6 @@ import { existsSync } from "node:fs";
2
2
 
3
3
  import { basename, join } from "pathe";
4
4
 
5
- import type { AssetMount } from "./assets.ts";
6
5
  import type {
7
6
  ContentGraph,
8
7
  Diagnostic,
@@ -38,25 +37,15 @@ interface ExternalRef extends LinkSite {
38
37
  /** Lookups derived once from the content graph. */
39
38
  interface LinkContext {
40
39
  anchors: Map<string, Set<string>>;
41
- /** `content.assets` mounts served alongside `public/` (checked in place). */
42
- assetMounts: AssetMount[];
43
40
  publicDir: string | null;
44
41
  /** Normalized `redirect.from` paths — valid targets that resolve at runtime. */
45
42
  redirects: Set<string>;
46
43
  routes: Set<string>;
47
44
  }
48
45
 
49
- /** Whether a resolved asset path exists under `public/` or an asset mount. */
50
- const assetIsPresent = (resolved: string, ctx: LinkContext): boolean => {
51
- if (ctx.publicDir && existsSync(join(ctx.publicDir, resolved))) {
52
- return true;
53
- }
54
- return ctx.assetMounts.some(
55
- (mount) =>
56
- (resolved === mount.url || resolved.startsWith(`${mount.url}/`)) &&
57
- existsSync(join(mount.dir, resolved.slice(mount.url.length)))
58
- );
59
- };
46
+ /** Whether a resolved asset path exists under `public/`. */
47
+ const assetIsPresent = (resolved: string, ctx: LinkContext): boolean =>
48
+ ctx.publicDir !== null && existsSync(join(ctx.publicDir, resolved));
60
49
 
61
50
  /** Outcome of classifying one link target. */
62
51
  type LinkResult = Diagnostic | "asset-unchecked" | null;
@@ -165,8 +154,8 @@ const checkPathLink = (
165
154
  if (assetIsPresent(resolved, ctx)) {
166
155
  return null;
167
156
  }
168
- // Nowhere to look: no `public/` and no asset mounts configured.
169
- if (ctx.publicDir === null && ctx.assetMounts.length === 0) {
157
+ // Nowhere to look: no `public/` directory.
158
+ if (ctx.publicDir === null) {
170
159
  return "asset-unchecked";
171
160
  }
172
161
  return {
@@ -347,15 +336,12 @@ export const validateLinks = async (
347
336
  options: {
348
337
  publicDir: string | null;
349
338
  checkExternal?: boolean;
350
- /** `content.assets` mounts served alongside `public/`. */
351
- assetMounts?: AssetMount[];
352
339
  /** Configured redirects; their `from` paths count as valid link targets. */
353
340
  redirects?: { from: string }[];
354
341
  }
355
342
  ): Promise<Diagnostic[]> => {
356
343
  const ctx: LinkContext = {
357
344
  anchors: buildAnchorIndex(graph.pages),
358
- assetMounts: options.assetMounts ?? [],
359
345
  publicDir: options.publicDir,
360
346
  redirects: new Set(
361
347
  (options.redirects ?? []).map((redirect) => toRoute(redirect.from))
package/src/core/meta.ts CHANGED
@@ -39,7 +39,7 @@ export const discoverFolderMeta = async (
39
39
  absolute: true,
40
40
  cwd: contentRoot,
41
41
  // Never descend into dependencies or build output — relevant when the
42
- // content root is the project root (e.g. a migrated Mintlify project).
42
+ // content root is the project root (e.g. a `.`-rooted or all-staged project).
43
43
  ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
44
44
  onlyFiles: true,
45
45
  });