blume 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/cli/index.js +1137 -722
  2. package/dist/cli/index.js.map +28 -23
  3. package/dist/types/core/data.d.ts +2 -0
  4. package/dist/types/core/project.d.ts +12 -2
  5. package/dist/types/core/schema.d.ts +154 -15
  6. package/dist/types/core/types.d.ts +7 -0
  7. package/docs/advanced/api-reference.mdx +33 -23
  8. package/docs/advanced/bridge.mdx +74 -0
  9. package/docs/advanced/meta.ts +8 -1
  10. package/docs/advanced/migrate.mdx +119 -0
  11. package/docs/configuration/index.mdx +1 -1
  12. package/docs/content/components.mdx +55 -2
  13. package/docs/content/i18n.mdx +1 -1
  14. package/docs/content/syntax.mdx +2 -2
  15. package/docs/index.mdx +2 -2
  16. package/docs/reference/cli.mdx +29 -1
  17. package/docs/reference/frontmatter.mdx +5 -0
  18. package/package.json +11 -1
  19. package/src/astro/generate.ts +18 -8
  20. package/src/astro/templates.ts +28 -4
  21. package/src/cli/commands/build.ts +107 -63
  22. package/src/cli/commands/check.ts +20 -0
  23. package/src/cli/dev-lock.ts +13 -5
  24. package/src/cli/prepare.ts +3 -0
  25. package/src/components/BlumePage.astro +6 -0
  26. package/src/components/Icon.astro +13 -10
  27. package/src/components/content/ApiField.astro +75 -0
  28. package/src/components/content/ParamField.astro +39 -0
  29. package/src/components/content/RequestField.astro +23 -0
  30. package/src/components/content/ResponseField.astro +23 -0
  31. package/src/components/content/Step.astro +1 -1
  32. package/src/components/layout/Breadcrumbs.astro +7 -2
  33. package/src/components/layout/NavTree.astro +24 -8
  34. package/src/components/layout/RootLayout.astro +56 -34
  35. package/src/components/layout/Search.astro +1 -1
  36. package/src/components/openapi/ApiOverview.astro +84 -0
  37. package/src/components/openapi/MethodBadge.astro +28 -0
  38. package/src/components/openapi/Operation.astro +140 -0
  39. package/src/components/openapi/ParametersTable.astro +97 -0
  40. package/src/components/openapi/RequestBody.astro +58 -0
  41. package/src/components/openapi/RequestPanel.astro +169 -0
  42. package/src/components/openapi/Responses.astro +91 -0
  43. package/src/components/openapi/SchemaProperty.astro +118 -0
  44. package/src/components/openapi/SchemaTable.astro +86 -0
  45. package/src/components/openapi/helpers.ts +238 -0
  46. package/src/components/openapi/panel.ts +59 -0
  47. package/src/components/openapi/snippets.ts +201 -0
  48. package/src/core/builtin-tags.ts +5 -0
  49. package/src/core/data.ts +2 -0
  50. package/src/core/project-graph.ts +5 -1
  51. package/src/core/project.ts +25 -3
  52. package/src/core/schema.ts +47 -6
  53. package/src/core/sources/mintlify.ts +1 -1
  54. package/src/core/sources/resolve.ts +28 -6
  55. package/src/core/types.ts +7 -0
  56. package/src/migrate/mintlify/config.ts +153 -1
  57. package/src/migrate/mintlify/content.ts +8 -2
  58. package/src/migrate/mintlify/index.ts +58 -1
  59. package/src/openapi/model.ts +174 -0
  60. package/src/openapi/parse.ts +48 -0
  61. package/src/openapi/references.ts +164 -0
  62. package/src/openapi/render-mdx.ts +76 -0
  63. package/src/openapi/scalar.ts +15 -103
  64. package/src/openapi/source.ts +140 -0
  65. package/src/registry/eject.ts +15 -2
  66. package/src/theme/chrome-icons.ts +22 -0
  67. package/src/theme/icons.ts +151 -161
@@ -6,6 +6,8 @@ import { defineCommand } from "citty";
6
6
  import { join } from "pathe";
