blume 0.4.0 → 0.5.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.
Files changed (81) hide show
  1. package/dist/cli/index.js +1170 -820
  2. package/dist/cli/index.js.map +32 -27
  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 -41
  6. package/dist/types/core/types.d.ts +7 -6
  7. package/dist/types/migrate/mintlify/config.d.ts +14 -0
  8. package/docs/01-quickstart.mdx +6 -2
  9. package/docs/02-deployment.mdx +3 -1
  10. package/docs/advanced/api-reference.mdx +37 -23
  11. package/docs/advanced/bridge.mdx +76 -0
  12. package/docs/advanced/custom-pages.mdx +3 -1
  13. package/docs/advanced/meta.ts +8 -1
  14. package/docs/advanced/migrate.mdx +123 -0
  15. package/docs/configuration/ai.mdx +3 -1
  16. package/docs/configuration/analytics.mdx +3 -1
  17. package/docs/configuration/export.mdx +6 -2
  18. package/docs/configuration/index.mdx +1 -1
  19. package/docs/configuration/seo.mdx +3 -1
  20. package/docs/content/components.mdx +55 -2
  21. package/docs/content/i18n.mdx +6 -2
  22. package/docs/content/islands.mdx +6 -2
  23. package/docs/content/meta.mdx +3 -1
  24. package/docs/content/syntax.mdx +40 -14
  25. package/docs/index.mdx +2 -2
  26. package/docs/reference/cli.mdx +29 -1
  27. package/docs/reference/frontmatter.mdx +5 -0
  28. package/package.json +11 -1
  29. package/src/astro/generate.ts +18 -9
  30. package/src/astro/templates.ts +28 -4
  31. package/src/cli/commands/build.ts +107 -63
  32. package/src/cli/commands/check.ts +20 -0
  33. package/src/cli/dev-lock.ts +13 -5
  34. package/src/cli/prepare.ts +3 -0
  35. package/src/components/BlumePage.astro +6 -0
  36. package/src/components/Icon.astro +13 -10
  37. package/src/components/content/ApiField.astro +75 -0
  38. package/src/components/content/ParamField.astro +39 -0
  39. package/src/components/content/RequestField.astro +23 -0
  40. package/src/components/content/ResponseField.astro +23 -0
  41. package/src/components/content/Step.astro +1 -1
  42. package/src/components/layout/Breadcrumbs.astro +7 -2
  43. package/src/components/layout/NavTree.astro +24 -8
  44. package/src/components/layout/RootLayout.astro +56 -34
  45. package/src/components/layout/Search.astro +1 -1
  46. package/src/components/openapi/ApiOverview.astro +84 -0
  47. package/src/components/openapi/MethodBadge.astro +28 -0
  48. package/src/components/openapi/Operation.astro +140 -0
  49. package/src/components/openapi/ParametersTable.astro +97 -0
  50. package/src/components/openapi/RequestBody.astro +58 -0
  51. package/src/components/openapi/RequestPanel.astro +169 -0
  52. package/src/components/openapi/Responses.astro +91 -0
  53. package/src/components/openapi/SchemaProperty.astro +118 -0
  54. package/src/components/openapi/SchemaTable.astro +86 -0
  55. package/src/components/openapi/helpers.ts +238 -0
  56. package/src/components/openapi/panel.ts +59 -0
  57. package/src/components/openapi/snippets.ts +201 -0
  58. package/src/core/builtin-tags.ts +5 -0
  59. package/src/core/data.ts +2 -0
  60. package/src/core/graph.ts +0 -3
  61. package/src/core/nav-diagnostics.ts +2 -12
  62. package/src/core/navigation.ts +0 -10
  63. package/src/core/project-graph.ts +5 -1
  64. package/src/core/project.ts +25 -3
  65. package/src/core/schema.ts +47 -14
  66. package/src/core/sources/mintlify.ts +1 -1
  67. package/src/core/sources/resolve.ts +28 -6
  68. package/src/core/types.ts +7 -7
  69. package/src/migrate/mintlify/config.ts +190 -97
  70. package/src/migrate/mintlify/content.ts +24 -2
  71. package/src/migrate/mintlify/index.ts +76 -2
  72. package/src/migrate/mintlify/transform.ts +2 -0
  73. package/src/openapi/model.ts +174 -0
  74. package/src/openapi/parse.ts +48 -0
  75. package/src/openapi/references.ts +164 -0
  76. package/src/openapi/render-mdx.ts +76 -0
  77. package/src/openapi/scalar.ts +15 -103
  78. package/src/openapi/source.ts +140 -0
  79. package/src/registry/eject.ts +15 -2
  80. package/src/theme/chrome-icons.ts +22 -0
  81. package/src/theme/icons.ts +151 -161
