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,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from "astro-icon/components";
|
|
3
|
+
import type { Field as FieldType } from "../OpenApiPage.astro";
|
|
4
|
+
import Field from "../ui/Field.astro";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
field: FieldType;
|
|
8
|
+
requestPart: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { field, requestPart } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
class="flex justify-between flex-col sm:flex-row lg:flex-col xl:flex-row gap-4"
|
|
16
|
+
>
|
|
17
|
+
<div class="w-full mt-1.5">
|
|
18
|
+
<Field
|
|
19
|
+
name={field.name}
|
|
20
|
+
type={field.type}
|
|
21
|
+
description={field.description}
|
|
22
|
+
required={field.required}
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="w-full">
|
|
26
|
+
{
|
|
27
|
+
field.enum && field.enum.length > 0 ? (
|
|
28
|
+
<div class="relative">
|
|
29
|
+
<select
|
|
30
|
+
name={`${requestPart}_${field.name}`}
|
|
31
|
+
x-model={`inputs['${requestPart}']['${field.name}']`}
|
|
32
|
+
class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow placeholder:text-neutral-400 transition-all duration-200 bg-white appearance-none"
|
|
33
|
+
>
|
|
34
|
+
{field.enum.map((value) => (
|
|
35
|
+
<option value={String(value)}>{value}</option>
|
|
36
|
+
))}
|
|
37
|
+
</select>
|
|
38
|
+
<Icon
|
|
39
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
|
|
40
|
+
name="lucide:chevrons-up-down"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
) : (
|
|
44
|
+
<input
|
|
45
|
+
name={field.name}
|
|
46
|
+
placeholder={`Enter ${field.name}`}
|
|
47
|
+
type={field.type === "number" ? "number" : "text"}
|
|
48
|
+
x-model={`inputs['${requestPart}']['${field.name}']`}
|
|
49
|
+
class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow placeholder:text-neutral-400 transition-all duration-200"
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from "astro-icon/components";
|
|
3
|
+
import type { OpenApiRoute } from "../../lib/routes";
|
|
4
|
+
import { renderMarkdown } from "../../lib/utils";
|
|
5
|
+
import { headers, type RequestFields } from "../OpenApiPage.astro";
|
|
6
|
+
import Accordian from "../user/Accordian.astro";
|
|
7
|
+
import PlaygroundBar from "./PlaygroundBar.astro";
|
|
8
|
+
import Field from "../ui/Field.astro";
|
|
9
|
+
import ResponseDisplay from "./ResponseDisplay.astro";
|
|
10
|
+
import PlaygroundField from "./PlaygroundField.astro";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
route: OpenApiRoute;
|
|
14
|
+
serverUrl?: string;
|
|
15
|
+
requestFields: RequestFields;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { route, serverUrl, requestFields } = Astro.props;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<div
|
|
22
|
+
x-data=`{
|
|
23
|
+
loading: false,
|
|
24
|
+
response: null,
|
|
25
|
+
inputs: {
|
|
26
|
+
header: {},
|
|
27
|
+
path: {},
|
|
28
|
+
query: {},
|
|
29
|
+
body: {}
|
|
30
|
+
},
|
|
31
|
+
async sendRequest() {
|
|
32
|
+
this.loading = true;
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
34
|
+
try {
|
|
35
|
+
// 1. Construct URL with Path Parameters
|
|
36
|
+
let url = ${JSON.stringify(serverUrl + route.openApiPath)};
|
|
37
|
+
Object.entries(this.inputs.path).forEach(([key, val]) => {
|
|
38
|
+
url = url.replace('{' + key + '}', encodeURIComponent(val));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// 2. Add Query Parameters
|
|
42
|
+
const queryParams = new URLSearchParams(this.inputs.query).toString();
|
|
43
|
+
if (queryParams) url += '?' + queryParams;
|
|
44
|
+
|
|
45
|
+
// 3. Prepare Fetch Options
|
|
46
|
+
console.log("Hello", this.inputs)
|
|
47
|
+
const options = {
|
|
48
|
+
method: ${JSON.stringify(route.openApiMethod.toUpperCase())},
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
...this.inputs.header
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 4. Add Body if needed
|
|
56
|
+
if (['POST', 'PUT', 'PATCH'].includes(options.method)) {
|
|
57
|
+
options.body = JSON.stringify(this.inputs.body);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 5. Execute Request
|
|
61
|
+
const res = await fetch(url, options);
|
|
62
|
+
|
|
63
|
+
const status = res.status;
|
|
64
|
+
const statusTextMap = {
|
|
65
|
+
400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden',
|
|
66
|
+
404: 'Not Found', 405: 'Method Not Allowed', 409: 'Conflict',
|
|
67
|
+
422: 'Unprocessable Entity', 429: 'Too Many Requests',
|
|
68
|
+
500: 'Internal Server Error', 502: 'Bad Gateway',
|
|
69
|
+
503: 'Service Unavailable', 504: 'Gateway Timeout'
|
|
70
|
+
};
|
|
71
|
+
const statusText = res.statusText || statusTextMap[status] || 'Unknown';
|
|
72
|
+
|
|
73
|
+
const headers = {};
|
|
74
|
+
res.headers.forEach((value, key) => {
|
|
75
|
+
headers[key] = value;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let data;
|
|
79
|
+
let highlightedData;
|
|
80
|
+
const contentType = res.headers.get('content-type') || '';
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
if (contentType.includes('application/json')) {
|
|
84
|
+
data = await res.json();
|
|
85
|
+
highlightedData = Prism.highlight(
|
|
86
|
+
JSON.stringify(data, null, 2),
|
|
87
|
+
Prism.languages.json,
|
|
88
|
+
'json'
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
// For non-JSON responses, try to get as text
|
|
92
|
+
const text = await res.text();
|
|
93
|
+
console.log("TEXT", text)
|
|
94
|
+
highlightedData = Prism.highlight(
|
|
95
|
+
text || '(empty response)',
|
|
96
|
+
Prism.languages.plaintext,
|
|
97
|
+
'plaintext'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
} catch (parseError) {
|
|
101
|
+
// If parsing fails, still show the response with error info
|
|
102
|
+
highlightedData = Prism.highlight(
|
|
103
|
+
JSON.stringify({ error: 'Failed to parse response', message: parseError.message }, null, 2),
|
|
104
|
+
Prism.languages.json,
|
|
105
|
+
'json'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.response = {
|
|
110
|
+
status,
|
|
111
|
+
statusText,
|
|
112
|
+
headers,
|
|
113
|
+
highlightedData
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.log('error', err)
|
|
118
|
+
this.response = {
|
|
119
|
+
status: err.status || 0,
|
|
120
|
+
statusText: err.statusText || err.message || 'Network Error',
|
|
121
|
+
};
|
|
122
|
+
} finally {
|
|
123
|
+
this.loading = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}`
|
|
127
|
+
class="flex flex-col gap-4"
|
|
128
|
+
>
|
|
129
|
+
<div class="w-full">
|
|
130
|
+
<PlaygroundBar route={route} serverUrl={serverUrl}>
|
|
131
|
+
<button
|
|
132
|
+
@click="sendRequest()"
|
|
133
|
+
:disabled="loading"
|
|
134
|
+
class="m-px flex items-center gap-1.5 px-4 py-[5px] text-sm font-medium rounded-lg bg-neutral-900 text-white/95 hover:text-white shadow-[inset_0_1px_0_rgb(255,255,255,0.3),0_0_0_1px_var(--color-neutral-800)] duration-200 whitespace-nowrap cursor-pointer relative before:absolute before:inset-0 before:shadow-sm before:rounded-lg before:bg-transparent disabled:bg-neutral-300 disabled:shadow-none disabled:before:shadow-none disabled:duration-0"
|
|
135
|
+
>
|
|
136
|
+
<span
|
|
137
|
+
class="flex items-center gap-2"
|
|
138
|
+
x-bind:class="loading ? 'opacity-0':''"
|
|
139
|
+
>
|
|
140
|
+
Send <span class="hidden xs:inline">Request</span>
|
|
141
|
+
<Icon class="size-4" name="lucide:square-arrow-up-right" />
|
|
142
|
+
</span>
|
|
143
|
+
<Icon
|
|
144
|
+
x-show="loading"
|
|
145
|
+
class="size-4 absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 animate-spin **:stroke-3"
|
|
146
|
+
name="lucide:loader"
|
|
147
|
+
/>
|
|
148
|
+
</button>
|
|
149
|
+
</PlaygroundBar>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="flex flex-col lg:flex-row-reverse gap-4">
|
|
153
|
+
<div class="lg:max-w-xl w-full">
|
|
154
|
+
<ResponseDisplay />
|
|
155
|
+
</div>
|
|
156
|
+
<div class="flex-3 space-y-4">
|
|
157
|
+
{
|
|
158
|
+
Object.keys(requestFields).map(
|
|
159
|
+
(key) =>
|
|
160
|
+
requestFields[key as keyof RequestFields].length > 0 && (
|
|
161
|
+
<div class="border border-neutral-200 shadow-xs rounded-xl p-4">
|
|
162
|
+
<Accordian title={headers[key]} defaultOpen titleSize="xl">
|
|
163
|
+
{requestFields[key as keyof RequestFields].map((field) => (
|
|
164
|
+
<div class="border-b border-b-neutral-100 last:border-none pb-4 mb-4 last:pb-0 last:mb-0 first:pt-2">
|
|
165
|
+
<PlaygroundField field={field} requestPart={key} />
|
|
166
|
+
</div>
|
|
167
|
+
))}
|
|
168
|
+
</Accordian>
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<script>
|
|
178
|
+
import Prism from "prismjs";
|
|
179
|
+
import "prismjs/components/prism-json";
|
|
180
|
+
// Use a dark theme that matches the #0d1117 background
|
|
181
|
+
import "prismjs/themes/prism-tomorrow.css";
|
|
182
|
+
// Make Prism available to Alpine
|
|
183
|
+
window.Prism = Prism;
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<style>
|
|
187
|
+
@reference "../../styles/global.css";
|
|
188
|
+
|
|
189
|
+
:global(pre code span.token.punctuation) {
|
|
190
|
+
@apply text-[#24292E];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
:global(pre code span.token.operator) {
|
|
194
|
+
@apply text-[#24292E];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
:global(pre code span.token.property) {
|
|
198
|
+
@apply text-[#C62C2C];
|
|
199
|
+
}
|
|
200
|
+
:global(pre code span.token.string) {
|
|
201
|
+
@apply text-[#1E7734];
|
|
202
|
+
}
|
|
203
|
+
</style>
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Code } from "astro-expressive-code/components";
|
|
3
|
+
import { Icon } from "astro-icon/components";
|
|
4
|
+
import { methodColors } from "../../lib/utils";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { resolve, dirname } from "node:path";
|
|
8
|
+
import oasToSnippet from "@readme/oas-to-snippet";
|
|
9
|
+
import type { Language } from "@readme/oas-to-snippet/languages";
|
|
10
|
+
import type { DataForHAR, HttpMethods, MediaTypeObject } from "oas/types";
|
|
11
|
+
import * as Sampler from "openapi-sampler";
|
|
12
|
+
import type { JSONSchema7 } from "json-schema";
|
|
13
|
+
import type Oas from "oas";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
api: Oas;
|
|
17
|
+
method: string;
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { method, path, api } = Astro.props;
|
|
22
|
+
|
|
23
|
+
const operation = api.operation(path, method as HttpMethods);
|
|
24
|
+
|
|
25
|
+
// Identify and build the Auth object
|
|
26
|
+
const auth: Record<string, string | number | { user?: string; pass?: string }> =
|
|
27
|
+
{};
|
|
28
|
+
const securityByType = operation.prepareSecurity();
|
|
29
|
+
Object.values(securityByType).forEach((schemes) => {
|
|
30
|
+
// schemes is an array of security scheme objects
|
|
31
|
+
(schemes as any[]).forEach((scheme) => {
|
|
32
|
+
const schemeName = scheme._key || scheme.name;
|
|
33
|
+
|
|
34
|
+
// Keep the original conditionals - they're correct!
|
|
35
|
+
if (scheme?.type === "http" && scheme?.scheme === "bearer") {
|
|
36
|
+
auth[schemeName] = "<BEARER_TOKEN>";
|
|
37
|
+
} else if (scheme?.type === "apiKey") {
|
|
38
|
+
auth[schemeName] = "<API_KEY>";
|
|
39
|
+
} else if (scheme?.type === "http" && scheme?.scheme === "basic") {
|
|
40
|
+
auth[schemeName] = { user: "<USERNAME>", pass: "<PASSWORD>" };
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 1. Initialize the values object
|
|
46
|
+
const values: DataForHAR = {
|
|
47
|
+
path: {},
|
|
48
|
+
query: {},
|
|
49
|
+
body: undefined,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// 2. Process Parameters (Path and Query)
|
|
53
|
+
const parameters = operation.getParameters();
|
|
54
|
+
parameters.forEach((param) => {
|
|
55
|
+
const name = param.name;
|
|
56
|
+
|
|
57
|
+
// Get example if it exists
|
|
58
|
+
const example =
|
|
59
|
+
param.example ||
|
|
60
|
+
(param.schema as any)?.example ||
|
|
61
|
+
(param.schema as any)?.default;
|
|
62
|
+
|
|
63
|
+
if (param.in === "path") {
|
|
64
|
+
// Per your request: Maintain curly braces for path
|
|
65
|
+
values.path![name] = `{${name}}`;
|
|
66
|
+
} else if (param.in === "query") {
|
|
67
|
+
// Precedence: Example -> Placeholder
|
|
68
|
+
values.query![name] = example ? String(example) : `<${name.toUpperCase()}>`;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Process Request Body
|
|
73
|
+
if (operation.hasRequestBody()) {
|
|
74
|
+
const requestBody = operation.getRequestBody(
|
|
75
|
+
"application/json"
|
|
76
|
+
) as MediaTypeObject;
|
|
77
|
+
const schema = requestBody?.schema;
|
|
78
|
+
|
|
79
|
+
if (schema) {
|
|
80
|
+
values.body = Sampler.sample(schema as JSONSchema7, { skipReadOnly: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let snippets: {
|
|
85
|
+
code: string | false;
|
|
86
|
+
highlightMode: string | false;
|
|
87
|
+
install: string | false;
|
|
88
|
+
}[] = [];
|
|
89
|
+
|
|
90
|
+
const languages = [
|
|
91
|
+
["go", "native"],
|
|
92
|
+
["shell", "curl"],
|
|
93
|
+
["javascript", "fetch"],
|
|
94
|
+
["python", "requests"],
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
languages.forEach(([lang, target]) => {
|
|
98
|
+
try {
|
|
99
|
+
// Generate the snippet
|
|
100
|
+
// formData and auth can be empty objects for generic examples
|
|
101
|
+
const res = oasToSnippet(api, operation, values, auth, [
|
|
102
|
+
lang,
|
|
103
|
+
target,
|
|
104
|
+
] as Language);
|
|
105
|
+
|
|
106
|
+
if (res && res.code) {
|
|
107
|
+
res.code = res.code
|
|
108
|
+
.replace(/%7B/g, "{")
|
|
109
|
+
.replace(/%7D/g, "}")
|
|
110
|
+
.replace(/%3C/g, "<")
|
|
111
|
+
.replace(/%3E/g, ">");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
snippets.push(res);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
117
|
+
console.error(`Failed to generate ${lang} snippet:`, errorMessage);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Resolve the path to node_modules
|
|
122
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
123
|
+
const __dirname = dirname(__filename);
|
|
124
|
+
const iconsPath = resolve(
|
|
125
|
+
__dirname,
|
|
126
|
+
"../../../node_modules/@xt0rted/expressive-code-file-icons/dist/icons"
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Helper function to read SVG file
|
|
130
|
+
function getIconSvg(iconName: string): string {
|
|
131
|
+
try {
|
|
132
|
+
return readFileSync(resolve(iconsPath, `${iconName}.svg`), "utf-8");
|
|
133
|
+
} catch {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Map language to display label
|
|
139
|
+
const displayLabelMap: Record<string, string> = {
|
|
140
|
+
shell: "cURL",
|
|
141
|
+
javascript: "JavaScript",
|
|
142
|
+
python: "Python",
|
|
143
|
+
go: "Go",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Map language to icon filename
|
|
147
|
+
const iconFilenameMap: Record<string, string> = {
|
|
148
|
+
shell: "file_type_shell",
|
|
149
|
+
javascript: "file_type_js_official",
|
|
150
|
+
python: "file_type_python",
|
|
151
|
+
go: "file_type_go",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Create icon map with SVG content
|
|
155
|
+
const iconMap: Record<string, string> = {};
|
|
156
|
+
for (const [lang, iconName] of Object.entries(iconFilenameMap)) {
|
|
157
|
+
iconMap[lang] = getIconSvg(iconName);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Prepare snippets with display labels
|
|
161
|
+
const snippetsWithIcons = snippets.map((snippet) => {
|
|
162
|
+
if (!snippet.highlightMode) {
|
|
163
|
+
return { ...snippet, iconSvg: null, displayLabel: snippet.highlightMode };
|
|
164
|
+
}
|
|
165
|
+
const iconSvg = iconMap[snippet.highlightMode.toLowerCase()] || null;
|
|
166
|
+
const displayLabel =
|
|
167
|
+
displayLabelMap[snippet.highlightMode.toLowerCase()] ||
|
|
168
|
+
snippet.highlightMode;
|
|
169
|
+
return { ...snippet, iconSvg, displayLabel };
|
|
170
|
+
});
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
<div
|
|
174
|
+
x-data={`{
|
|
175
|
+
snippets: ${JSON.stringify(snippetsWithIcons)},
|
|
176
|
+
selected: 0,
|
|
177
|
+
select(index) {
|
|
178
|
+
this.selected = index;
|
|
179
|
+
this.$refs.button?.focus();
|
|
180
|
+
this.close();
|
|
181
|
+
},
|
|
182
|
+
close(focusAfter) {
|
|
183
|
+
if (!this.open) return;
|
|
184
|
+
this.open = false;
|
|
185
|
+
focusAfter?.focus();
|
|
186
|
+
}
|
|
187
|
+
}`}
|
|
188
|
+
class="request-code-snippets bg-neutral-100 rounded-[14px] border border-neutral-200 p-[3px] inset-shadow-xs"
|
|
189
|
+
>
|
|
190
|
+
<div class="flex justify-between items-center">
|
|
191
|
+
<div class="font-medium ml-2 text-neutral-700 text-sm truncate">
|
|
192
|
+
<span
|
|
193
|
+
class:list={[
|
|
194
|
+
"text-xs uppercase font-semibold px-1.5 border py-px rounded-md mr-1.5",
|
|
195
|
+
methodColors[method],
|
|
196
|
+
]}>{method}</span
|
|
197
|
+
>{path}
|
|
198
|
+
</div>
|
|
199
|
+
<div
|
|
200
|
+
x-data="{
|
|
201
|
+
open: false,
|
|
202
|
+
toggle() {
|
|
203
|
+
if (this.open) {
|
|
204
|
+
return this.close()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.$refs.button.focus()
|
|
208
|
+
|
|
209
|
+
this.open = true
|
|
210
|
+
},
|
|
211
|
+
close(focusAfter) {
|
|
212
|
+
if (! this.open) return
|
|
213
|
+
|
|
214
|
+
this.open = false
|
|
215
|
+
|
|
216
|
+
focusAfter && focusAfter.focus()
|
|
217
|
+
}
|
|
218
|
+
}"
|
|
219
|
+
x-on:keydown.escape.prevent.stop="close($refs.button)"
|
|
220
|
+
x-on:focusin.window="! $refs.panel.contains($event.target) && close()"
|
|
221
|
+
x-id="['dropdown-button']"
|
|
222
|
+
class="shrink-0 relative max-w-36 w-full"
|
|
223
|
+
>
|
|
224
|
+
<button
|
|
225
|
+
x-ref="button"
|
|
226
|
+
x-on:click="toggle()"
|
|
227
|
+
:aria-expanded="open"
|
|
228
|
+
:aria-controls="$id('dropdown-button')"
|
|
229
|
+
type="button"
|
|
230
|
+
class="flex items-center 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"
|
|
231
|
+
>
|
|
232
|
+
<span class="flex items-center gap-2">
|
|
233
|
+
<span
|
|
234
|
+
x-show="snippets[selected].iconSvg"
|
|
235
|
+
x-html="snippets[selected].iconSvg"
|
|
236
|
+
class="size-4 [&>svg]:w-full [&>svg]:h-full rounded overflow-hidden"
|
|
237
|
+
set:html={snippetsWithIcons[0]?.iconSvg || ""}
|
|
238
|
+
/>
|
|
239
|
+
<span
|
|
240
|
+
x-text="snippets[selected].displayLabel"
|
|
241
|
+
set:html={snippetsWithIcons[0]?.displayLabel || ""}
|
|
242
|
+
/>
|
|
243
|
+
</span>
|
|
244
|
+
<Icon name="lucide:chevrons-up-down" class="ml-auto" />
|
|
245
|
+
</button>
|
|
246
|
+
<!-- Panel -->
|
|
247
|
+
<ul
|
|
248
|
+
x-ref="panel"
|
|
249
|
+
x-show="open"
|
|
250
|
+
x-transition.origin.top.left
|
|
251
|
+
x-on:click.outside="close($refs.button)"
|
|
252
|
+
:id="$id('dropdown-button')"
|
|
253
|
+
x-cloak
|
|
254
|
+
role="menu"
|
|
255
|
+
class="absolute top-full right-2 min-w-48 rounded-lg shadow-xl mt-2 z-10 origin-top-right bg-white py-0.5 outline-none border border-neutral-200"
|
|
256
|
+
>
|
|
257
|
+
<template x-for="(snippet, index) in snippets" :key="index">
|
|
258
|
+
<li role="menuitem">
|
|
259
|
+
<button
|
|
260
|
+
x-on:click="select(index)"
|
|
261
|
+
type="button"
|
|
262
|
+
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"
|
|
263
|
+
x-bind:class="index === selected ? 'before:bg-neutral-200/50 text-neutral-900' : 'hover:before:bg-neutral-100/70'"
|
|
264
|
+
>
|
|
265
|
+
<span
|
|
266
|
+
x-show="snippet.iconSvg"
|
|
267
|
+
x-html="snippet.iconSvg"
|
|
268
|
+
class="size-4 [&>svg]:w-full [&>svg]:h-full rounded overflow-hidden"
|
|
269
|
+
></span>
|
|
270
|
+
<span x-text="snippet.displayLabel"></span>
|
|
271
|
+
</button>
|
|
272
|
+
</li>
|
|
273
|
+
</template>
|
|
274
|
+
</ul>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
{
|
|
278
|
+
snippets.map((snippet, index) => (
|
|
279
|
+
<div
|
|
280
|
+
{...(index !== 0 ? { "x-cloak": true } : {})}
|
|
281
|
+
x-show={`selected === ${index}`}
|
|
282
|
+
data-snippet-index={index}
|
|
283
|
+
>
|
|
284
|
+
{snippet.code && snippet.highlightMode && (
|
|
285
|
+
<Code
|
|
286
|
+
code={snippet.code as string}
|
|
287
|
+
lang={snippet.highlightMode as string}
|
|
288
|
+
frame="code"
|
|
289
|
+
/>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
))
|
|
293
|
+
}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<style>
|
|
297
|
+
@reference "../../styles/global.css";
|
|
298
|
+
:global(.request-code-snippets .expressive-code .frame) {
|
|
299
|
+
@apply shadow-sm;
|
|
300
|
+
}
|
|
301
|
+
:global(.request-code-snippets .expressive-code pre) {
|
|
302
|
+
@apply rounded-xl! rounded-tr-none! border-neutral-200/70!;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
:global(.request-code-snippets .expressive-code pre code) {
|
|
306
|
+
@apply max-h-64 overflow-y-auto;
|
|
307
|
+
}
|
|
308
|
+
</style>
|