7
7
 
8
8
  import { buildLlmsFiles } from "../../ai/llms.ts";
9
+ import { ensureGitignore } from "../../core/gitignore.ts";
10
+ import type { BlumeProject } from "../../core/project-graph.ts";
9
11
  import type { ResolvedConfig } from "../../core/schema.ts";
10
12
  import { serverFeatures } from "../../core/server-features.ts";
11
13
  import {
@@ -169,6 +171,82 @@ const enforceBudget = async (
169
171
  return passed ? "pass" : "fail";
170
172
  };
171
173
 
174
+ /**
175
+ * Run every deploy post-step of a real (non-isolated) build: the search index +
176
+ * hosted-provider sync, llms.txt, sitemap/robots, redirect files, the summary
177
+ * box, and the optional bundle report / budget gate. Exits non-zero if a budget
178
+ * is exceeded. Isolated verify builds skip all of this.
179
+ */
180
+ const publishBuildArtifacts = async (
181
+ project: BlumeProject,
182
+ distDir: string,
183
+ args: { analyze?: boolean; "budget-css"?: string; "budget-js"?: string }
184
+ ): Promise<void> => {
185
+ if (project.config.search.provider === "pagefind") {
186
+ logger.start("Building search index");
187
+ const indexed = await buildSearchIndex(distDir);
188
+ logger.success(`Indexed ${indexed} page(s) for search`);
189
+ }
190
+
191
+ // Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
192
+ // Skipped with a warning when its admin key isn't configured.
193
+ await syncSearchProvider(project, {
194
+ start: (message) => logger.start(message),
195
+ success: (message) => logger.success(message),
196
+ warn: (message) => logger.warn(message),
197
+ });
198
+
199
+ if (project.config.ai.llmsTxt) {
200
+ const { index, full } = await buildLlmsFiles(project);
201
+ await Promise.all([
202
+ writeFile(join(distDir, "llms.txt"), index, "utf-8"),
203
+ writeFile(join(distDir, "llms-full.txt"), full, "utf-8"),
204
+ ]);
205
+ logger.success("Generated llms.txt and llms-full.txt");
206
+ }
207
+
208
+ // A user's own public/ file (copied into dist by Astro) always wins.
209
+ const sitemap = buildSitemap(project);
210
+ if (sitemap && !existsSync(join(distDir, "sitemap.xml"))) {
211
+ await writeFile(join(distDir, "sitemap.xml"), sitemap, "utf-8");
212
+ logger.success("Generated sitemap.xml");
213
+ }
214
+
215
+ const robots = buildRobots(project);
216
+ if (robots && !existsSync(join(distDir, "robots.txt"))) {
217
+ await writeFile(join(distDir, "robots.txt"), robots, "utf-8");
218
+ logger.success("Generated robots.txt");
219
+ }
220
+
221
+ await emitRedirectFiles(project.config, distDir);
222
+
223
+ const { config } = project;
224
+ const features = serverFeatures(config);
225
+ logger.box(
226
+ [
227
+ `Output ${config.deployment.output}`,
228
+ `Adapter ${config.deployment.adapter ?? "none"}`,
229
+ `Site ${config.deployment.site ?? "not set"}`,
230
+ `Search ${config.search.provider}`,
231
+ `Redirects ${config.redirects.length}`,
232
+ `Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
233
+ `Robots ${robots ? "yes" : "no"}`,
234
+ `LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
235
+ `Server features ${features.length > 0 ? features.join(", ") : "none"}`,
236
+ ].join("\n")
237
+ );
238
+
239
+ if (args.analyze) {
240
+ await reportBundleSizes(distDir);
241
+ }
242
+
243
+ if ((await enforceBudget(distDir, args)) === "fail") {
244
+ process.exit(1);
245
+ }
246
+
247
+ logger.success(`Built to ${distDir}`);
248
+ };
249
+
172
250
  export const buildCommand = defineCommand({
173
251
  args: {
174
252
  adapter: {
@@ -191,6 +269,11 @@ export const buildCommand = defineCommand({
191
269
  description: "Fail if total client JavaScript exceeds this many kB.",
192
270
  type: "string",
193
271
  },
272
+ isolated: {
273
+ description:
274
+ "Build into an isolated .blume-verify runtime (and its own dist) so a running dev server and the real dist/ are untouched. For verifying changes while `blume dev` runs.",
275
+ type: "boolean",
276
+ },
194
277
  output: {
195
278
  description: "Output mode: static | server.",
196
279
  type: "string",
@@ -207,7 +290,18 @@ export const buildCommand = defineCommand({
207
290
  },
208
291
  async run({ args }) {
209
292
  const root = process.cwd();
210
- refuseIfDevRunning(root, "building");
293
+
294
+ // `--isolated` (or BLUME_RUNTIME_DIR) relocates the whole runtime to a
295
+ // sibling dir so this build never touches a live dev server's `.blume/` or
296
+ // the user's real `dist/`. A non-default runtime dir has no dev lock, so the
297
+ // refusal below lets it proceed; a plain build still refuses.
298
+ const runtimeDir = args.isolated
299
+ ? ".blume-verify"
300
+ : process.env.BLUME_RUNTIME_DIR;
301
+ refuseIfDevRunning(root, "building", runtimeDir);
302
+ if (args.isolated) {
303
+ await ensureGitignore(root, [".blume-verify/"]);
304
+ }
211
305
 
212
306
  if (args.output && args.output !== "static" && args.output !== "server") {
213
307
  logger.error(`Invalid --output "${args.output}" (use static | server).`);
@@ -230,6 +324,7 @@ export const buildCommand = defineCommand({
230
324
  },
231
325
  preview: args.preview,
232
326
  root,
327
+ runtimeDir,
233
328
  strict: args.strict,
234
329
  });
235
330
 
@@ -242,70 +337,19 @@ export const buildCommand = defineCommand({
242
337
  root: project.context.outDir,
243
338
  });
244
339
 
245
- const distDir = join(root, "dist");
246
-
247
- if (project.config.search.provider === "pagefind") {
248
- logger.start("Building search index");
249
- const indexed = await buildSearchIndex(distDir);
250
- logger.success(`Indexed ${indexed} page(s) for search`);
251
- }
252
-
253
- // Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
254
- // Skipped with a warning when its admin key isn't configured.
255
- await syncSearchProvider(project, {
256
- start: (message) => logger.start(message),
257
- success: (message) => logger.success(message),
258
- warn: (message) => logger.warn(message),
259
- });
260
-
261
- if (project.config.ai.llmsTxt) {
262
- const { index, full } = await buildLlmsFiles(project);
263
- await Promise.all([
264
- writeFile(join(distDir, "llms.txt"), index, "utf-8"),
265
- writeFile(join(distDir, "llms-full.txt"), full, "utf-8"),
266
- ]);
267
- logger.success("Generated llms.txt and llms-full.txt");
268
- }
340
+ const distDir = project.context.distDir ?? join(root, "dist");
269
341
 
270
- // A user's own public/ file (copied into dist by Astro) always wins.
271
- const sitemap = buildSitemap(project);
272
- if (sitemap && !existsSync(join(distDir, "sitemap.xml"))) {
273
- await writeFile(join(distDir, "sitemap.xml"), sitemap, "utf-8");
274
- logger.success("Generated sitemap.xml");
275
- }
276
-
277
- const robots = buildRobots(project);
278
- if (robots && !existsSync(join(distDir, "robots.txt"))) {
279
- await writeFile(join(distDir, "robots.txt"), robots, "utf-8");
280
- logger.success("Generated robots.txt");
281
- }
282
-
283
- await emitRedirectFiles(project.config, distDir);
284
-
285
- const { config } = project;
286
- const features = serverFeatures(config);
287
- logger.box(
288
- [
289
- `Output ${config.deployment.output}`,
290
- `Adapter ${config.deployment.adapter ?? "none"}`,
291
- `Site ${config.deployment.site ?? "not set"}`,
292
- `Search ${config.search.provider}`,
293
- `Redirects ${config.redirects.length}`,
294
- `Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
295
- `Robots ${robots ? "yes" : "no"}`,
296
- `LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
297
- `Server features ${features.length > 0 ? features.join(", ") : "none"}`,
298
- ].join("\n")
299
- );
300
-
301
- if (args.analyze) {
302
- await reportBundleSizes(distDir);
303
- }
304
-
305
- if ((await enforceBudget(distDir, args)) === "fail") {
306
- process.exit(1);
342
+ // An isolated build is a throwaway verify: it only needs to confirm the site
343
+ // compiles and renders. Skip the network post-steps (search sync) and
344
+ // deploy artifacts (index/llms/sitemap/robots/redirects) that only matter
345
+ // for a real publish and would push to hosted providers.
346
+ if (runtimeDir) {
347
+ logger.success(
348
+ `Isolated build OK — output at ${distDir} (not published).`
349
+ );
350
+ return;
307
351
  }
308
352
 
309
- logger.success(`Built to ${distDir}`);
353
+ await publishBuildArtifacts(project, distDir, args);
310
354
  },
311
355
  });
@@ -5,11 +5,18 @@ import { sync } from "astro";
5
5
  import { defineCommand } from "citty";
6
6
  import { join } from "pathe";
7
7
 
8
+ import { ensureGitignore } from "../../core/gitignore.ts";
9
+ import { refuseIfDevRunning } from "../dev-lock.ts";
8
10
  import { logger } from "../log.ts";
9
11
  import { prepareProject } from "../prepare.ts";
10
12
 
11
13
  export const checkCommand = defineCommand({
12
14
  args: {
15
+ isolated: {
16
+ description:
17
+ "Type-check in an isolated .blume-verify runtime so a running dev server is untouched. For verifying changes while `blume dev` runs.",
18
+ type: "boolean",
19
+ },
13
20
  preview: {
14
21
  description: "Include drafts and unpublished CMS content.",
15
22
  type: "boolean",
@@ -25,10 +32,23 @@ export const checkCommand = defineCommand({
25
32
  },
26
33
  async run({ args }) {
27
34
  const root = process.cwd();
35
+
36
+ // `blume check` regenerates `.blume` just like `build`, so it must refuse a
37
+ // live dev server unless isolated. `--isolated` (or BLUME_RUNTIME_DIR)
38
+ // relocates the runtime to `.blume-verify`, which dev never locks.
39
+ const runtimeDir = args.isolated
40
+ ? ".blume-verify"
41
+ : process.env.BLUME_RUNTIME_DIR;
42
+ refuseIfDevRunning(root, "checking", runtimeDir);
43
+ if (args.isolated) {
44
+ await ensureGitignore(root, [".blume-verify/"]);
45
+ }
46
+
28
47
  const project = await prepareProject({
29
48
  mode: "build",
30
49
  preview: args.preview,
31
50
  root,
51
+ runtimeDir,
32
52
  strict: args.strict,
33
53
  });
34
54
 
@@ -8,6 +8,7 @@ import {
8
8
 
9
9
  import { join } from "pathe";
10
10
 
11
+ import { resolveRuntimeDir } from "../core/project.ts";
11
12
  import { logger } from "./log.ts";
12
13
 
13
14
  /**
@@ -71,13 +72,20 @@ export const acquireDevLock = (outDir: string): (() => void) => {
71
72
  };
72
73
 
73
74
  /**
74
- * Exit with an error when a live `blume dev` owns the `.blume` dir under `root`.
75
- * `action` names the operation being refused (e.g. "building").
75
+ * Exit with an error when a live `blume dev` owns the runtime dir under `root`.
76
+ * `action` names the operation being refused (e.g. "building"). `runtimeDir`
77
+ * relocates the checked dir: an isolated verify (`.blume-verify`) targets a dir
78
+ * dev never locks, so it proceeds; a default or `--runtime-dir .blume` run still
79
+ * refuses.
76
80
  */
77
- export const refuseIfDevRunning = (root: string, action: string): void => {
78
- if (isDevLocked(join(root, ".blume"))) {
81
+ export const refuseIfDevRunning = (
82
+ root: string,
83
+ action: string,
84
+ runtimeDir?: string
85
+ ): void => {
86
+ if (isDevLocked(resolveRuntimeDir(root, runtimeDir))) {
79
87
  logger.error(
80
- `A \`blume dev\` server is running against .blume; ${action} would corrupt it. Stop the dev server first.`
88
+ `A \`blume dev\` server is running against .blume; ${action} would corrupt it. Stop the dev server, or re-run with --isolated to build/verify against .blume-verify without touching it.`
81
89
  );
82
90
  process.exit(1);
83
91
  }
@@ -24,6 +24,8 @@ export interface PrepareOptions {
24
24
  refresh?: boolean;
25
25
  /** CLI config overrides (e.g. `--output`, `--content-dir`). */
26
26
  overrides?: ConfigOverrides;
27
+ /** Relocate the generated runtime (e.g. `.blume-verify` for `--isolated`). */
28
+ runtimeDir?: string;
27
29
  }
28
30
 
29
31
  /**
@@ -45,6 +47,7 @@ export const prepareProject = async (
45
47
  overrides: options.overrides,
46
48
  preview: options.preview,
47
49
  refresh: options.refresh,
50
+ runtimeDir: options.runtimeDir,
48
51
  });
49
52
  } catch (error) {
50
53
  if (error instanceof BlumeError) {
@@ -38,7 +38,10 @@ import FileTree from "./content/FileTree.astro";
38
38
  import Frame from "./content/Frame.astro";
39
39
  import GithubInfo from "./content/GithubInfo.astro";
40
40
  import Panel from "./content/Panel.astro";
41
+ import ParamField from "./content/ParamField.astro";
41
42
  import Prompt from "./content/Prompt.astro";
43
+ import RequestField from "./content/RequestField.astro";
44
+ import ResponseField from "./content/ResponseField.astro";
42
45
  import Step from "./content/Step.astro";
43
46
  import Steps from "./content/Steps.astro";
44
47
  import Tab from "./content/Tab.astro";
@@ -87,7 +90,10 @@ const components = {
87
90
  GithubInfo,
88
91
  Icon,
89
92
  Panel,
93
+ ParamField,
90
94
  Prompt,
95
+ RequestField,
96
+ ResponseField,
91
97
  Step,
92
98
  Steps,
93
99
  Tab,
@@ -1,10 +1,13 @@
1
1
  ---
2
+ import data from "blume:data";
2
3
  import { resolveIcon } from "../theme/icons.ts";
3
4
 
4
5
  interface Props {
5
6
  name?: unknown;
6
7
  icon?: unknown;
7
8
  iconType?: string;
9
+ /** Icon library for a bare name; defaults to the project's `icons.library`. */
10
+ library?: string;
8
11
  size?: number;
9
12
  class?: string;
10
13
  className?: string;
@@ -16,6 +19,7 @@ const {
16
19
  color,
17
20
  icon,
18
21
  iconType,
22
+ library,
19
23
  name,
20
24
  size = 16,
21
25
  class: astroClass,
@@ -37,8 +41,12 @@ const isImageIcon = (value: string): boolean =>
37
41
  /\.(?:avif|gif|jpe?g|png|svg|webp)$/iu.test(value);
38
42
  const imageSrc = iconName && !rawSvg && isImageIcon(iconName) ? iconName : null;
39
43
  const resolvedIcon =
40
- iconName && !(imageSrc || rawSvg) ? resolveIcon(iconName, iconType) : null;
41
- const markup = resolvedIcon?.markup;
44
+ iconName && !(imageSrc || rawSvg)
45
+ ? resolveIcon(iconName, {
46
+ iconType,
47
+ library: library ?? data.config.icons.library,
48
+ })
49
+ : null;
42
50
  const resolvedClass = astroClass ?? className;
43
51
  const customStyle = `display:inline-flex;width:${size}px;height:${size}px;${
44
52
  color ? `color:${color}` : ""
@@ -76,21 +84,16 @@ const style = color ? `color:${color}` : undefined;
76
84
  width={size}
77
85
  />
78
86
  ) : (
79
- markup && (
87
+ resolvedIcon && (
80
88
  <svg
81
89
  aria-hidden={label ? undefined : "true"}
82
90
  aria-label={label}
83
91
  class={resolvedClass}
84
- fill="none"
85
92
  height={size}
86
93
  role={label ? "img" : undefined}
87
- set:html={markup}
88
- stroke="currentColor"
89
- stroke-linecap="round"
90
- stroke-linejoin="round"
91
- stroke-width="2"
94
+ set:html={resolvedIcon.body}
92
95
  style={style}
93
- viewBox="0 0 24 24"
96
+ viewBox={resolvedIcon.viewBox}
94
97
  width={size}
95
98
  xmlns="http://www.w3.org/2000/svg"
96
99
  />
@@ -0,0 +1,75 @@
1
+ ---
2
+ // Shared field row behind the Mintlify-compat <ParamField>/<ResponseField>/
3
+ // <RequestField> components: a labeled name + type + badges, with the field's
4
+ // description rendered from the slot (which may hold rich MDX, including nested
5
+ // <Expandable> or further fields). Styled to match the native OpenAPI reference
6
+ // rows (src/components/openapi/), so a migrated site reads consistently.
7
+ interface Props {
8
+ default?: unknown;
9
+ deprecated?: unknown;
10
+ location?: string;
11
+ name?: string;
12
+ required?: unknown;
13
+ type?: string;
14
+ }
15
+
16
+ const {
17
+ default: defaultValue,
18
+ deprecated,
19
+ location,
20
+ name,
21
+ required,
22
+ type,
23
+ } = Astro.props;
24
+
25
+ // Booleans arrive as MDX shorthand (`required`) or strings (`required="true"`).
26
+ const isTrue = (value: unknown): boolean => value === true || value === "true";
27
+ const hasDefault =
28
+ defaultValue !== undefined && defaultValue !== null && defaultValue !== "";
29
+ const hasDescription = Astro.slots.has("default");
30
+ ---
31
+
32
+ <div class="not-prose my-2 rounded-blume border border-border px-4 py-3">
33
+ <div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
34
+ {name && <code class="font-mono text-foreground text-sm">{name}</code>}
35
+ {
36
+ location && (
37
+ <span class="rounded bg-muted px-1.5 py-0.5 font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide">
38
+ {location}
39
+ </span>
40
+ )
41
+ }
42
+ {type && <span class="text-muted-foreground text-xs">{type}</span>}
43
+ {
44
+ isTrue(required) && (
45
+ <span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
46
+ required
47
+ </span>
48
+ )
49
+ }
50
+ {
51
+ isTrue(deprecated) && (
52
+ <span class="font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide line-through">
53
+ deprecated
54
+ </span>
55
+ )
56
+ }
57
+ </div>
58
+ {
59
+ hasDefault && (
60
+ <div class="mt-1 text-muted-foreground text-xs">
61
+ Default:{" "}
62
+ <code class="rounded bg-muted px-1 py-0.5 text-foreground">
63
+ {String(defaultValue)}
64
+ </code>
65
+ </div>
66
+ )
67
+ }
68
+ {
69
+ hasDescription && (
70
+ <div class="mt-1.5 text-muted-foreground text-sm [&>:first-child]:mt-0 [&>:last-child]:mb-0">
71
+ <slot />
72
+ </div>
73
+ )
74
+ }
75
+ </div>
@@ -0,0 +1,39 @@
1
+ ---
2
+ // Mintlify-compat <ParamField>. Mintlify encodes the parameter's location in the
3
+ // attribute name (`<ParamField path="id" />`, `query`, `header`, `body`); the
4
+ // attribute's value is the field name. Falls back to a plain `name` prop.
5
+ import ApiField from "./ApiField.astro";
6
+
7
+ const {
8
+ body,
9
+ default: defaultValue,
10
+ deprecated,
11
+ header,
12
+ name,
13
+ path,
14
+ query,
15
+ required,
16
+ type,
17
+ } = Astro.props;
18
+
19
+ const locations = [
20
+ { key: "path", value: path },
21
+ { key: "query", value: query },
22
+ { key: "header", value: header },
23
+ { key: "body", value: body },
24
+ ] as const;
25
+ const located = locations.find((entry) => typeof entry.value === "string");
26
+ const fieldName = located ? located.value : name;
27
+ const location = located?.key;
28
+ ---
29
+
30
+ <ApiField
31
+ default={defaultValue}
32
+ deprecated={deprecated}
33
+ location={location}
34
+ name={fieldName}
35
+ required={required}
36
+ type={type}
37
+ >
38
+ <slot />
39
+ </ApiField>
@@ -0,0 +1,23 @@
1
+ ---
2
+ // Mintlify-compat <RequestField>: same shape as <ResponseField> — a named field
3
+ // (`name`, `type`, `required`, `default`, `deprecated`) with a slotted body.
4
+ import ApiField from "./ApiField.astro";
5
+
6
+ const {
7
+ default: defaultValue,
8
+ deprecated,
9
+ name,
10
+ required,
11
+ type,
12
+ } = Astro.props;
13
+ ---
14
+
15
+ <ApiField
16
+ default={defaultValue}
17
+ deprecated={deprecated}
18
+ name={name}
19
+ required={required}
20
+ type={type}
21
+ >
22
+ <slot />
23
+ </ApiField>
@@ -0,0 +1,23 @@
1
+ ---
2
+ // Mintlify-compat <ResponseField>: a named field (`name`, `type`, `required`,
3
+ // `default`, `deprecated`) with its description in the slot.
4
+ import ApiField from "./ApiField.astro";
5
+
6
+ const {
7
+ default: defaultValue,
8
+ deprecated,
9
+ name,
10
+ required,
11
+ type,
12
+ } = Astro.props;
13
+ ---
14
+
15
+ <ApiField
16
+ default={defaultValue}
17
+ deprecated={deprecated}
18
+ name={name}
19
+ required={required}
20
+ type={type}
21
+ >
22
+ <slot />
23
+ </ApiField>
@@ -18,7 +18,7 @@ const canRenderIcon =
18
18
  icon !== undefined &&
19
19
  (/^\s*<svg[\s\S]*<\/svg>\s*$/u.test(icon) ||
20
20
  isImageIcon(icon) ||
21
- hasIcon(icon, iconType));
21
+ hasIcon(icon, { iconType }));
22
22
  ---
23
23
 
24
24
  <div
@@ -4,9 +4,11 @@ import type { Crumb } from "./nav-utils.ts";
4
4
  interface Props {
5
5
  /** Full breadcrumb trail from the site root to the current page. */
6
6
  crumbs: Crumb[];
7
+ /** Left-align full width (for the wide API layout) instead of the prose measure. */
8
+ wide?: boolean;
7
9
  }
8
10
 
9
- const { crumbs } = Astro.props;
11
+ const { crumbs, wide = false } = Astro.props;
10
12
 
11
13
  // The built-in shows a single "eyebrow" crumb — the parent group — rather than
12
14
  // the whole trail. An override receives the complete list and can render more.
@@ -17,7 +19,10 @@ const eyebrowCrumb = crumbs.length > 1 ? crumbs[crumbs.length - 2] : null;
17
19
  eyebrowCrumb && (
18
20
  <nav
19
21
  aria-label="Breadcrumb"
20
- class="mx-auto mb-2 max-w-[42rem] text-muted-foreground text-sm"
22
+ class:list={[
23
+ "mb-2 text-muted-foreground text-sm",
24
+ wide ? "max-w-none" : "mx-auto max-w-[42rem]",
25
+ ]}
21
26
  >
22
27
  {eyebrowCrumb.route ? (
23
28
  <a class="hover:text-foreground" href={eyebrowCrumb.route}>
@@ -31,8 +31,24 @@ const {
31
31
  root = depth === 0,
32
32
  } = Astro.props;
33
33
 
34
- const badgeClass =
35
- "shrink-0 rounded-full bg-muted px-1.5 py-0.5 font-medium text-[0.65rem] text-muted-foreground leading-none";
34
+ const badgeBase =
35
+ "shrink-0 rounded-full px-1.5 py-0.5 font-medium text-[0.65rem] leading-none";
36
+ const badgeClass = `${badgeBase} bg-muted text-muted-foreground`;
37
+ // HTTP-method badges (from an OpenAPI reference's sidebar) are colour-coded;
38
+ // every other badge keeps the neutral style.
39
+ const METHOD_BADGE: Record<string, string> = {
40
+ DELETE: "bg-red-500/15 text-red-700 dark:text-red-300",
41
+ GET: "bg-green-500/15 text-green-700 dark:text-green-300",
42
+ HEAD: "bg-muted text-muted-foreground",
43
+ OPTIONS: "bg-muted text-muted-foreground",
44
+ PATCH: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
45
+ POST: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
46
+ PUT: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
47
+ };
48
+ const badgeClassFor = (badge: string): string => {
49
+ const method = METHOD_BADGE[badge.toUpperCase()];
50
+ return method ? `${badgeBase} ${method}` : badgeClass;
51
+ };
36
52
  const deprecatedClass =
37
53
  "shrink-0 rounded-full bg-amber-500/10 px-1.5 py-0.5 font-medium text-[0.65rem] text-amber-700 leading-none dark:text-amber-300";
38
54
 
@@ -154,7 +170,7 @@ const initialId =
154
170
  />
155
171
  )}
156
172
  <span class="min-w-0 flex-1 truncate">{item.label}</span>
157
- {item.badge && <span class={badgeClass}>{item.badge}</span>}
173
+ {item.badge && <span class={badgeClassFor(item.badge)}>{item.badge}</span>}
158
174
  {item.deprecated && (
159
175
  <span class={deprecatedClass}>deprecated</span>
160
176
  )}
@@ -188,7 +204,7 @@ const initialId =
188
204
  />
189
205
  )}
190
206
  <span class="min-w-0 flex-1 truncate">{item.label}</span>
191
- {item.badge && <span class={badgeClass}>{item.badge}</span>}
207
+ {item.badge && <span class={badgeClassFor(item.badge)}>{item.badge}</span>}
192
208
  <Icon
193
209
  class="shrink-0 text-muted-foreground"
194
210
  name="chevron-right"
@@ -224,7 +240,7 @@ const initialId =
224
240
  )}
225
241
  <span class="min-w-0 flex-1 truncate">{item.label}</span>
226
242
  {item.badge && (
227
- <span class={badgeClass}>{item.badge}</span>
243
+ <span class={badgeClassFor(item.badge)}>{item.badge}</span>
228
244
  )}
229
245
  </a>
230
246
  ) : (
@@ -238,7 +254,7 @@ const initialId =
238
254
  )}
239
255
  <span class="min-w-0 flex-1 truncate">{item.label}</span>
240
256
  {item.badge && (
241
- <span class={badgeClass}>{item.badge}</span>
257
+ <span class={badgeClassFor(item.badge)}>{item.badge}</span>
242
258
  )}
243
259
  </>
244
260
  )}
@@ -279,7 +295,7 @@ const initialId =
279
295
  )}
280
296
  <span class="min-w-0 truncate">{item.label}</span>
281
297
  {item.badge && (
282
- <span class={`ml-auto ${badgeClass}`}>{item.badge}</span>
298
+ <span class={`ml-auto ${badgeClassFor(item.badge)}`}>{item.badge}</span>
283
299
  )}
284
300
  </a>
285
301
  ) : (
@@ -293,7 +309,7 @@ const initialId =
293
309
  )}
294
310
  <span class="min-w-0 truncate">{item.label}</span>
295
311
  {item.badge && (
296
- <span class={`ml-auto ${badgeClass}`}>{item.badge}</span>
312
+ <span class={`ml-auto ${badgeClassFor(item.badge)}`}>{item.badge}</span>
297
313
  )}
298
314
  </>
299
315
  )}