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.
Files changed (61) hide show
  1. package/dist/index.js +312 -0
  2. package/package.json +38 -0
  3. package/template/.vscode/extensions.json +4 -0
  4. package/template/.vscode/launch.json +11 -0
  5. package/template/astro.config.mjs +216 -0
  6. package/template/ec.config.mjs +51 -0
  7. package/template/package-lock.json +12546 -0
  8. package/template/package.json +51 -0
  9. package/template/public/favicon.svg +9 -0
  10. package/template/src/assets/icons/check.svg +33 -0
  11. package/template/src/assets/icons/danger.svg +37 -0
  12. package/template/src/assets/icons/info.svg +36 -0
  13. package/template/src/assets/icons/lightbulb.svg +74 -0
  14. package/template/src/assets/icons/warning.svg +37 -0
  15. package/template/src/components/Header.astro +176 -0
  16. package/template/src/components/MdxPage.astro +49 -0
  17. package/template/src/components/OpenApiPage.astro +270 -0
  18. package/template/src/components/Search.astro +362 -0
  19. package/template/src/components/Sidebar.astro +19 -0
  20. package/template/src/components/SidebarDropdown.astro +149 -0
  21. package/template/src/components/SidebarGroup.astro +51 -0
  22. package/template/src/components/SidebarLink.astro +56 -0
  23. package/template/src/components/SidebarMenu.astro +46 -0
  24. package/template/src/components/SidebarSubgroup.astro +136 -0
  25. package/template/src/components/TableOfContents.astro +480 -0
  26. package/template/src/components/ThemeSwitcher.astro +84 -0
  27. package/template/src/components/endpoint/PlaygroundBar.astro +68 -0
  28. package/template/src/components/endpoint/PlaygroundButton.astro +44 -0
  29. package/template/src/components/endpoint/PlaygroundField.astro +54 -0
  30. package/template/src/components/endpoint/PlaygroundForm.astro +203 -0
  31. package/template/src/components/endpoint/RequestSnippets.astro +308 -0
  32. package/template/src/components/endpoint/ResponseDisplay.astro +177 -0
  33. package/template/src/components/endpoint/ResponseFields.astro +224 -0
  34. package/template/src/components/endpoint/ResponseSnippets.astro +247 -0
  35. package/template/src/components/sidebar/SidebarEndpointLink.astro +51 -0
  36. package/template/src/components/sidebar/SidebarOpenApi.astro +207 -0
  37. package/template/src/components/ui/Field.astro +69 -0
  38. package/template/src/components/ui/Tag.astro +5 -0
  39. package/template/src/components/ui/demo/CodeDemo.astro +15 -0
  40. package/template/src/components/ui/demo/Demo.astro +3 -0
  41. package/template/src/components/ui/demo/UiDisplay.astro +13 -0
  42. package/template/src/components/user/Accordian.astro +69 -0
  43. package/template/src/components/user/AccordianGroup.astro +13 -0
  44. package/template/src/components/user/Callout.astro +101 -0
  45. package/template/src/components/user/Step.astro +51 -0
  46. package/template/src/components/user/Steps.astro +9 -0
  47. package/template/src/components/user/Tab.astro +25 -0
  48. package/template/src/components/user/Tabs.astro +122 -0
  49. package/template/src/content.config.ts +11 -0
  50. package/template/src/entrypoint.ts +9 -0
  51. package/template/src/layouts/Layout.astro +92 -0
  52. package/template/src/lib/component-error.ts +163 -0
  53. package/template/src/lib/frontmatter-schema.ts +9 -0
  54. package/template/src/lib/oas.ts +24 -0
  55. package/template/src/lib/pagefind.ts +88 -0
  56. package/template/src/lib/routes.ts +316 -0
  57. package/template/src/lib/utils.ts +59 -0
  58. package/template/src/lib/validation.ts +1097 -0
  59. package/template/src/pages/[...slug].astro +77 -0
  60. package/template/src/styles/global.css +209 -0
  61. 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>