@@ -0,0 +1,97 @@
1
+ ---
2
+ import {
3
+ constraints,
4
+ resolveSchema,
5
+ type SchemaLike,
6
+ typeLabel,
7
+ } from "./helpers.ts";
8
+
9
+ interface ParamLike {
10
+ name?: string;
11
+ in?: string;
12
+ description?: string;
13
+ required?: boolean;
14
+ deprecated?: boolean;
15
+ schema?: SchemaLike;
16
+ }
17
+
18
+ interface Props {
19
+ parameters: ParamLike[];
20
+ schemas: Record<string, SchemaLike>;
21
+ }
22
+
23
+ const { parameters, schemas } = Astro.props;
24
+
25
+ const SECTIONS: { in: string; title: string }[] = [
26
+ { in: "path", title: "Path parameters" },
27
+ { in: "query", title: "Query parameters" },
28
+ { in: "header", title: "Header parameters" },
29
+ { in: "cookie", title: "Cookie parameters" },
30
+ ];
31
+
32
+ const groups = SECTIONS.map((section) => ({
33
+ items: parameters.filter((param) => param.in === section.in),
34
+ title: section.title,
35
+ })).filter((group) => group.items.length > 0);
36
+ ---
37
+
38
+ {
39
+ groups.map((group) => (
40
+ <section class="mt-6">
41
+ <div
42
+ aria-level="3"
43
+ class="mb-2 font-semibold text-foreground text-sm"
44
+ role="heading"
45
+ >
46
+ {group.title}
47
+ </div>
48
+ <div class="not-prose rounded-blume border border-border px-4">
49
+ {group.items.map((param) => {
50
+ const resolved = resolveSchema(schemas, param.schema ?? {});
51
+ const type = param.schema ? typeLabel(param.schema, schemas) : "string";
52
+ const limits = constraints(resolved);
53
+ const enumValues = Array.isArray(resolved.enum) ? resolved.enum : null;
54
+ return (
55
+ <div class="border-border border-t py-3 first:border-t-0">
56
+ <div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
57
+ <code class="font-mono text-foreground text-sm">{param.name}</code>
58
+ <span class="text-muted-foreground text-xs">{type}</span>
59
+ {param.required && (
60
+ <span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
61
+ required
62
+ </span>
63
+ )}
64
+ {param.deprecated && (
65
+ <span class="font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide line-through">
66
+ deprecated
67
+ </span>
68
+ )}
69
+ </div>
70
+ {param.description && (
71
+ <div
72
+ class="mt-1 text-muted-foreground text-sm"
73
+ set:text={param.description}
74
+ />
75
+ )}
76
+ {limits.length > 0 && (
77
+ <div class="mt-1 text-muted-foreground text-xs">
78
+ {limits.join(" · ")}
79
+ </div>
80
+ )}
81
+ {enumValues && (
82
+ <div class="mt-1 flex flex-wrap items-center gap-1 text-xs">
83
+ <span class="text-muted-foreground">Allowed:</span>
84
+ {enumValues.map((value) => (
85
+ <code class="rounded bg-muted px-1 py-0.5 text-foreground">
86
+ {String(value)}
87
+ </code>
88
+ ))}
89
+ </div>
90
+ )}
91
+ </div>
92
+ );
93
+ })}
94
+ </div>
95
+ </section>
96
+ ))
97
+ }
@@ -0,0 +1,58 @@
1
+ ---
2
+ import type { SchemaLike } from "./helpers.ts";
3
+ import SchemaTable from "./SchemaTable.astro";
4
+
5
+ interface MediaTypeLike {
6
+ schema?: SchemaLike;
7
+ }
8
+
9
+ interface RequestBodyLike {
10
+ description?: string;
11
+ required?: boolean;
12
+ content?: Record<string, MediaTypeLike>;
13
+ }
14
+
15
+ interface Props {
16
+ requestBody: RequestBodyLike;
17
+ schemas: Record<string, SchemaLike>;
18
+ expandAll?: boolean;
19
+ }
20
+
21
+ const { requestBody, schemas, expandAll = false } = Astro.props;
22
+
23
+ const entries = Object.entries(requestBody.content ?? {});
24
+ const chosen = entries.find(([type]) => type.includes("json")) ?? entries[0];
25
+ const contentType = chosen?.[0] ?? "application/json";
26
+ const schema = chosen?.[1]?.schema ?? {};
27
+ ---
28
+
29
+ <section class="mt-8">
30
+ <div class="mb-2 flex flex-wrap items-center gap-2">
31
+ <div
32
+ aria-level="2"
33
+ class="font-semibold text-foreground text-lg"
34
+ role="heading"
35
+ >
36
+ Request body
37
+ </div>
38
+ {
39
+ requestBody.required && (
40
+ <span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
41
+ required
42
+ </span>
43
+ )
44
+ }
45
+ <code class="text-muted-foreground text-xs">{contentType}</code>
46
+ </div>
47
+ {
48
+ requestBody.description && (
49
+ <div
50
+ class="text-muted-foreground text-sm"
51
+ set:text={requestBody.description}
52
+ />
53
+ )
54
+ }
55
+ <div class="mt-3">
56
+ <SchemaTable expandAll={expandAll} schema={schema} schemas={schemas} />
57
+ </div>
58
+ </section>
@@ -0,0 +1,169 @@
1
+ ---
2
+ import { highlightCode } from "../../markdown/index.ts";
3
+ import { exampleValue, type SchemaLike, toJson } from "./helpers.ts";
4
+ import type { RequestSample, SampleLanguage } from "./snippets.ts";
5
+
6
+ interface MediaTypeLike {
7
+ schema?: SchemaLike;
8
+ example?: unknown;
9
+ }
10
+
11
+ interface ResponseLike {
12
+ description?: string;
13
+ content?: Record<string, MediaTypeLike>;
14
+ }
15
+
16
+ interface Props {
17
+ sample: RequestSample;
18
+ languages: SampleLanguage[];
19
+ responses: Record<string, ResponseLike>;
20
+ schemas: Record<string, SchemaLike>;
21
+ }
22
+
23
+ const { sample, languages, responses, schemas } = Astro.props;
24
+
25
+ // A `.prose` wrapper gives Shiki its scoped token colours; the global style at
26
+ // the foot of this file strips the standalone code block's own box (border,
27
+ // injected copy button, language label) so the code sits flush inside the one
28
+ // panel border.
29
+ const CODE_WRAP = "prose max-w-none text-xs";
30
+ const TAB_CLASS =
31
+ "-mb-px cursor-pointer border-transparent border-b-2 bg-transparent py-2 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground aria-[selected=true]:border-accent aria-[selected=true]:text-foreground";
32
+ const HEADING = "mb-2 font-semibold text-foreground text-sm";
33
+
34
+ const requestSamples = await Promise.all(
35
+ languages.map(async (language) => ({
36
+ html: await highlightCode(language.build(sample), language.lang, {
37
+ icons: false,
38
+ }),
39
+ id: language.id,
40
+ label: language.label,
41
+ }))
42
+ );
43
+
44
+ const responseEntries = await Promise.all(
45
+ Object.entries(responses).map(async ([status, response]) => {
46
+ const media =
47
+ Object.entries(response.content ?? {}).find(([type]) =>
48
+ type.includes("json")
49
+ )?.[1] ?? Object.values(response.content ?? {})[0];
50
+ const example = media
51
+ ? (media.example ?? exampleValue(media.schema, schemas))
52
+ : undefined;
53
+ const html =
54
+ example === undefined || example === null
55
+ ? null
56
+ : await highlightCode(toJson(example), "json", { icons: false });
57
+ return { description: response.description ?? "", html, status };
58
+ })
59
+ );
60
+ ---
61
+
62
+ <div class="not-prose flex flex-col gap-6">
63
+ {
64
+ requestSamples.length > 0 && (
65
+ <div>
66
+ <div aria-level="3" class={HEADING} role="heading">
67
+ Request
68
+ </div>
69
+ <blume-panel-tabs class="block overflow-hidden rounded-blume border border-border bg-background">
70
+ <div class="flex items-center justify-between gap-2 border-border border-b px-3">
71
+ <div class="flex gap-4" role="tablist">
72
+ {requestSamples.map((entry, index) => (
73
+ <button
74
+ aria-selected={index === 0 ? "true" : "false"}
75
+ class={TAB_CLASS}
76
+ data-panel-tab={entry.id}
77
+ role="tab"
78
+ type="button"
79
+ >
80
+ {entry.label}
81
+ </button>
82
+ ))}
83
+ </div>
84
+ <button
85
+ aria-label="Copy request"
86
+ class="group shrink-0 cursor-pointer rounded px-1.5 py-1 text-muted-foreground text-xs hover:text-foreground"
87
+ data-panel-copy
88
+ type="button"
89
+ >
90
+ <span class="group-data-[copied]:hidden">Copy</span>
91
+ <span class="hidden group-data-[copied]:inline">Copied</span>
92
+ </button>
93
+ </div>
94
+ {requestSamples.map((entry, index) => (
95
+ <div
96
+ class:list={[index === 0 ? "" : "hidden", CODE_WRAP]}
97
+ data-panel={entry.id}
98
+ >
99
+ <Fragment set:html={entry.html} />
100
+ </div>
101
+ ))}
102
+ </blume-panel-tabs>
103
+ </div>
104
+ )
105
+ }
106
+ {
107
+ responseEntries.length > 0 && (
108
+ <div>
109
+ <div aria-level="3" class={HEADING} role="heading">
110
+ Response
111
+ </div>
112
+ <blume-panel-tabs class="block overflow-hidden rounded-blume border border-border bg-background">
113
+ <div class="flex flex-wrap gap-4 border-border border-b px-3" role="tablist">
114
+ {responseEntries.map((entry, index) => (
115
+ <button
116
+ aria-selected={index === 0 ? "true" : "false"}
117
+ class={`${TAB_CLASS} font-mono`}
118
+ data-panel-tab={entry.status}
119
+ role="tab"
120
+ type="button"
121
+ >
122
+ {entry.status}
123
+ </button>
124
+ ))}
125
+ </div>
126
+ {responseEntries.map((entry, index) => (
127
+ <div
128
+ class:list={[index === 0 ? "" : "hidden"]}
129
+ data-panel={entry.status}
130
+ >
131
+ {entry.html ? (
132
+ <div class={CODE_WRAP}>
133
+ <Fragment set:html={entry.html} />
134
+ </div>
135
+ ) : (
136
+ <div class="px-3 py-4 text-muted-foreground text-xs">
137
+ {entry.description || "No example response."}
138
+ </div>
139
+ )}
140
+ </div>
141
+ ))}
142
+ </blume-panel-tabs>
143
+ </div>
144
+ )
145
+ }
146
+ </div>
147
+
148
+ <script>
149
+ import "./panel.ts";
150
+ </script>
151
+
152
+ <style is:global>
153
+ /* Strip the standalone code block's own chrome inside a panel: the border,
154
+ margin, radius, and the copy button + language label the prose code theme
155
+ adds — the panel supplies a single border and its own copy button. */
156
+ blume-panel-tabs pre.astro-code {
157
+ margin: 0 !important;
158
+ border: 0 !important;
159
+ border-radius: 0 !important;
160
+ background: transparent !important;
161
+ padding: 0.75rem 1rem !important;
162
+ }
163
+ blume-panel-tabs pre.astro-code::before {
164
+ content: none !important;
165
+ }
166
+ blume-panel-tabs [data-blume-copy] {
167
+ display: none !important;
168
+ }
169
+ </style>
@@ -0,0 +1,91 @@
1
+ ---
2
+ import type { SchemaLike } from "./helpers.ts";
3
+ import SchemaTable from "./SchemaTable.astro";
4
+
5
+ interface MediaTypeLike {
6
+ schema?: SchemaLike;
7
+ }
8
+
9
+ interface ResponseLike {
10
+ description?: string;
11
+ content?: Record<string, MediaTypeLike>;
12
+ }
13
+
14
+ interface Props {
15
+ responses: Record<string, ResponseLike>;
16
+ schemas: Record<string, SchemaLike>;
17
+ expandAll?: boolean;
18
+ }
19
+
20
+ const { responses, schemas, expandAll = false } = Astro.props;
21
+
22
+ const statusColor = (status: string): string => {
23
+ if (status.startsWith("2")) {
24
+ return "bg-green-500/15 text-green-700 dark:text-green-300";
25
+ }
26
+ if (status.startsWith("3")) {
27
+ return "bg-blue-500/15 text-blue-700 dark:text-blue-300";
28
+ }
29
+ if (status.startsWith("4")) {
30
+ return "bg-orange-500/15 text-orange-700 dark:text-orange-300";
31
+ }
32
+ if (status.startsWith("5")) {
33
+ return "bg-red-500/15 text-red-700 dark:text-red-300";
34
+ }
35
+ return "bg-muted text-muted-foreground";
36
+ };
37
+
38
+ const items = Object.entries(responses);
39
+ ---
40
+
41
+ {
42
+ items.length > 0 && (
43
+ <section class="mt-8">
44
+ <div
45
+ aria-level="2"
46
+ class="mb-3 font-semibold text-foreground text-lg"
47
+ role="heading"
48
+ >
49
+ Responses
50
+ </div>
51
+ <div class="flex flex-col gap-4">
52
+ {items.map(([status, response]) => {
53
+ const schema = (
54
+ Object.entries(response.content ?? {}).find(([type]) =>
55
+ type.includes("json")
56
+ )?.[1] ?? Object.values(response.content ?? {})[0]
57
+ )?.schema;
58
+ return (
59
+ <div class="rounded-blume border border-border p-4">
60
+ <div class="flex flex-wrap items-center gap-2">
61
+ <span
62
+ class:list={[
63
+ "not-prose inline-flex items-center rounded-md px-2 py-0.5 font-mono font-semibold text-xs",
64
+ statusColor(status),
65
+ ]}
66
+ >
67
+ {status}
68
+ </span>
69
+ {response.description && (
70
+ <span
71
+ class="text-muted-foreground text-sm"
72
+ set:text={response.description}
73
+ />
74
+ )}
75
+ </div>
76
+ {schema && (
77
+ <div class="mt-3">
78
+ <SchemaTable
79
+ expandAll={expandAll}
80
+ schema={schema}
81
+ schemas={schemas}
82
+ />
83
+ </div>
84
+ )}
85
+ </div>
86
+ );
87
+ })}
88
+ </div>
89
+ </section>
90
+ )
91
+ }
@@ -0,0 +1,118 @@
1
+ ---
2
+ import {
3
+ constraints,
4
+ isNullable,
5
+ refName,
6
+ resolveSchema,
7
+ type SchemaLike,
8
+ typeLabel,
9
+ } from "./helpers.ts";
10
+ import SchemaTable from "./SchemaTable.astro";
11
+
12
+ interface Props {
13
+ name: string;
14
+ schema: SchemaLike;
15
+ required?: boolean;
16
+ schemas: Record<string, SchemaLike>;
17
+ seen?: string[];
18
+ expandAll?: boolean;
19
+ }
20
+
21
+ const {
22
+ name,
23
+ schema,
24
+ required = false,
25
+ schemas,
26
+ seen = [],
27
+ expandAll = false,
28
+ } = Astro.props;
29
+
30
+ const refLabel = typeof schema.$ref === "string" ? refName(schema.$ref) : null;
31
+ const circular = refLabel !== null && seen.includes(refLabel);
32
+ const resolved = resolveSchema(schemas, schema);
33
+
34
+ const type = typeLabel(schema, schemas);
35
+ const description = resolved.description ?? schema.description ?? "";
36
+ const deprecated = resolved.deprecated === true;
37
+ const nullable = isNullable(resolved);
38
+ const enumValues = Array.isArray(resolved.enum) ? resolved.enum : null;
39
+ const limits = constraints(resolved);
40
+
41
+ const types = Array.isArray(resolved.type) ? resolved.type : [resolved.type];
42
+ const isArray = types.includes("array");
43
+ const items = isArray ? resolveSchema(schemas, resolved.items) : null;
44
+
45
+ const hasObjectShape = (candidate: SchemaLike): boolean =>
46
+ Boolean(
47
+ candidate.properties ||
48
+ candidate.allOf ||
49
+ candidate.oneOf ||
50
+ candidate.anyOf ||
51
+ typeof candidate.$ref === "string"
52
+ );
53
+ const expandable =
54
+ !circular && (hasObjectShape(resolved) || Boolean(items && hasObjectShape(items)));
55
+ ---
56
+
57
+ <div class="border-border border-t py-3 first:border-t-0 last:pb-0">
58
+ <div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
59
+ <code class="font-mono text-foreground text-sm">{name}</code>
60
+ <span class="text-muted-foreground text-xs"
61
+ >{type}{nullable ? " | null" : ""}</span
62
+ >
63
+ {
64
+ required && (
65
+ <span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
66
+ required
67
+ </span>
68
+ )
69
+ }
70
+ {
71
+ deprecated && (
72
+ <span class="font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide line-through">
73
+ deprecated
74
+ </span>
75
+ )
76
+ }
77
+ </div>
78
+ {
79
+ description && (
80
+ <div class="mt-1 text-muted-foreground text-sm" set:text={description} />
81
+ )
82
+ }
83
+ {
84
+ limits.length > 0 && (
85
+ <div class="mt-1 text-muted-foreground text-xs">{limits.join(" · ")}</div>
86
+ )
87
+ }
88
+ {
89
+ enumValues && (
90
+ <div class="mt-1 flex flex-wrap items-center gap-1 text-xs">
91
+ <span class="text-muted-foreground">Allowed:</span>
92
+ {enumValues.map((value) => (
93
+ <code class="rounded bg-muted px-1 py-0.5 text-foreground">
94
+ {String(value)}
95
+ </code>
96
+ ))}
97
+ </div>
98
+ )
99
+ }
100
+ {
101
+ expandable && (
102
+ <details class="group mt-2" open={expandAll}>
103
+ <summary class="cursor-pointer select-none text-accent text-xs hover:underline">
104
+ <span class="group-open:hidden">Show properties</span>
105
+ <span class="hidden group-open:inline">Hide properties</span>
106
+ </summary>
107
+ <div class="mt-2 border-border border-l pl-4">
108
+ <SchemaTable
109
+ schema={schema}
110
+ schemas={schemas}
111
+ seen={seen}
112
+ expandAll={expandAll}
113
+ />
114
+ </div>
115
+ </details>
116
+ )
117
+ }
118
+ </div>
@@ -0,0 +1,86 @@
1
+ ---
2
+ import {
3
+ objectProperties,
4
+ refName,
5
+ resolveSchema,
6
+ type SchemaLike,
7
+ typeLabel,
8
+ } from "./helpers.ts";
9
+ import SchemaProperty from "./SchemaProperty.astro";
10
+
11
+ interface Props {
12
+ schema: SchemaLike;
13
+ schemas: Record<string, SchemaLike>;
14
+ /** `$ref` names already on the current branch, to break circular schemas. */
15
+ seen?: string[];
16
+ expandAll?: boolean;
17
+ }
18
+
19
+ const { schema, schemas, seen = [], expandAll = false } = Astro.props;
20
+
21
+ // Track the ref so a self-referential model stops instead of recursing forever.
22
+ const refLabel =
23
+ typeof schema.$ref === "string" ? refName(schema.$ref) : null;
24
+ const circular = refLabel !== null && seen.includes(refLabel);
25
+ const nextSeen = refLabel ? [...seen, refLabel] : seen;
26
+ const resolved = circular ? schema : resolveSchema(schemas, schema);
27
+
28
+ const types = Array.isArray(resolved.type) ? resolved.type : [resolved.type];
29
+ const isArray = types.includes("array");
30
+ // Keep array items unresolved so a self-referential item `$ref` stays trackable
31
+ // via `seen` — resolving here would drop the name and loop forever.
32
+ const items = isArray ? (resolved.items ?? null) : null;
33
+ const branches = resolved.oneOf ?? resolved.anyOf ?? null;
34
+
35
+ const { properties, required } = circular
36
+ ? { properties: [] as [string, SchemaLike][], required: new Set<string>() }
37
+ : objectProperties(resolved, schemas);
38
+ ---
39
+
40
+ {
41
+ circular ? (
42
+ <div class="text-muted-foreground text-sm">
43
+ Circular reference to <code class="text-foreground">{refLabel}</code>.
44
+ </div>
45
+ ) : isArray ? (
46
+ <div>
47
+ <div class="mb-2 text-muted-foreground text-xs">
48
+ Array of <code class="text-foreground">{typeLabel(items ?? {}, schemas)}</code>
49
+ </div>
50
+ {items && (
51
+ <Astro.self schema={items} schemas={schemas} seen={nextSeen} expandAll={expandAll} />
52
+ )}
53
+ </div>
54
+ ) : branches ? (
55
+ <div class="flex flex-col gap-3">
56
+ <div class="text-muted-foreground text-xs">
57
+ {resolved.oneOf ? "One of" : "Any of"}:
58
+ </div>
59
+ {branches.map((branch, index) => (
60
+ <div class="rounded-blume border border-border p-3">
61
+ <div class="mb-2 font-medium text-foreground text-xs">
62
+ {typeLabel(branch, schemas) || `Option ${index + 1}`}
63
+ </div>
64
+ <Astro.self schema={branch} schemas={schemas} seen={nextSeen} expandAll={expandAll} />
65
+ </div>
66
+ ))}
67
+ </div>
68
+ ) : properties.length > 0 ? (
69
+ <div class="not-prose">
70
+ {properties.map(([name, prop]) => (
71
+ <SchemaProperty
72
+ name={name}
73
+ schema={prop}
74
+ required={required.has(name)}
75
+ schemas={schemas}
76
+ seen={nextSeen}
77
+ expandAll={expandAll}
78
+ />
79
+ ))}
80
+ </div>
81
+ ) : (
82
+ <div class="text-muted-foreground text-sm">
83
+ <code class="text-foreground">{typeLabel(resolved, schemas)}</code>
84
+ </div>
85
+ )
86
+ }