radiant-docs 0.1.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.
- package/dist/index.js +312 -0
- package/package.json +38 -0
- package/template/.vscode/extensions.json +4 -0
- package/template/.vscode/launch.json +11 -0
- package/template/astro.config.mjs +216 -0
- package/template/ec.config.mjs +51 -0
- package/template/package-lock.json +12546 -0
- package/template/package.json +51 -0
- package/template/public/favicon.svg +9 -0
- package/template/src/assets/icons/check.svg +33 -0
- package/template/src/assets/icons/danger.svg +37 -0
- package/template/src/assets/icons/info.svg +36 -0
- package/template/src/assets/icons/lightbulb.svg +74 -0
- package/template/src/assets/icons/warning.svg +37 -0
- package/template/src/components/Header.astro +176 -0
- package/template/src/components/MdxPage.astro +49 -0
- package/template/src/components/OpenApiPage.astro +270 -0
- package/template/src/components/Search.astro +362 -0
- package/template/src/components/Sidebar.astro +19 -0
- package/template/src/components/SidebarDropdown.astro +149 -0
- package/template/src/components/SidebarGroup.astro +51 -0
- package/template/src/components/SidebarLink.astro +56 -0
- package/template/src/components/SidebarMenu.astro +46 -0
- package/template/src/components/SidebarSubgroup.astro +136 -0
- package/template/src/components/TableOfContents.astro +480 -0
- package/template/src/components/ThemeSwitcher.astro +84 -0
- package/template/src/components/endpoint/PlaygroundBar.astro +68 -0
- package/template/src/components/endpoint/PlaygroundButton.astro +44 -0
- package/template/src/components/endpoint/PlaygroundField.astro +54 -0
- package/template/src/components/endpoint/PlaygroundForm.astro +203 -0
- package/template/src/components/endpoint/RequestSnippets.astro +308 -0
- package/template/src/components/endpoint/ResponseDisplay.astro +177 -0
- package/template/src/components/endpoint/ResponseFields.astro +224 -0
- package/template/src/components/endpoint/ResponseSnippets.astro +247 -0
- package/template/src/components/sidebar/SidebarEndpointLink.astro +51 -0
- package/template/src/components/sidebar/SidebarOpenApi.astro +207 -0
- package/template/src/components/ui/Field.astro +69 -0
- package/template/src/components/ui/Tag.astro +5 -0
- package/template/src/components/ui/demo/CodeDemo.astro +15 -0
- package/template/src/components/ui/demo/Demo.astro +3 -0
- package/template/src/components/ui/demo/UiDisplay.astro +13 -0
- package/template/src/components/user/Accordian.astro +69 -0
- package/template/src/components/user/AccordianGroup.astro +13 -0
- package/template/src/components/user/Callout.astro +101 -0
- package/template/src/components/user/Step.astro +51 -0
- package/template/src/components/user/Steps.astro +9 -0
- package/template/src/components/user/Tab.astro +25 -0
- package/template/src/components/user/Tabs.astro +122 -0
- package/template/src/content.config.ts +11 -0
- package/template/src/entrypoint.ts +9 -0
- package/template/src/layouts/Layout.astro +92 -0
- package/template/src/lib/component-error.ts +163 -0
- package/template/src/lib/frontmatter-schema.ts +9 -0
- package/template/src/lib/oas.ts +24 -0
- package/template/src/lib/pagefind.ts +88 -0
- package/template/src/lib/routes.ts +316 -0
- package/template/src/lib/utils.ts +59 -0
- package/template/src/lib/validation.ts +1097 -0
- package/template/src/pages/[...slug].astro +77 -0
- package/template/src/styles/global.css +209 -0
- package/template/tsconfig.json +5 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from "astro-icon/components";
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<div
|
|
6
|
+
x-data={`{
|
|
7
|
+
selected: 'body',
|
|
8
|
+
select(value) {
|
|
9
|
+
this.selected = value;
|
|
10
|
+
this.$refs.button?.focus();
|
|
11
|
+
this.close();
|
|
12
|
+
},
|
|
13
|
+
close(focusAfter) {
|
|
14
|
+
if (!this.open) return;
|
|
15
|
+
this.open = false;
|
|
16
|
+
focusAfter?.focus();
|
|
17
|
+
}
|
|
18
|
+
}`}
|
|
19
|
+
class="bg-neutral-100 rounded-[14px] border border-neutral-200 p-[3px] inset-shadow-xs"
|
|
20
|
+
x-bind:class="loading && 'animate-loading'"
|
|
21
|
+
>
|
|
22
|
+
<div class="flex justify-between items-center">
|
|
23
|
+
<div
|
|
24
|
+
class="flex-1 min-w-0 flex items-center font-medium ml-2 text-neutral-700 text-sm"
|
|
25
|
+
>
|
|
26
|
+
<span
|
|
27
|
+
class:list={[
|
|
28
|
+
"shrink-0 text-xs uppercase font-semibold px-1.5 border py-px rounded-md mr-1.5",
|
|
29
|
+
]}
|
|
30
|
+
x-bind:class="{
|
|
31
|
+
'bg-green-50 text-green-700/70 border-green-700/10': Math.floor(response?.status / 100) === 2,
|
|
32
|
+
'bg-blue-50 text-blue-700/70 border-blue-700/10': Math.floor(response?.status / 100) === 3,
|
|
33
|
+
'bg-amber-50 text-amber-600/80 border-amber-700/10': Math.floor(response?.status / 100) === 4,
|
|
34
|
+
'bg-red-50 text-red-700/70 border-red-700/10': Math.floor(response?.status / 100) === 5
|
|
35
|
+
}"
|
|
36
|
+
x-text="response?.status"
|
|
37
|
+
x-show="response?.status"></span>
|
|
38
|
+
<span class="truncate" x-text="response?.statusText"></span>
|
|
39
|
+
</div>
|
|
40
|
+
<div
|
|
41
|
+
x-data="{
|
|
42
|
+
open: false,
|
|
43
|
+
toggle() {
|
|
44
|
+
if (this.open) {
|
|
45
|
+
return this.close()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.$refs.button.focus()
|
|
49
|
+
|
|
50
|
+
this.open = true
|
|
51
|
+
},
|
|
52
|
+
close(focusAfter) {
|
|
53
|
+
if (! this.open) return
|
|
54
|
+
|
|
55
|
+
this.open = false
|
|
56
|
+
|
|
57
|
+
focusAfter && focusAfter.focus()
|
|
58
|
+
}
|
|
59
|
+
}"
|
|
60
|
+
x-on:keydown.escape.prevent.stop="close($refs.button)"
|
|
61
|
+
x-on:focusin.window="! $refs.panel.contains($event.target) && close()"
|
|
62
|
+
x-id="['dropdown-button']"
|
|
63
|
+
class="shrink-0 relative max-w-24 w-full"
|
|
64
|
+
>
|
|
65
|
+
<button
|
|
66
|
+
x-ref="button"
|
|
67
|
+
x-on:click="toggle()"
|
|
68
|
+
:aria-expanded="open"
|
|
69
|
+
:aria-controls="$id('dropdown-button')"
|
|
70
|
+
type="button"
|
|
71
|
+
class="flex items-center justify-between px-3 pt-2 pb-1.5 relative border-x border-t border-neutral-200 rounded-t-xl w-full text-sm font-medium bg-white shadow-xs cursor-pointer after:absolute after:-bottom-[2px] after:inset-x-0 after:z-50 after:h-[3px] after:bg-white"
|
|
72
|
+
>
|
|
73
|
+
<span class="flex items-center gap-2">
|
|
74
|
+
<span x-text="selected" class="capitalize"></span>
|
|
75
|
+
</span>
|
|
76
|
+
<Icon name="lucide:chevrons-up-down" />
|
|
77
|
+
</button>
|
|
78
|
+
<!-- Panel -->
|
|
79
|
+
<ul
|
|
80
|
+
x-ref="panel"
|
|
81
|
+
x-show="open"
|
|
82
|
+
x-transition.origin.top.left
|
|
83
|
+
x-on:click.outside="close($refs.button)"
|
|
84
|
+
:id="$id('dropdown-button')"
|
|
85
|
+
x-cloak
|
|
86
|
+
role="menu"
|
|
87
|
+
class="absolute top-full right-2 min-w-28 rounded-lg shadow-xl mt-2 z-10 origin-top-right bg-white py-0.5 outline-none border border-neutral-200"
|
|
88
|
+
>
|
|
89
|
+
<li role="menuitem">
|
|
90
|
+
<button
|
|
91
|
+
x-on:click="select('header')"
|
|
92
|
+
type="button"
|
|
93
|
+
class="w-full text-left px-3 py-2 text-sm text-neutral-700 rounded-md transition-colors flex items-center gap-2 relative before:absolute before:inset-x-1 before:inset-y-0.5 before:-z-10 before:rounded-md before:duration-150 cursor-pointer"
|
|
94
|
+
x-bind:class="selected === 'header' ? 'before:bg-neutral-200/50 text-neutral-900' : 'hover:before:bg-neutral-100/70'"
|
|
95
|
+
>
|
|
96
|
+
Header
|
|
97
|
+
</button>
|
|
98
|
+
</li>
|
|
99
|
+
<li role="menuitem">
|
|
100
|
+
<button
|
|
101
|
+
x-on:click="select('body')"
|
|
102
|
+
type="button"
|
|
103
|
+
class="w-full text-left px-3 py-2 text-sm text-neutral-700 rounded-md transition-colors flex items-center gap-2 relative before:absolute before:inset-x-1 before:inset-y-0.5 before:-z-10 before:rounded-md before:duration-150 cursor-pointer"
|
|
104
|
+
x-bind:class="selected === 'body' ? 'before:bg-neutral-200/50 text-neutral-900' : 'hover:before:bg-neutral-100/70'"
|
|
105
|
+
>
|
|
106
|
+
Body
|
|
107
|
+
</button>
|
|
108
|
+
</li>
|
|
109
|
+
</ul>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div
|
|
113
|
+
class="bg-white border border-neutral-200 rounded-xl rounded-tr-none shadow-xs transition duration-300"
|
|
114
|
+
>
|
|
115
|
+
<div x-bind:class="loading && 'opacity-60 duration-700'">
|
|
116
|
+
<div x-show="selected === 'body'">
|
|
117
|
+
<div
|
|
118
|
+
x-show="!response || !response.highlightedData"
|
|
119
|
+
class="text-sm px-4 py-4 xs:py-8 flex flex-col items-center justify-center"
|
|
120
|
+
>
|
|
121
|
+
<div class="bg-neutral-50 p-2 rounded-xl mb-1">
|
|
122
|
+
<Icon
|
|
123
|
+
class="size-6 text-neutral-300"
|
|
124
|
+
name="lucide:square-arrow-up-right"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="text-lg text-center text-neutral-700">Send request</div>
|
|
128
|
+
<p class="text-sm text-neutral-500 text-center">
|
|
129
|
+
Send a request to see the response body.
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
<div x-show="response && response.highlightedData">
|
|
133
|
+
<pre
|
|
134
|
+
class="bg-transparent! m-0!"><code class="language-json text-[13px]! font-mono! text-neutral-700!" x-html="response?.highlightedData" /></pre>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<div x-show="selected === 'header'">
|
|
138
|
+
<div
|
|
139
|
+
x-show="!response || !response.headers || Object.keys(response.headers).length === 0"
|
|
140
|
+
class="text-sm px-4 py-4 xs:py-8 flex flex-col items-center justify-center"
|
|
141
|
+
>
|
|
142
|
+
<div class="bg-neutral-50 p-2 rounded-xl mb-1">
|
|
143
|
+
<Icon
|
|
144
|
+
class="size-6 text-neutral-300"
|
|
145
|
+
name="lucide:square-arrow-up-right"
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="text-lg text-center text-neutral-700">Send request</div>
|
|
149
|
+
<p class="text-sm text-neutral-500 text-center">
|
|
150
|
+
Send a request to see the response header.
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
<div
|
|
154
|
+
x-show="response && response.headers && Object.keys(response.headers).length > 0"
|
|
155
|
+
class=""
|
|
156
|
+
>
|
|
157
|
+
<table class="w-full border-collapse text-xs table-fixed">
|
|
158
|
+
<tbody>
|
|
159
|
+
<template x-for="(value, key) in response?.headers" :key="key">
|
|
160
|
+
<tr
|
|
161
|
+
class="flex border-b last:border-b-0 divide-x divide-neutral-100 border-neutral-100 hover:bg-neutral-50/50"
|
|
162
|
+
>
|
|
163
|
+
<td
|
|
164
|
+
class="py-2 px-3 basis-1/2 font-mono font-medium text-neutral-700 overflow-x-auto whitespace-nowrap"
|
|
165
|
+
x-text="key"></td>
|
|
166
|
+
<td
|
|
167
|
+
class="py-2 px-3 basis-1/2 font-mono text-neutral-600 overflow-x-auto whitespace-nowrap"
|
|
168
|
+
x-text="value"></td>
|
|
169
|
+
</tr>
|
|
170
|
+
</template>
|
|
171
|
+
</tbody>
|
|
172
|
+
</table>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
|
|
3
|
+
import Field from "../ui/Field.astro";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
responses: OpenAPIV3.ResponsesObject | OpenAPIV3_1.ResponsesObject;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { responses } = Astro.props;
|
|
10
|
+
|
|
11
|
+
interface ResponseField {
|
|
12
|
+
name: string;
|
|
13
|
+
required: boolean;
|
|
14
|
+
type: string;
|
|
15
|
+
description: string;
|
|
16
|
+
nested?: ResponseField[]; // For nested objects
|
|
17
|
+
enum?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ResponseData {
|
|
21
|
+
statusCode: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
fields: ResponseField[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Helper function to extract fields from a schema recursively
|
|
27
|
+
function extractFieldsFromSchema(
|
|
28
|
+
schema: any,
|
|
29
|
+
parentName: string = "",
|
|
30
|
+
required: string[] = []
|
|
31
|
+
): ResponseField[] {
|
|
32
|
+
if (!schema) return [];
|
|
33
|
+
|
|
34
|
+
const fields: ResponseField[] = [];
|
|
35
|
+
|
|
36
|
+
// Handle allOf - merge properties and required arrays
|
|
37
|
+
let properties = schema.properties || {};
|
|
38
|
+
let schemaRequired = schema.required || [];
|
|
39
|
+
|
|
40
|
+
if (schema.allOf) {
|
|
41
|
+
schema.allOf.forEach((s: any) => {
|
|
42
|
+
if (s.properties) {
|
|
43
|
+
properties = { ...properties, ...s.properties };
|
|
44
|
+
}
|
|
45
|
+
if (s.required && Array.isArray(s.required)) {
|
|
46
|
+
schemaRequired = [...new Set([...schemaRequired, ...s.required])];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle array types - extract fields from items
|
|
52
|
+
if (schema.type === "array" && schema.items) {
|
|
53
|
+
const itemFields = extractFieldsFromSchema(
|
|
54
|
+
schema.items,
|
|
55
|
+
"",
|
|
56
|
+
schema.items.required || []
|
|
57
|
+
);
|
|
58
|
+
if (itemFields.length > 0) {
|
|
59
|
+
fields.push({
|
|
60
|
+
name: parentName || "items",
|
|
61
|
+
required: required.includes(parentName) || false,
|
|
62
|
+
type: "array",
|
|
63
|
+
description: schema.description || "",
|
|
64
|
+
nested: itemFields,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return fields;
|
|
68
|
+
}
|
|
69
|
+
// Extract object properties
|
|
70
|
+
if (properties && Object.keys(properties).length > 0) {
|
|
71
|
+
Object.entries(properties)
|
|
72
|
+
.sort(([nameA], [nameB]) => {
|
|
73
|
+
const aRequired = schemaRequired.includes(nameA);
|
|
74
|
+
const bRequired = schemaRequired.includes(nameB);
|
|
75
|
+
if (aRequired && !bRequired) return -1;
|
|
76
|
+
if (!aRequired && bRequired) return 1;
|
|
77
|
+
return 0;
|
|
78
|
+
})
|
|
79
|
+
.forEach(([name, propSchema]: [string, any]) => {
|
|
80
|
+
const fullName = parentName ? `${parentName}.${name}` : name;
|
|
81
|
+
const isRequired = schemaRequired.includes(name);
|
|
82
|
+
|
|
83
|
+
// Determine type
|
|
84
|
+
let type = propSchema.type || "object";
|
|
85
|
+
if (propSchema.format) {
|
|
86
|
+
type = `${type} (${propSchema.format})`;
|
|
87
|
+
} else if (propSchema.enum) {
|
|
88
|
+
type = `enum<${propSchema.type || "string"}>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Handle nested objects
|
|
92
|
+
if (
|
|
93
|
+
propSchema.type === "object" ||
|
|
94
|
+
propSchema.properties ||
|
|
95
|
+
propSchema.allOf
|
|
96
|
+
) {
|
|
97
|
+
const nestedFields = extractFieldsFromSchema(
|
|
98
|
+
propSchema,
|
|
99
|
+
fullName,
|
|
100
|
+
propSchema.required || []
|
|
101
|
+
);
|
|
102
|
+
fields.push({
|
|
103
|
+
name: fullName,
|
|
104
|
+
required: isRequired,
|
|
105
|
+
type: type,
|
|
106
|
+
description: propSchema.description || "",
|
|
107
|
+
nested: nestedFields.length > 0 ? nestedFields : undefined,
|
|
108
|
+
enum: propSchema.enum,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Handle arrays
|
|
112
|
+
else if (propSchema.type === "array" && propSchema.items) {
|
|
113
|
+
const itemFields = extractFieldsFromSchema(
|
|
114
|
+
propSchema.items,
|
|
115
|
+
`${fullName}[]`,
|
|
116
|
+
propSchema.items.required || []
|
|
117
|
+
);
|
|
118
|
+
fields.push({
|
|
119
|
+
name: fullName,
|
|
120
|
+
required: isRequired,
|
|
121
|
+
type: propSchema.items?.enum
|
|
122
|
+
? `enum<${propSchema.items?.type || "object"}>[]`
|
|
123
|
+
: `${propSchema.items.type || "object"}[]`,
|
|
124
|
+
description: propSchema.description || "",
|
|
125
|
+
nested: itemFields.length > 0 ? itemFields : undefined,
|
|
126
|
+
enum: propSchema.items?.enum,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Handle primitive types
|
|
130
|
+
else {
|
|
131
|
+
fields.push({
|
|
132
|
+
name: fullName,
|
|
133
|
+
required: isRequired,
|
|
134
|
+
type: type,
|
|
135
|
+
description: propSchema.description || "",
|
|
136
|
+
enum: propSchema.enum,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return fields;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Process all responses
|
|
146
|
+
const responseFields: ResponseData[] = [];
|
|
147
|
+
|
|
148
|
+
Object.entries(responses)
|
|
149
|
+
.sort(([codeA], [codeB]) => {
|
|
150
|
+
// Sort by status code: 2xx, 3xx, 4xx, 5xx, then by numeric value
|
|
151
|
+
const numA = parseInt(codeA) || 0;
|
|
152
|
+
const numB = parseInt(codeB) || 0;
|
|
153
|
+
const categoryA = Math.floor(numA / 100);
|
|
154
|
+
const categoryB = Math.floor(numB / 100);
|
|
155
|
+
|
|
156
|
+
if (categoryA !== categoryB) {
|
|
157
|
+
return categoryA - categoryB;
|
|
158
|
+
}
|
|
159
|
+
return numA - numB;
|
|
160
|
+
})
|
|
161
|
+
.forEach(([statusCode, response]: [string, any]) => {
|
|
162
|
+
// Try to get application/json content first
|
|
163
|
+
const contentType = response.content?.["application/json"]
|
|
164
|
+
? "application/json"
|
|
165
|
+
: Object.keys(response.content || {})[0];
|
|
166
|
+
|
|
167
|
+
const mediaType = contentType ? response.content?.[contentType] : null;
|
|
168
|
+
const schema = mediaType?.schema;
|
|
169
|
+
|
|
170
|
+
const fields: ResponseField[] = schema
|
|
171
|
+
? extractFieldsFromSchema(schema, "", schema.required || [])
|
|
172
|
+
: [];
|
|
173
|
+
|
|
174
|
+
responseFields.push({
|
|
175
|
+
statusCode,
|
|
176
|
+
description: response.description,
|
|
177
|
+
fields,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
<div>
|
|
183
|
+
<h4 class="text-xl font-semibold mt-10">Responses</h4>
|
|
184
|
+
{
|
|
185
|
+
responseFields.map((response) => (
|
|
186
|
+
<div class="mt-6">
|
|
187
|
+
<div class="flex items-center gap-2 mb-4">
|
|
188
|
+
<h5 class="text-lg font-medium">{response.statusCode}</h5>
|
|
189
|
+
{response.description && (
|
|
190
|
+
<span class="text-sm text-neutral-600">{response.description}</span>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
{response.fields.length > 0 ? (
|
|
194
|
+
<div class="ml-2 space-y-4">
|
|
195
|
+
{response.fields.map((field) => (
|
|
196
|
+
<div>
|
|
197
|
+
<Field
|
|
198
|
+
name={field.name}
|
|
199
|
+
type={field.type}
|
|
200
|
+
optional={!field.required}
|
|
201
|
+
description={field.description}
|
|
202
|
+
/>
|
|
203
|
+
{field.nested && field.nested.length > 0 && (
|
|
204
|
+
<div class="ml-6 mt-2 space-y-2">
|
|
205
|
+
{field.nested.map((nestedField) => (
|
|
206
|
+
<Field
|
|
207
|
+
name={nestedField.name}
|
|
208
|
+
type={nestedField.type}
|
|
209
|
+
optional={!nestedField.required}
|
|
210
|
+
description={nestedField.description}
|
|
211
|
+
/>
|
|
212
|
+
))}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
) : (
|
|
219
|
+
<p class="text-sm text-neutral-500 ml-2">No response body fields</p>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
))
|
|
223
|
+
}
|
|
224
|
+
</div>
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
import * as Sampler from "openapi-sampler";
|
|
3
|
+
import type { JSONSchema7 } from "json-schema";
|
|
4
|
+
import type { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
|
|
5
|
+
import { Icon } from "astro-icon/components";
|
|
6
|
+
import { Code } from "astro-expressive-code/components";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
responses: OpenAPIV3.ResponsesObject | OpenAPIV3_1.ResponsesObject;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { responses } = Astro.props;
|
|
13
|
+
|
|
14
|
+
// Extract and sample response data
|
|
15
|
+
const responseData: Array<{
|
|
16
|
+
statusCode: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
example?: any;
|
|
19
|
+
contentType?: string;
|
|
20
|
+
}> = [];
|
|
21
|
+
|
|
22
|
+
Object.entries(responses).forEach(([statusCode, response]: [string, any]) => {
|
|
23
|
+
// Try to get application/json content first, fallback to first available content type
|
|
24
|
+
const contentTypes = Object.keys(response.content || {});
|
|
25
|
+
const contentType = response.content?.["application/json"]
|
|
26
|
+
? "application/json"
|
|
27
|
+
: contentTypes[0];
|
|
28
|
+
|
|
29
|
+
const mediaType = contentType ? response.content?.[contentType] : null;
|
|
30
|
+
const schema = mediaType?.schema;
|
|
31
|
+
|
|
32
|
+
let example: any = undefined;
|
|
33
|
+
if (schema) {
|
|
34
|
+
try {
|
|
35
|
+
example = Sampler.sample(schema as JSONSchema7, {
|
|
36
|
+
skipReadOnly: false,
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(`Failed to sample response ${statusCode}:`, err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
responseData.push({
|
|
44
|
+
statusCode,
|
|
45
|
+
description: response.description,
|
|
46
|
+
example,
|
|
47
|
+
contentType,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const statusCodeStyles = {
|
|
52
|
+
"2": {
|
|
53
|
+
badge: "bg-green-50 text-green-700/70 border-green-700/10",
|
|
54
|
+
dot: "bg-green-700/70",
|
|
55
|
+
},
|
|
56
|
+
"3": {
|
|
57
|
+
badge: "bg-blue-50 text-blue-700/70 border-blue-700/10",
|
|
58
|
+
dot: "bg-blue-700/70",
|
|
59
|
+
},
|
|
60
|
+
"4": {
|
|
61
|
+
badge: "bg-amber-50 text-amber-600/80 border-amber-700/10",
|
|
62
|
+
dot: "bg-amber-600/80",
|
|
63
|
+
},
|
|
64
|
+
"5": {
|
|
65
|
+
badge: "bg-red-50 text-red-700/70 border-red-700/10",
|
|
66
|
+
dot: "bg-red-700/70",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getStatusCodeClasses = (statusCode: string, type: "badge" | "dot") => {
|
|
71
|
+
const firstChar = statusCode[0];
|
|
72
|
+
return (
|
|
73
|
+
statusCodeStyles[firstChar as keyof typeof statusCodeStyles]?.[type] || ""
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const initialStatusCodeClasses = responseData[0]
|
|
78
|
+
? getStatusCodeClasses(responseData[0].statusCode, "badge")
|
|
79
|
+
: "";
|
|
80
|
+
const initialStatusCodeDotClasses = responseData[0]
|
|
81
|
+
? getStatusCodeClasses(responseData[0].statusCode, "dot")
|
|
82
|
+
: "";
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
<div
|
|
86
|
+
x-data={`{
|
|
87
|
+
responses: ${JSON.stringify(responseData)},
|
|
88
|
+
selected: 0,
|
|
89
|
+
select(index) {
|
|
90
|
+
this.selected = index;
|
|
91
|
+
this.$refs.button?.focus();
|
|
92
|
+
this.close();
|
|
93
|
+
},
|
|
94
|
+
close(focusAfter) {
|
|
95
|
+
if (!this.open) return;
|
|
96
|
+
this.open = false;
|
|
97
|
+
focusAfter?.focus();
|
|
98
|
+
}
|
|
99
|
+
}`}
|
|
100
|
+
class="response-code-snippets bg-neutral-100 rounded-[14px] border border-neutral-200 shadow-xs. p-[3px] shadow-[inset_0px_0.5px_2px_2px_rgba(0,0,0,0.01)]. inset-shadow-xs"
|
|
101
|
+
>
|
|
102
|
+
<div class="flex justify-between items-center">
|
|
103
|
+
<div
|
|
104
|
+
class="flex-1 min-w-0 flex items-center font-medium ml-2 text-neutral-700 text-sm"
|
|
105
|
+
>
|
|
106
|
+
<span
|
|
107
|
+
class:list={[
|
|
108
|
+
"shrink-0 text-xs uppercase font-semibold px-1.5 border py-px rounded-md mr-1.5",
|
|
109
|
+
initialStatusCodeClasses,
|
|
110
|
+
]}
|
|
111
|
+
x-bind:class="{
|
|
112
|
+
'bg-green-50 text-green-700/70 border-green-700/10': responses[selected].statusCode[0] === '2',
|
|
113
|
+
'bg-blue-50 text-blue-700/70 border-blue-700/10': responses[selected].statusCode[0] === '3',
|
|
114
|
+
'bg-amber-50 text-amber-600/80 border-amber-700/10': responses[selected].statusCode[0] === '4',
|
|
115
|
+
'bg-red-50 text-red-700/70 border-red-700/10': responses[selected].statusCode[0] === '5'
|
|
116
|
+
}"
|
|
117
|
+
x-text="responses[selected].statusCode"
|
|
118
|
+
set:html={responseData[0].statusCode}
|
|
119
|
+
/>
|
|
120
|
+
<span
|
|
121
|
+
class="truncate"
|
|
122
|
+
x-text="responses[selected].description"
|
|
123
|
+
set:html={responseData[0].description}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
<div
|
|
127
|
+
x-data="{
|
|
128
|
+
open: false,
|
|
129
|
+
toggle() {
|
|
130
|
+
if (this.open) {
|
|
131
|
+
return this.close()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.$refs.button.focus()
|
|
135
|
+
|
|
136
|
+
this.open = true
|
|
137
|
+
},
|
|
138
|
+
close(focusAfter) {
|
|
139
|
+
if (! this.open) return
|
|
140
|
+
|
|
141
|
+
this.open = false
|
|
142
|
+
|
|
143
|
+
focusAfter && focusAfter.focus()
|
|
144
|
+
}
|
|
145
|
+
}"
|
|
146
|
+
x-on:keydown.escape.prevent.stop="close($refs.button)"
|
|
147
|
+
x-on:focusin.window="! $refs.panel.contains($event.target) && close()"
|
|
148
|
+
x-id="['dropdown-button']"
|
|
149
|
+
class="shrink-0 relative max-w-24 w-full"
|
|
150
|
+
>
|
|
151
|
+
<button
|
|
152
|
+
x-ref="button"
|
|
153
|
+
x-on:click="toggle()"
|
|
154
|
+
:aria-expanded="open"
|
|
155
|
+
:aria-controls="$id('dropdown-button')"
|
|
156
|
+
type="button"
|
|
157
|
+
class="flex items-center justify-between px-3 pt-2 pb-1.5 relative border-x border-t border-neutral-200/70 rounded-t-xl w-full text-sm font-medium bg-white shadow-[-1px_0px_2px_0px_rgba(0,0,0,0.01)]. shadow-sm cursor-pointer after:absolute after:-bottom-[1.5px] after:inset-x-0 after:z-10 after:h-[3px] after:bg-white"
|
|
158
|
+
>
|
|
159
|
+
<span class="flex items-center gap-2">
|
|
160
|
+
<span
|
|
161
|
+
class:list={["size-1.5 rounded-full", initialStatusCodeDotClasses]}
|
|
162
|
+
x-bind:class="{
|
|
163
|
+
'bg-green-700/70': responses[selected].statusCode[0] === '2',
|
|
164
|
+
'bg-blue-700/70': responses[selected].statusCode[0] === '3',
|
|
165
|
+
'bg-amber-600/80': responses[selected].statusCode[0] === '4',
|
|
166
|
+
'bg-red-700/70': responses[selected].statusCode[0] === '5'
|
|
167
|
+
}"
|
|
168
|
+
></span>
|
|
169
|
+
<span
|
|
170
|
+
x-text="responses[selected].statusCode"
|
|
171
|
+
set:html={responseData[0].statusCode}
|
|
172
|
+
/>
|
|
173
|
+
</span>
|
|
174
|
+
<Icon name="lucide:chevrons-up-down" class="ml-auto." />
|
|
175
|
+
</button>
|
|
176
|
+
<!-- Panel -->
|
|
177
|
+
<ul
|
|
178
|
+
x-ref="panel"
|
|
179
|
+
x-show="open"
|
|
180
|
+
x-transition.origin.top.left
|
|
181
|
+
x-on:click.outside="close($refs.button)"
|
|
182
|
+
:id="$id('dropdown-button')"
|
|
183
|
+
x-cloak
|
|
184
|
+
role="menu"
|
|
185
|
+
class="absolute top-full right-2 min-w-28 rounded-lg shadow-xl mt-2 z-10 origin-top-right bg-white py-0.5 outline-none border border-neutral-200"
|
|
186
|
+
>
|
|
187
|
+
<template
|
|
188
|
+
x-for="(response, index) in responses"
|
|
189
|
+
:key="response.statusCode"
|
|
190
|
+
>
|
|
191
|
+
<li role="menuitem">
|
|
192
|
+
<button
|
|
193
|
+
x-on:click="select(index)"
|
|
194
|
+
type="button"
|
|
195
|
+
class="w-full text-left px-3 py-2 text-sm text-neutral-700 rounded-md transition-colors flex items-center gap-2 relative before:absolute before:inset-x-1 before:inset-y-0.5 before:-z-10 before:rounded-md before:duration-150 cursor-pointer"
|
|
196
|
+
x-bind:class="index === selected ? 'before:bg-neutral-200/50 text-neutral-900' : 'hover:before:bg-neutral-100/70'"
|
|
197
|
+
>
|
|
198
|
+
<span
|
|
199
|
+
class="size-1.5 rounded-full"
|
|
200
|
+
x-bind:class="{
|
|
201
|
+
'bg-green-700/70': response.statusCode[0] === '2',
|
|
202
|
+
'bg-blue-700/70': response.statusCode[0] === '3',
|
|
203
|
+
'bg-amber-600/80': response.statusCode[0] === '4',
|
|
204
|
+
'bg-red-700/70': response.statusCode[0] === '5'
|
|
205
|
+
}"
|
|
206
|
+
>
|
|
207
|
+
</span>
|
|
208
|
+
<span x-text="response.statusCode"></span>
|
|
209
|
+
</button>
|
|
210
|
+
</li>
|
|
211
|
+
</template>
|
|
212
|
+
</ul>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
{
|
|
216
|
+
responseData.map((response, index) => (
|
|
217
|
+
<div
|
|
218
|
+
{...(index !== 0 ? { "x-cloak": true } : {})}
|
|
219
|
+
x-show={`selected === ${index}`}
|
|
220
|
+
data-snippet-index={index}
|
|
221
|
+
>
|
|
222
|
+
<Code
|
|
223
|
+
code={
|
|
224
|
+
JSON.stringify(response.example, null, 2) ||
|
|
225
|
+
"This response has no body data."
|
|
226
|
+
}
|
|
227
|
+
lang="json"
|
|
228
|
+
frame="code"
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
))
|
|
232
|
+
}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<style>
|
|
236
|
+
@reference "../../styles/global.css";
|
|
237
|
+
:global(.response-code-snippets .expressive-code .frame) {
|
|
238
|
+
@apply shadow-sm rounded-2xl!;
|
|
239
|
+
}
|
|
240
|
+
:global(.response-code-snippets .expressive-code pre) {
|
|
241
|
+
@apply rounded-xl! rounded-tr-none! border-neutral-200/70!;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
:global(.response-code-snippets .expressive-code pre code) {
|
|
245
|
+
@apply max-h-64 overflow-y-auto;
|
|
246
|
+
}
|
|
247
|
+
</style>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { slugify } from "../../lib/utils";
|
|
3
|
+
import { methodColors } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
method: string;
|
|
7
|
+
path: string;
|
|
8
|
+
summary?: string;
|
|
9
|
+
parentSlug?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { method, path: pathStr, summary, parentSlug = "" } = Astro.props;
|
|
13
|
+
|
|
14
|
+
// Generate slug from path and method (matching routes.ts logic)
|
|
15
|
+
const pathSlug = pathStr
|
|
16
|
+
.replace(/^\//, "") // Remove leading slash
|
|
17
|
+
.replace(/\//g, "-") // Replace slashes with hyphens
|
|
18
|
+
.replace(/\{[^}]+\}/g, "param"); // Replace path params like {id} with "param"
|
|
19
|
+
|
|
20
|
+
const methodSlug = slugify(method);
|
|
21
|
+
const endpointSlug = pathSlug ? `${pathSlug}-${methodSlug}` : methodSlug;
|
|
22
|
+
const href = parentSlug ? `/${parentSlug}/${endpointSlug}` : `/${endpointSlug}`;
|
|
23
|
+
|
|
24
|
+
// Use summary for title, fallback to method + path
|
|
25
|
+
const text = summary || pathStr;
|
|
26
|
+
|
|
27
|
+
// Normalize paths for comparison (remove trailing slashes)
|
|
28
|
+
const currentPath = Astro.url.pathname.replace(/\/$/, "");
|
|
29
|
+
const targetPath = href.replace(/\/$/, "");
|
|
30
|
+
const isActive = currentPath === targetPath;
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<a
|
|
34
|
+
href={href}
|
|
35
|
+
class:list={[
|
|
36
|
+
"flex items-center px-2 py-[7px] text-sm relative z-0 font-[450] before:-z-10 before:absolute before:inset-x-0 before:inset-y-px before:rounded-md before:duration-150",
|
|
37
|
+
isActive
|
|
38
|
+
? "before:bg-neutral-200/50 dark:before:bg-neutral-800 text-neutral-900 dark:text-neutral-200"
|
|
39
|
+
: "text-neutral-600 dark:text-neutral-400 hover:before:bg-neutral-100/70 dark:hover:before:bg-neutral-800/50 hover:text-neutral-900 dark:hover:text-neutral-300",
|
|
40
|
+
]}
|
|
41
|
+
>
|
|
42
|
+
<span
|
|
43
|
+
class:list={[
|
|
44
|
+
"px-1 py-px mr-1.5 border rounded-md text-[10px] font-semibold uppercase",
|
|
45
|
+
methodColors[method],
|
|
46
|
+
]}
|
|
47
|
+
>
|
|
48
|
+
{method.toLowerCase() !== "delete" ? method : "del"}
|
|
49
|
+
</span>
|
|
50
|
+
{text}
|
|
51
|
+
</a>
|