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.
- package/dist/cli/index.js +2985 -6883
- package/dist/cli/index.js.map +27 -58
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +2 -2
- package/dist/types/core/schema.d.ts +58 -453
- package/dist/types/core/types.d.ts +1 -7
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +2 -0
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/theming.mdx +2 -10
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +0 -4
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +4 -6
- package/skills/blume/SKILL.md +71 -0
- package/skills/blume-update-docs/SKILL.md +52 -0
- package/skills/blume-update-docs/references/audit-checklist.md +46 -0
- package/src/astro/generate.ts +15 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +9 -16
- package/src/cli/commands/dev.ts +0 -6
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/index.ts +0 -2
- package/src/components/BlumePage.astro +0 -6
- package/src/components/Icon.astro +1 -12
- package/src/components/content/AccordionItem.astro +3 -6
- package/src/components/content/Badge.astro +1 -3
- package/src/components/content/Callout.astro +3 -9
- package/src/components/content/Card.astro +2 -3
- package/src/components/content/ColorItem.astro +2 -2
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/Prompt.astro +1 -1
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/TypeTable.astro +5 -5
- package/src/components/content/Update.astro +1 -1
- package/src/components/layout/Header.astro +26 -3
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +14 -14
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +31 -2
- package/src/components/layout/Search.astro +5 -5
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +5 -28
- package/src/core/data.ts +2 -2
- package/src/core/graph.ts +1 -4
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/navigation.ts +0 -5
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +13 -90
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +3 -2
- package/src/core/types.ts +1 -8
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +44 -10
- package/src/openapi/source.ts +19 -2
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +18 -109
- package/src/theme/palette.ts +4 -37
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/core/package-json.d.ts +0 -12
- package/dist/types/migrate/mintlify/assets.d.ts +0 -8
- package/dist/types/migrate/mintlify/config.d.ts +0 -16
- package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
- package/dist/types/migrate/shared.d.ts +0 -153
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -124
- package/src/astro/static-assets.ts +0 -124
- package/src/cli/commands/migrate.ts +0 -39
- package/src/components/content/ApiField.astro +0 -75
- package/src/components/content/ParamField.astro +0 -39
- package/src/components/content/RequestField.astro +0 -23
- package/src/components/content/ResponseField.astro +0 -23
- package/src/components/content/Warning.astro +0 -9
- package/src/core/assets.ts +0 -31
- package/src/core/bridge.ts +0 -102
- package/src/core/sources/mintlify.ts +0 -190
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -376
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -355
- package/src/migrate/fumadocs/meta.ts +0 -244
- package/src/migrate/migrate.ts +0 -53
- package/src/migrate/mintlify/assets.ts +0 -46
- package/src/migrate/mintlify/config.ts +0 -954
- package/src/migrate/mintlify/content.ts +0 -120
- package/src/migrate/mintlify/frontmatter.ts +0 -126
- package/src/migrate/mintlify/i18n.ts +0 -51
- package/src/migrate/mintlify/icons.ts +0 -128
- package/src/migrate/mintlify/index.ts +0 -459
- package/src/migrate/mintlify/snippets.ts +0 -315
- package/src/migrate/mintlify/transform.ts +0 -82
- package/src/migrate/nextra/content.ts +0 -46
- package/src/migrate/nextra/frontmatter.ts +0 -40
- package/src/migrate/nextra/index.ts +0 -389
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -801
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -75
- package/src/migrate/starlight/frontmatter.ts +0 -111
- package/src/migrate/starlight/i18n.ts +0 -54
- 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="
|
|
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="
|
|
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
|
|
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="
|
|
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
|
|
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="
|
|
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="
|
|
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
|
|
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="
|
|
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="
|
|
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
|
|
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
|
|
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="
|
|
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="
|
|
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
|
|
@@ -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-
|
|
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="
|
|
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
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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.
|
|
165
|
-
*
|
|
166
|
-
* sidebar
|
|
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) ??
|
|
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
|
+
}
|
package/src/core/builtin-tags.ts
CHANGED
|
@@ -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",
|
package/src/core/config.ts
CHANGED
|
@@ -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
|
-
|
|
81
|
-
? readFileSync(
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
50
|
-
const assetIsPresent = (resolved: string, ctx: LinkContext): boolean =>
|
|
51
|
-
|
|
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/`
|
|
169
|
-
if (ctx.publicDir === null
|
|
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
|
|
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
|
});
|