blume 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/dist/cli/index.js +13404 -10228
- package/dist/cli/index.js.map +94 -63
- package/dist/types/ai/component-markdown.d.ts +12 -1
- package/dist/types/core/config-input.d.ts +73 -4
- package/dist/types/core/data.d.ts +9 -0
- package/dist/types/core/deployment-env.d.ts +6 -0
- package/dist/types/core/diagnostics.d.ts +23 -0
- package/dist/types/core/i18n-ui.d.ts +8 -8
- package/dist/types/core/schema.d.ts +144 -22
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +41 -0
- package/dist/types/core/types.d.ts +20 -0
- package/dist/types/og/card.d.ts +63 -0
- package/dist/types/og/dimensions.d.ts +12 -0
- package/docs/01-quickstart.mdx +1 -1
- package/docs/02-deployment.mdx +9 -1
- package/docs/advanced/api-reference.mdx +11 -0
- package/docs/advanced/changelog.mdx +2 -2
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +3 -3
- package/docs/configuration/customization.mdx +1 -1
- package/docs/configuration/export.mdx +1 -1
- package/docs/configuration/index.mdx +21 -1
- package/docs/configuration/search.mdx +28 -1
- package/docs/configuration/seo.mdx +37 -2
- package/docs/configuration/theming.mdx +1 -1
- package/docs/content/components.mdx +14 -0
- package/docs/content/index.mdx +1 -1
- package/docs/content/meta.mdx +1 -1
- package/docs/content/navigation.mdx +5 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/reference/cli.mdx +80 -2
- package/docs/reference/frontmatter.mdx +31 -1
- package/package.json +4 -3
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +16 -4
- package/src/ai/component-markdown.ts +39 -11
- package/src/ai/llms.ts +19 -2
- package/src/ai/markdown.ts +5 -1
- package/src/astro/adapter-root.ts +70 -0
- package/src/astro/generate.ts +124 -50
- package/src/astro/index.ts +1 -0
- package/src/astro/pages.ts +39 -8
- package/src/astro/templates.ts +93 -28
- package/src/audit/agent.ts +114 -0
- package/src/audit/catalog.ts +826 -0
- package/src/audit/checks/assets.ts +177 -0
- package/src/audit/checks/content.ts +231 -0
- package/src/audit/checks/duplicates.ts +131 -0
- package/src/audit/checks/i18n.ts +246 -0
- package/src/audit/checks/indexability.ts +213 -0
- package/src/audit/checks/links.ts +223 -0
- package/src/audit/checks/llms.ts +138 -0
- package/src/audit/checks/network.ts +272 -0
- package/src/audit/checks/og-image.ts +113 -0
- package/src/audit/checks/redirects.ts +87 -0
- package/src/audit/checks/robots.ts +114 -0
- package/src/audit/checks/sitemap.ts +229 -0
- package/src/audit/checks/social.ts +238 -0
- package/src/audit/crawl.ts +259 -0
- package/src/audit/graph.ts +74 -0
- package/src/audit/html.ts +54 -0
- package/src/audit/image-size.ts +63 -0
- package/src/audit/locate.ts +33 -0
- package/src/audit/redirects.ts +74 -0
- package/src/audit/report.ts +278 -0
- package/src/audit/run.ts +198 -0
- package/src/audit/snapshot.ts +189 -0
- package/src/audit/types.ts +214 -0
- package/src/audit/url.ts +103 -0
- package/src/cli/commands/audit.ts +205 -0
- package/src/cli/commands/build.ts +64 -13
- package/src/cli/index.ts +2 -0
- package/src/cli/prepare.ts +10 -2
- package/src/components/content/Tabs.astro +98 -15
- package/src/components/layout/Breadcrumbs.astro +1 -1
- package/src/components/layout/Header.astro +1 -0
- package/src/components/layout/PageFeedback.astro +1 -1
- package/src/components/layout/PageLayout.astro +5 -1
- package/src/components/layout/Pagination.astro +1 -1
- package/src/components/layout/RootLayout.astro +5 -3
- package/src/components/layout/Search.astro +35 -6
- package/src/components/layout/TableOfContents.astro +1 -1
- package/src/components/openapi/Authorization.astro +80 -0
- package/src/components/openapi/Operation.astro +19 -1
- package/src/components/openapi/ParametersTable.astro +1 -1
- package/src/components/openapi/security.ts +201 -0
- package/src/components/openapi/snippets.ts +42 -13
- package/src/core/config-input.ts +78 -4
- package/src/core/data.ts +9 -1
- package/src/core/deployment-env.ts +9 -0
- package/src/core/diagnostics.ts +61 -12
- package/src/core/graph.ts +23 -4
- package/src/core/links.ts +2 -91
- package/src/core/nav-diagnostics.ts +48 -4
- package/src/core/navigation.ts +169 -14
- package/src/core/probe.ts +136 -0
- package/src/core/project-graph.ts +54 -20
- package/src/core/schema.ts +93 -3
- package/src/core/sources/github-releases.ts +65 -2
- package/src/core/sources/normalize.ts +198 -25
- package/src/core/sources/types.ts +3 -1
- package/src/core/standard-schema.ts +54 -0
- package/src/core/types.ts +20 -0
- package/src/deploy/adapter-output.ts +27 -15
- package/src/deploy/headers.ts +66 -0
- package/src/deploy/redirects.ts +49 -9
- package/src/markdown/index.ts +1 -0
- package/src/markdown/twoslash.ts +60 -0
- package/src/og/card.ts +98 -33
- package/src/og/index.ts +1 -1
- package/src/registry/eject.ts +3 -1
- package/src/search/popular.ts +33 -0
- package/src/theme/entry.ts +6 -1
- /package/docs/{03-faq.mdx → 07-faq.mdx} +0 -0
|
@@ -4,6 +4,20 @@ interface Props {
|
|
|
4
4
|
defaultTabIndex?: number;
|
|
5
5
|
dropdown?: boolean;
|
|
6
6
|
hash?: boolean | "false" | "true";
|
|
7
|
+
/**
|
|
8
|
+
* Render borderless — a tab strip on a full-width rule with the content
|
|
9
|
+
* flowing beneath as prose — instead of the default bordered box. Reads as
|
|
10
|
+
* page-level navigation rather than an isolated widget; pair with `param` for
|
|
11
|
+
* shareable, multiple-per-page tabs.
|
|
12
|
+
*/
|
|
13
|
+
inline?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Sync the active tab to this URL query-param key instead of the hash. Each
|
|
16
|
+
* group owns its own param, so several `Tabs` can share a page and every
|
|
17
|
+
* selection is deep-linkable (`?install=windows`). Give each group a unique
|
|
18
|
+
* `param`; it takes precedence over `hash`.
|
|
19
|
+
*/
|
|
20
|
+
param?: string;
|
|
7
21
|
sync?: boolean;
|
|
8
22
|
}
|
|
9
23
|
|
|
@@ -12,34 +26,53 @@ const {
|
|
|
12
26
|
defaultTabIndex = 0,
|
|
13
27
|
dropdown = false,
|
|
14
28
|
hash = true,
|
|
29
|
+
inline = false,
|
|
30
|
+
param,
|
|
15
31
|
sync = true,
|
|
16
32
|
} = Astro.props;
|
|
17
33
|
|
|
18
34
|
// MDX string attributes (`hash="false"` from generated markup) must read as
|
|
19
35
|
// their boolean meaning — the string "false" is truthy.
|
|
20
36
|
const hashEnabled = String(hash) !== "false";
|
|
37
|
+
// The dropdown (select) affordance belongs to the boxed layout; inline always
|
|
38
|
+
// renders a tab strip.
|
|
39
|
+
const useDropdown = dropdown && !inline;
|
|
21
40
|
---
|
|
22
41
|
|
|
23
42
|
<blume-tabs
|
|
24
|
-
class=
|
|
43
|
+
class:list={[
|
|
44
|
+
"my-6 block",
|
|
45
|
+
inline
|
|
46
|
+
? ""
|
|
47
|
+
: "not-prose overflow-hidden rounded-blume border border-border bg-background",
|
|
48
|
+
]}
|
|
25
49
|
data-border-bottom={borderBottom ? "true" : "false"}
|
|
26
50
|
data-default-tab-index={defaultTabIndex}
|
|
27
|
-
data-dropdown={
|
|
51
|
+
data-dropdown={useDropdown ? "true" : "false"}
|
|
28
52
|
data-hash={hashEnabled ? "true" : "false"}
|
|
53
|
+
data-param={param}
|
|
29
54
|
data-sync={sync ? "true" : "false"}
|
|
30
55
|
>
|
|
31
|
-
<div
|
|
56
|
+
<div
|
|
57
|
+
class:list={[
|
|
58
|
+
inline ? "not-prose" : "",
|
|
59
|
+
useDropdown ? "" : "overflow-x-auto",
|
|
60
|
+
]}
|
|
61
|
+
>
|
|
32
62
|
<div
|
|
33
63
|
class:list={[
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
useDropdown || borderBottom ? "border-border border-b" : "",
|
|
65
|
+
useDropdown ? "p-3" : inline ? "flex gap-4" : "flex gap-4 px-3",
|
|
36
66
|
]}
|
|
37
67
|
data-blume-tablist
|
|
38
|
-
role={
|
|
68
|
+
role={useDropdown ? undefined : "tablist"}
|
|
39
69
|
>
|
|
40
70
|
</div>
|
|
41
71
|
</div>
|
|
42
|
-
<div
|
|
72
|
+
<div
|
|
73
|
+
class:list={[inline ? "pt-4 [&>[data-blume-tab-panel]]:p-0!" : ""]}
|
|
74
|
+
data-blume-tab-content
|
|
75
|
+
>
|
|
43
76
|
<slot />
|
|
44
77
|
</div>
|
|
45
78
|
</blume-tabs>
|
|
@@ -114,7 +147,7 @@ const hashEnabled = String(hash) !== "false";
|
|
|
114
147
|
if (this.#syncEnabled()) {
|
|
115
148
|
document.addEventListener(SYNC_EVENT, this.#sync);
|
|
116
149
|
}
|
|
117
|
-
|
|
150
|
+
this.#addUrlListener();
|
|
118
151
|
return;
|
|
119
152
|
}
|
|
120
153
|
this.dataset.blumeTabsReady = "true";
|
|
@@ -173,12 +206,13 @@ const hashEnabled = String(hash) !== "false";
|
|
|
173
206
|
if (this.#syncEnabled()) {
|
|
174
207
|
document.addEventListener(SYNC_EVENT, this.#sync);
|
|
175
208
|
}
|
|
176
|
-
|
|
209
|
+
this.#addUrlListener();
|
|
177
210
|
}
|
|
178
211
|
|
|
179
212
|
disconnectedCallback() {
|
|
180
213
|
document.removeEventListener(SYNC_EVENT, this.#sync);
|
|
181
|
-
window.removeEventListener("hashchange", this.#
|
|
214
|
+
window.removeEventListener("hashchange", this.#urlChange);
|
|
215
|
+
window.removeEventListener("popstate", this.#urlChange);
|
|
182
216
|
}
|
|
183
217
|
|
|
184
218
|
activate(index: number, sync: boolean, updateHash: boolean) {
|
|
@@ -201,8 +235,18 @@ const hashEnabled = String(hash) !== "false";
|
|
|
201
235
|
this.#select.value = String(index);
|
|
202
236
|
}
|
|
203
237
|
const id = panelId(activePanel);
|
|
204
|
-
if (updateHash && id
|
|
205
|
-
|
|
238
|
+
if (updateHash && id) {
|
|
239
|
+
const param = this.#paramName();
|
|
240
|
+
if (param) {
|
|
241
|
+
// Query-param mode: write only this group's key, so several groups on
|
|
242
|
+
// one page each deep-link independently. replaceState (not push) keeps
|
|
243
|
+
// the shareable URL current without stacking a history entry per click.
|
|
244
|
+
const url = new URL(window.location.href);
|
|
245
|
+
url.searchParams.set(param, id);
|
|
246
|
+
history.replaceState(history.state, "", url);
|
|
247
|
+
} else if (this.#hashEnabled()) {
|
|
248
|
+
history.replaceState(null, "", `#${encodeURIComponent(id)}`);
|
|
249
|
+
}
|
|
206
250
|
}
|
|
207
251
|
if (sync && this.#syncEnabled()) {
|
|
208
252
|
document.dispatchEvent(
|
|
@@ -239,8 +283,29 @@ const hashEnabled = String(hash) !== "false";
|
|
|
239
283
|
return true;
|
|
240
284
|
}
|
|
241
285
|
|
|
286
|
+
#activateByParam() {
|
|
287
|
+
const param = this.#paramName();
|
|
288
|
+
if (!param) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
const value = new URL(window.location.href).searchParams.get(param);
|
|
292
|
+
if (!value) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
const index = this.#panels.findIndex((panel) => panelId(panel) === value);
|
|
296
|
+
if (index === -1) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.activate(index, false, false);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
242
304
|
#initialIndex() {
|
|
243
|
-
|
|
305
|
+
const activated = this.#paramName()
|
|
306
|
+
? this.#activateByParam()
|
|
307
|
+
: this.#activateByHash();
|
|
308
|
+
if (activated) {
|
|
244
309
|
return this.#panels.findIndex((panel) => !panel.classList.contains("hidden"));
|
|
245
310
|
}
|
|
246
311
|
return boundedIndex(
|
|
@@ -278,6 +343,20 @@ const hashEnabled = String(hash) !== "false";
|
|
|
278
343
|
return this.dataset.hash !== "false";
|
|
279
344
|
}
|
|
280
345
|
|
|
346
|
+
#paramName() {
|
|
347
|
+
return this.dataset.param || null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Query-param changes don't fire `hashchange`; they surface through history
|
|
351
|
+
// navigation (`popstate`). Listen to whichever channel this group syncs on.
|
|
352
|
+
#addUrlListener() {
|
|
353
|
+
if (this.#paramName()) {
|
|
354
|
+
window.addEventListener("popstate", this.#urlChange);
|
|
355
|
+
} else {
|
|
356
|
+
window.addEventListener("hashchange", this.#urlChange);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
281
360
|
#sync = (event: Event) => {
|
|
282
361
|
const title = (event as CustomEvent<{ title?: string }>).detail?.title;
|
|
283
362
|
if (title) {
|
|
@@ -285,8 +364,12 @@ const hashEnabled = String(hash) !== "false";
|
|
|
285
364
|
}
|
|
286
365
|
};
|
|
287
366
|
|
|
288
|
-
#
|
|
289
|
-
this.#
|
|
367
|
+
#urlChange = () => {
|
|
368
|
+
if (this.#paramName()) {
|
|
369
|
+
this.#activateByParam();
|
|
370
|
+
} else {
|
|
371
|
+
this.#activateByHash();
|
|
372
|
+
}
|
|
290
373
|
};
|
|
291
374
|
}
|
|
292
375
|
|
|
@@ -31,7 +31,7 @@ const eyebrowCrumb = crumbs.length > 1 ? crumbs[crumbs.length - 2] : null;
|
|
|
31
31
|
aria-label={n.breadcrumb}
|
|
32
32
|
class:list={[
|
|
33
33
|
"mb-2 text-muted-foreground text-sm",
|
|
34
|
-
wide ? "max-w-none" : "mx-auto max-w-
|
|
34
|
+
wide ? "max-w-none" : "mx-auto max-w-content",
|
|
35
35
|
]}
|
|
36
36
|
>
|
|
37
37
|
{eyebrowCrumb.route ? (
|
|
@@ -20,7 +20,7 @@ const buttonClass =
|
|
|
20
20
|
|
|
21
21
|
<section
|
|
22
22
|
aria-label={f.question}
|
|
23
|
-
class="mx-auto mt-12 flex max-w-
|
|
23
|
+
class="mx-auto mt-12 flex max-w-content items-center justify-between gap-4 border-border border-t pt-6"
|
|
24
24
|
data-blume-page-feedback
|
|
25
25
|
>
|
|
26
26
|
<p class="text-muted-foreground text-sm">{f.question}</p>
|
|
@@ -186,7 +186,11 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
186
186
|
<title>{pageTitle}</title>
|
|
187
187
|
<Favicon appleIcon={appleIcon} favicon={favicon} />
|
|
188
188
|
<Fonts cssVars={fontCssVars ?? []} />
|
|
189
|
-
{
|
|
189
|
+
{/* Google warns against pairing a canonical with noindex — the canonical
|
|
190
|
+
says "index that URL", the robots meta says "distrust this page". */}
|
|
191
|
+
{resolvedCanonical && !noindex && (
|
|
192
|
+
<link href={resolvedCanonical} rel="canonical" />
|
|
193
|
+
)}
|
|
190
194
|
{noindex && <meta content="noindex" name="robots" />}
|
|
191
195
|
{description && <meta content={description} name="description" />}
|
|
192
196
|
<meta content="website" property="og:type" />
|
|
@@ -26,7 +26,7 @@ const s = { ...EN_UI.page, ...strings };
|
|
|
26
26
|
(prev || next) && (
|
|
27
27
|
<nav
|
|
28
28
|
aria-label={s.pagination}
|
|
29
|
-
class="mx-auto mt-12 flex max-w-
|
|
29
|
+
class="mx-auto mt-12 flex max-w-content justify-between gap-4 border-border border-t pt-6 max-md:flex-col"
|
|
30
30
|
>
|
|
31
31
|
{prev ? (
|
|
32
32
|
<a
|
|
@@ -261,7 +261,7 @@ if (isBare) {
|
|
|
261
261
|
} else if (isApiOperation) {
|
|
262
262
|
gridClass = "lg:grid-cols-[17.5rem_minmax(0,1fr)]";
|
|
263
263
|
}
|
|
264
|
-
let articleClass = "prose mx-auto max-w-
|
|
264
|
+
let articleClass = "prose mx-auto max-w-content";
|
|
265
265
|
if (isBare) {
|
|
266
266
|
articleClass = "prose mx-auto max-w-[54rem]";
|
|
267
267
|
} else if (isApiOperation) {
|
|
@@ -345,7 +345,9 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
345
345
|
<title>{pageTitle}</title>
|
|
346
346
|
<Favicon favicon={favicon} appleIcon={appleIcon} />
|
|
347
347
|
<Fonts cssVars={fontCssVars ?? []} />
|
|
348
|
-
{
|
|
348
|
+
{/* Google warns against pairing a canonical with noindex — the canonical
|
|
349
|
+
says "index that URL", the robots meta says "distrust this page". */}
|
|
350
|
+
{canonical && !noindex && <link rel="canonical" href={canonical} />}
|
|
349
351
|
{
|
|
350
352
|
localeAlternates?.map((alt) => (
|
|
351
353
|
<link href={alt.href} hreflang={alt.hreflang} rel="alternate" />
|
|
@@ -592,7 +594,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
592
594
|
route={page.route}
|
|
593
595
|
/>
|
|
594
596
|
{formattedLastModified && (
|
|
595
|
-
<p class="mx-auto mt-10 max-w-
|
|
597
|
+
<p class="mx-auto mt-10 max-w-content text-muted-foreground text-sm">
|
|
596
598
|
{strings.page.lastUpdated} {formattedLastModified}
|
|
597
599
|
</p>
|
|
598
600
|
)}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
3
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
4
4
|
import type { Navigation } from "../../core/types.ts";
|
|
5
|
+
import { resolveIcon } from "../../theme/icons.ts";
|
|
5
6
|
import Icon from "../Icon.astro";
|
|
6
7
|
import { flattenPages } from "./nav-utils.ts";
|
|
7
8
|
|
|
@@ -13,19 +14,43 @@ import { flattenPages } from "./nav-utils.ts";
|
|
|
13
14
|
interface Props {
|
|
14
15
|
askEnabled?: boolean;
|
|
15
16
|
navigation?: Navigation;
|
|
17
|
+
/** Curated empty-state pages; falls back to the first sidebar pages when empty or omitted. */
|
|
18
|
+
popularPages?: { icon?: string; label: string; route: string }[];
|
|
16
19
|
strings?: UIStrings["search"];
|
|
17
20
|
/** Active locale to filter results to; omitted disables locale filtering. */
|
|
18
21
|
locale?: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
const { askEnabled = false, navigation, strings, locale } =
|
|
24
|
+
const { askEnabled = false, navigation, popularPages, strings, locale } =
|
|
25
|
+
Astro.props;
|
|
22
26
|
// Merge over the English baseline per key (rather than `strings ?? …`) so a
|
|
23
27
|
// partial — or empty `{}` — strings object still resolves every label to a
|
|
24
28
|
// default, matching the pattern PageActions uses for its own dictionary.
|
|
25
29
|
const s = { ...EN_UI.search, ...strings };
|
|
26
30
|
|
|
27
|
-
//
|
|
28
|
-
|
|
31
|
+
// Icons resolve to inline SVG here (`theme/icons.ts` is a server-only module —
|
|
32
|
+
// far too large to ship to the browser), so the island gets ready-to-render
|
|
33
|
+
// markup rather than a name it can't resolve. Same approach as AskAI.astro.
|
|
34
|
+
// A name outside the set falls through to the island's own file glyph.
|
|
35
|
+
const iconSvg = (name: string | undefined): string | undefined => {
|
|
36
|
+
const resolved = name ? resolveIcon(name) : null;
|
|
37
|
+
return resolved
|
|
38
|
+
? `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="${resolved.viewBox}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${resolved.body}</svg>`
|
|
39
|
+
: undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Pages shown in the empty state, before the user has typed anything. Only
|
|
43
|
+
// curated entries carry an icon; sidebar-derived rows keep the file glyph.
|
|
44
|
+
const popular =
|
|
45
|
+
popularPages && popularPages.length > 0
|
|
46
|
+
? popularPages.map((page) => ({
|
|
47
|
+
icon: iconSvg(page.icon),
|
|
48
|
+
label: page.label,
|
|
49
|
+
route: page.route,
|
|
50
|
+
}))
|
|
51
|
+
: navigation
|
|
52
|
+
? flattenPages(navigation.sidebar).slice(0, 6)
|
|
53
|
+
: [];
|
|
29
54
|
|
|
30
55
|
const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
31
56
|
---
|
|
@@ -166,6 +191,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
interface PopularPage {
|
|
194
|
+
/** Pre-resolved inline SVG from the server; absent falls back to `file`. */
|
|
195
|
+
icon?: string;
|
|
169
196
|
label: string;
|
|
170
197
|
route: string;
|
|
171
198
|
}
|
|
@@ -501,7 +528,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
501
528
|
if (this.popular.length > 0) {
|
|
502
529
|
const group = this.addGroup(this.popularMsg);
|
|
503
530
|
for (const page of this.popular) {
|
|
504
|
-
const item = this.createLinkRow(page.route, page.label);
|
|
531
|
+
const item = this.createLinkRow(page.route, page.label, page.icon);
|
|
505
532
|
group.appendChild(item.el);
|
|
506
533
|
this.selectables.push(item);
|
|
507
534
|
}
|
|
@@ -590,13 +617,15 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
590
617
|
return item;
|
|
591
618
|
}
|
|
592
619
|
|
|
593
|
-
createLinkRow(url: string, label: string): Selectable {
|
|
620
|
+
createLinkRow(url: string, label: string, icon?: string): Selectable {
|
|
594
621
|
const el = document.createElement("a");
|
|
595
622
|
const href = prefixBase(import.meta.env.BASE_URL, url);
|
|
596
623
|
el.href = href;
|
|
597
624
|
el.className = ROW_CLASS;
|
|
625
|
+
// `icon` is server-resolved markup from the bundled icon set, not
|
|
626
|
+
// author HTML — the label still goes through `escapeHtml`.
|
|
598
627
|
el.innerHTML = `
|
|
599
|
-
<span class="mt-0.5 shrink-0 text-muted-foreground">${svg("file")}</span>
|
|
628
|
+
<span class="mt-0.5 shrink-0 text-muted-foreground">${icon ?? svg("file")}</span>
|
|
600
629
|
<span class="flex-1">
|
|
601
630
|
<span class="block truncate font-normal text-foreground text-sm">${escapeHtml(label)}</span>
|
|
602
631
|
</span>`;
|
|
@@ -19,7 +19,7 @@ const { headings, title, variant } = Astro.props;
|
|
|
19
19
|
|
|
20
20
|
{
|
|
21
21
|
headings.length > 0 && variant === "mobile" && (
|
|
22
|
-
<details class="group mx-auto mb-6 max-w-
|
|
22
|
+
<details class="group mx-auto mb-6 max-w-content rounded-blume border border-border xl:hidden">
|
|
23
23
|
<summary class="flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 font-medium text-foreground text-sm [&::-webkit-details-marker]:hidden">
|
|
24
24
|
<span>{title}</span>
|
|
25
25
|
<Icon
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
type OperationSecurity,
|
|
4
|
+
schemeCarrier,
|
|
5
|
+
schemeLabel,
|
|
6
|
+
} from "./security.ts";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
security: OperationSecurity;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { security } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
security.alternatives.length > 0 && (
|
|
17
|
+
<section class="mt-6">
|
|
18
|
+
<div
|
|
19
|
+
aria-level="2"
|
|
20
|
+
class="mb-2 font-semibold text-foreground text-sm"
|
|
21
|
+
role="heading"
|
|
22
|
+
>
|
|
23
|
+
Authorization
|
|
24
|
+
</div>
|
|
25
|
+
{security.optional && (
|
|
26
|
+
<p class="mb-2 text-muted-foreground text-sm">
|
|
27
|
+
Optional — this operation also accepts unauthenticated requests.
|
|
28
|
+
</p>
|
|
29
|
+
)}
|
|
30
|
+
{security.alternatives.map((alternative, index) => (
|
|
31
|
+
<>
|
|
32
|
+
{index > 0 && (
|
|
33
|
+
<div class="my-2 font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide">
|
|
34
|
+
or
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
<div class="not-prose rounded-blume border border-border px-4">
|
|
38
|
+
{alternative.map((resolved) => {
|
|
39
|
+
const carrier = schemeCarrier(resolved);
|
|
40
|
+
return (
|
|
41
|
+
<div class="border-border border-t py-3 first:border-t-0">
|
|
42
|
+
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
43
|
+
<code class="font-mono text-foreground text-sm">
|
|
44
|
+
{carrier?.name ?? resolved.key}
|
|
45
|
+
</code>
|
|
46
|
+
<span class="text-muted-foreground text-xs">
|
|
47
|
+
{schemeLabel(resolved)}
|
|
48
|
+
{carrier ? ` · ${carrier.in}` : ""}
|
|
49
|
+
</span>
|
|
50
|
+
{!security.optional && (
|
|
51
|
+
<span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
|
|
52
|
+
required
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
{resolved.scheme?.description && (
|
|
57
|
+
<div
|
|
58
|
+
class="mt-1 text-muted-foreground text-sm"
|
|
59
|
+
set:text={resolved.scheme.description}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
{resolved.scopes.length > 0 && (
|
|
63
|
+
<div class="mt-1 flex flex-wrap items-center gap-1 text-xs">
|
|
64
|
+
<span class="text-muted-foreground">Scopes:</span>
|
|
65
|
+
{resolved.scopes.map((scope) => (
|
|
66
|
+
<code class="rounded bg-muted px-1 py-0.5 text-foreground">
|
|
67
|
+
{scope}
|
|
68
|
+
</code>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
</>
|
|
77
|
+
))}
|
|
78
|
+
</section>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -6,7 +6,15 @@ import {
|
|
|
6
6
|
resolveComponentRef,
|
|
7
7
|
type SchemaLike,
|
|
8
8
|
} from "./helpers.ts";
|
|
9
|
+
import {
|
|
10
|
+
effectiveSecurity,
|
|
11
|
+
resolveSecurity,
|
|
12
|
+
sampleAuth,
|
|
13
|
+
type SecurityRequirementLike,
|
|
14
|
+
type SecuritySchemeLike,
|
|
15
|
+
} from "./security.ts";
|
|
9
16
|
import { buildRequestSample, sampleLanguages } from "./snippets.ts";
|
|
17
|
+
import Authorization from "./Authorization.astro";
|
|
10
18
|
import MethodBadge from "./MethodBadge.astro";
|
|
11
19
|
import ParametersTable from "./ParametersTable.astro";
|
|
12
20
|
import RequestBody from "./RequestBody.astro";
|
|
@@ -43,6 +51,7 @@ interface FullOperation {
|
|
|
43
51
|
parameters?: ParameterLike[];
|
|
44
52
|
requestBody?: RequestBodyLike;
|
|
45
53
|
responses?: Record<string, ResponseLike>;
|
|
54
|
+
security?: SecurityRequirementLike[];
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
const { source, id } = Astro.props;
|
|
@@ -59,7 +68,9 @@ const doc = (spec?.document ?? {}) as {
|
|
|
59
68
|
parameters?: Record<string, ParameterLike>;
|
|
60
69
|
requestBodies?: Record<string, RequestBodyLike>;
|
|
61
70
|
responses?: Record<string, ResponseLike>;
|
|
71
|
+
securitySchemes?: Record<string, SecuritySchemeLike>;
|
|
62
72
|
};
|
|
73
|
+
security?: SecurityRequirementLike[];
|
|
63
74
|
servers?: { url?: string }[];
|
|
64
75
|
};
|
|
65
76
|
const pathItem = ref ? doc.paths?.[ref.path] : undefined;
|
|
@@ -84,6 +95,11 @@ const responses = Object.fromEntries(
|
|
|
84
95
|
])
|
|
85
96
|
);
|
|
86
97
|
|
|
98
|
+
const security = resolveSecurity(
|
|
99
|
+
effectiveSecurity(operation?.security, doc.security),
|
|
100
|
+
doc.components?.securitySchemes
|
|
101
|
+
);
|
|
102
|
+
|
|
87
103
|
const sample =
|
|
88
104
|
ref && operation
|
|
89
105
|
? buildRequestSample(
|
|
@@ -91,7 +107,8 @@ const sample =
|
|
|
91
107
|
ref.method,
|
|
92
108
|
ref.path,
|
|
93
109
|
doc.servers ?? [],
|
|
94
|
-
schemas
|
|
110
|
+
schemas,
|
|
111
|
+
sampleAuth(security)
|
|
95
112
|
)
|
|
96
113
|
: null;
|
|
97
114
|
const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
@@ -115,6 +132,7 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
|
115
132
|
</div>
|
|
116
133
|
<div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
|
|
117
134
|
<div>
|
|
135
|
+
<Authorization security={security} />
|
|
118
136
|
<ParametersTable parameters={params} schemas={schemas} />
|
|
119
137
|
{requestBody && (
|
|
120
138
|
<RequestBody
|