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.
- package/dist/cli/index.js +1170 -820
- package/dist/cli/index.js.map +32 -27
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/project.d.ts +12 -2
- package/dist/types/core/schema.d.ts +154 -41
- package/dist/types/core/types.d.ts +7 -6
- package/dist/types/migrate/mintlify/config.d.ts +14 -0
- package/docs/01-quickstart.mdx +6 -2
- package/docs/02-deployment.mdx +3 -1
- package/docs/advanced/api-reference.mdx +37 -23
- package/docs/advanced/bridge.mdx +76 -0
- package/docs/advanced/custom-pages.mdx +3 -1
- package/docs/advanced/meta.ts +8 -1
- package/docs/advanced/migrate.mdx +123 -0
- package/docs/configuration/ai.mdx +3 -1
- package/docs/configuration/analytics.mdx +3 -1
- package/docs/configuration/export.mdx +6 -2
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/seo.mdx +3 -1
- package/docs/content/components.mdx +55 -2
- package/docs/content/i18n.mdx +6 -2
- package/docs/content/islands.mdx +6 -2
- package/docs/content/meta.mdx +3 -1
- package/docs/content/syntax.mdx +40 -14
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +29 -1
- package/docs/reference/frontmatter.mdx +5 -0
- package/package.json +11 -1
- package/src/astro/generate.ts +18 -9
- package/src/astro/templates.ts +28 -4
- package/src/cli/commands/build.ts +107 -63
- package/src/cli/commands/check.ts +20 -0
- package/src/cli/dev-lock.ts +13 -5
- package/src/cli/prepare.ts +3 -0
- package/src/components/BlumePage.astro +6 -0
- package/src/components/Icon.astro +13 -10
- package/src/components/content/ApiField.astro +75 -0
- package/src/components/content/ParamField.astro +39 -0
- package/src/components/content/RequestField.astro +23 -0
- package/src/components/content/ResponseField.astro +23 -0
- package/src/components/content/Step.astro +1 -1
- package/src/components/layout/Breadcrumbs.astro +7 -2
- package/src/components/layout/NavTree.astro +24 -8
- package/src/components/layout/RootLayout.astro +56 -34
- package/src/components/layout/Search.astro +1 -1
- package/src/components/openapi/ApiOverview.astro +84 -0
- package/src/components/openapi/MethodBadge.astro +28 -0
- package/src/components/openapi/Operation.astro +140 -0
- package/src/components/openapi/ParametersTable.astro +97 -0
- package/src/components/openapi/RequestBody.astro +58 -0
- package/src/components/openapi/RequestPanel.astro +169 -0
- package/src/components/openapi/Responses.astro +91 -0
- package/src/components/openapi/SchemaProperty.astro +118 -0
- package/src/components/openapi/SchemaTable.astro +86 -0
- package/src/components/openapi/helpers.ts +238 -0
- package/src/components/openapi/panel.ts +59 -0
- package/src/components/openapi/snippets.ts +201 -0
- package/src/core/builtin-tags.ts +5 -0
- package/src/core/data.ts +2 -0
- package/src/core/graph.ts +0 -3
- package/src/core/nav-diagnostics.ts +2 -12
- package/src/core/navigation.ts +0 -10
- package/src/core/project-graph.ts +5 -1
- package/src/core/project.ts +25 -3
- package/src/core/schema.ts +47 -14
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/resolve.ts +28 -6
- package/src/core/types.ts +7 -7
- package/src/migrate/mintlify/config.ts +190 -97
- package/src/migrate/mintlify/content.ts +24 -2
- package/src/migrate/mintlify/index.ts +76 -2
- package/src/migrate/mintlify/transform.ts +2 -0
- package/src/openapi/model.ts +174 -0
- package/src/openapi/parse.ts +48 -0
- package/src/openapi/references.ts +164 -0
- package/src/openapi/render-mdx.ts +76 -0
- package/src/openapi/scalar.ts +15 -103
- package/src/openapi/source.ts +140 -0
- package/src/registry/eject.ts +15 -2
- package/src/theme/chrome-icons.ts +22 -0
- 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
|
+
}
|