radiant-docs 0.1.7 → 0.1.8

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 (78) hide show
  1. package/dist/index.js +28 -5
  2. package/package.json +3 -3
  3. package/template/astro.config.mjs +76 -3
  4. package/template/package-lock.json +924 -737
  5. package/template/package.json +7 -5
  6. package/template/scripts/generate-og-images.mjs +335 -0
  7. package/template/scripts/generate-og-metadata.mjs +173 -0
  8. package/template/scripts/rewrite-static-asset-host.mjs +408 -0
  9. package/template/scripts/stamp-image-versions.mjs +277 -0
  10. package/template/scripts/stamp-og-image-versions.mjs +199 -0
  11. package/template/scripts/stamp-pagefind-runtime-version.mjs +140 -0
  12. package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
  13. package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
  14. package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
  15. package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
  16. package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
  17. package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
  18. package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
  19. package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
  20. package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
  21. package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
  22. package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
  23. package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
  24. package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
  25. package/template/src/components/Footer.astro +94 -0
  26. package/template/src/components/Header.astro +11 -66
  27. package/template/src/components/LogoLink.astro +103 -0
  28. package/template/src/components/MdxPage.astro +126 -11
  29. package/template/src/components/OpenApiPage.astro +1036 -69
  30. package/template/src/components/Search.astro +0 -2
  31. package/template/src/components/SidebarDropdown.astro +34 -14
  32. package/template/src/components/SidebarGroup.astro +3 -6
  33. package/template/src/components/SidebarLink.astro +22 -12
  34. package/template/src/components/SidebarMenu.astro +19 -16
  35. package/template/src/components/SidebarSegmented.astro +99 -0
  36. package/template/src/components/SidebarSubgroup.astro +12 -12
  37. package/template/src/components/ThemeSwitcher.astro +30 -7
  38. package/template/src/components/endpoint/PlaygroundBar.astro +32 -36
  39. package/template/src/components/endpoint/PlaygroundButton.astro +40 -4
  40. package/template/src/components/endpoint/PlaygroundField.astro +1068 -22
  41. package/template/src/components/endpoint/PlaygroundForm.astro +559 -61
  42. package/template/src/components/endpoint/RequestSnippets.astro +342 -193
  43. package/template/src/components/endpoint/ResponseDisplay.astro +161 -147
  44. package/template/src/components/endpoint/ResponseFieldTree.astro +134 -0
  45. package/template/src/components/endpoint/ResponseFields.astro +711 -68
  46. package/template/src/components/endpoint/ResponseSnippets.astro +299 -173
  47. package/template/src/components/sidebar/SidebarEndpointLink.astro +1 -1
  48. package/template/src/components/ui/CodeLanguageIcon.astro +19 -0
  49. package/template/src/components/ui/CodeTabEdge.astro +79 -0
  50. package/template/src/components/ui/Field.astro +103 -20
  51. package/template/src/components/ui/Icon.astro +32 -0
  52. package/template/src/components/ui/ListChevronsToggle.astro +31 -0
  53. package/template/src/components/ui/Tag.astro +1 -1
  54. package/template/src/components/user/{Accordian.astro → Accordion.astro} +6 -6
  55. package/template/src/components/user/Callout.astro +5 -9
  56. package/template/src/components/user/CodeBlock.astro +400 -0
  57. package/template/src/components/user/CodeGroup.astro +225 -0
  58. package/template/src/components/user/ComponentPreview.astro +1 -0
  59. package/template/src/components/user/ComponentPreviewBlock.astro +181 -0
  60. package/template/src/components/user/Image.astro +132 -0
  61. package/template/src/components/user/Steps.astro +1 -3
  62. package/template/src/components/user/Tabs.astro +2 -2
  63. package/template/src/content.config.ts +1 -0
  64. package/template/src/layouts/Layout.astro +109 -8
  65. package/template/src/lib/code/code-block.ts +546 -0
  66. package/template/src/lib/frontmatter-schema.ts +8 -7
  67. package/template/src/lib/mdx/remark-code-block-component.ts +342 -0
  68. package/template/src/lib/mdx/remark-demote-h1.ts +16 -0
  69. package/template/src/lib/pagefind.ts +19 -5
  70. package/template/src/lib/routes.ts +49 -31
  71. package/template/src/lib/utils.ts +20 -0
  72. package/template/src/lib/validation.ts +638 -200
  73. package/template/src/pages/[...slug].astro +18 -5
  74. package/template/src/styles/geist-mono.css +33 -0
  75. package/template/src/styles/global.css +89 -84
  76. package/template/src/styles/google-sans-flex.css +143 -0
  77. package/template/ec.config.mjs +0 -51
  78. /package/template/src/components/user/{AccordianGroup.astro → AccordionGroup.astro} +0 -0
@@ -1,16 +1,19 @@
1
1
  ---
2
- import { Code } from "astro-expressive-code/components";
3
2
  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
3
  import oasToSnippet from "@readme/oas-to-snippet";
9
4
  import type { Language } from "@readme/oas-to-snippet/languages";
10
5
  import type { DataForHAR, HttpMethods, MediaTypeObject } from "oas/types";
11
6
  import * as Sampler from "openapi-sampler";
12
7
  import type { JSONSchema7 } from "json-schema";
13
8
  import type Oas from "oas";
9
+ import { methodColors } from "../../lib/utils";
10
+ import CodeTabEdge from "../ui/CodeTabEdge.astro";
11
+ import {
12
+ buildDefaultCodeFileName,
13
+ getCodeLineTokens,
14
+ getLanguageIconSvg,
15
+ normalizeCodeLanguageValue,
16
+ } from "../../lib/code/code-block";
14
17
 
15
18
  interface Props {
16
19
  api: Oas;
@@ -22,16 +25,12 @@ const { method, path, api } = Astro.props;
22
25
 
23
26
  const operation = api.operation(path, method as HttpMethods);
24
27
 
25
- // Identify and build the Auth object
26
28
  const auth: Record<string, string | number | { user?: string; pass?: string }> =
27
29
  {};
28
30
  const securityByType = operation.prepareSecurity();
29
31
  Object.values(securityByType).forEach((schemes) => {
30
- // schemes is an array of security scheme objects
31
32
  (schemes as any[]).forEach((scheme) => {
32
33
  const schemeName = scheme._key || scheme.name;
33
-
34
- // Keep the original conditionals - they're correct!
35
34
  if (scheme?.type === "http" && scheme?.scheme === "bearer") {
36
35
  auth[schemeName] = "<BEARER_TOKEN>";
37
36
  } else if (scheme?.type === "apiKey") {
@@ -42,37 +41,30 @@ Object.values(securityByType).forEach((schemes) => {
42
41
  });
43
42
  });
44
43
 
45
- // 1. Initialize the values object
46
44
  const values: DataForHAR = {
47
45
  path: {},
48
46
  query: {},
49
47
  body: undefined,
50
48
  };
51
49
 
52
- // 2. Process Parameters (Path and Query)
53
50
  const parameters = operation.getParameters();
54
51
  parameters.forEach((param) => {
55
52
  const name = param.name;
56
-
57
- // Get example if it exists
58
53
  const example =
59
54
  param.example ||
60
55
  (param.schema as any)?.example ||
61
56
  (param.schema as any)?.default;
62
57
 
63
58
  if (param.in === "path") {
64
- // Per your request: Maintain curly braces for path
65
59
  values.path![name] = `{${name}}`;
66
60
  } else if (param.in === "query") {
67
- // Precedence: Example -> Placeholder
68
61
  values.query![name] = example ? String(example) : `<${name.toUpperCase()}>`;
69
62
  }
70
63
  });
71
64
 
72
- // Process Request Body
73
65
  if (operation.hasRequestBody()) {
74
66
  const requestBody = operation.getRequestBody(
75
- "application/json"
67
+ "application/json",
76
68
  ) as MediaTypeObject;
77
69
  const schema = requestBody?.schema;
78
70
 
@@ -81,61 +73,53 @@ if (operation.hasRequestBody()) {
81
73
  }
82
74
  }
83
75
 
84
- let snippets: {
76
+ const generatedSnippets: Array<{
85
77
  code: string | false;
86
78
  highlightMode: string | false;
87
79
  install: string | false;
88
- }[] = [];
80
+ label?: string;
81
+ }> = [];
89
82
 
90
- const languages = [
91
- ["go", "native"],
92
- ["shell", "curl"],
93
- ["javascript", "fetch"],
94
- ["python", "requests"],
83
+ const snippetTargets: Array<{
84
+ lang: string;
85
+ target: string;
86
+ label: string;
87
+ }> = [
88
+ { lang: "shell", target: "curl", label: "cURL" },
89
+ { lang: "javascript", target: "fetch", label: "JavaScript" },
90
+ { lang: "python", target: "requests", label: "Python" },
91
+ { lang: "go", target: "native", label: "Go" },
92
+ { lang: "java", target: "okhttp", label: "Java" },
93
+ { lang: "php", target: "guzzle", label: "PHP" },
95
94
  ];
96
95
 
97
- languages.forEach(([lang, target]) => {
96
+ snippetTargets.forEach(({ lang, target, label }) => {
98
97
  try {
99
- // Generate the snippet
100
- // formData and auth can be empty objects for generic examples
101
- const res = oasToSnippet(api, operation, values, auth, [
98
+ const result = oasToSnippet(api, operation, values, auth, [
102
99
  lang,
103
100
  target,
104
101
  ] as Language);
105
102
 
106
- if (res && res.code) {
107
- res.code = res.code
103
+ if (result && result.code) {
104
+ result.code = result.code
108
105
  .replace(/%7B/g, "{")
109
106
  .replace(/%7D/g, "}")
110
107
  .replace(/%3C/g, "<")
111
108
  .replace(/%3E/g, ">");
109
+
110
+ result.code = result.code
111
+ .replace(/\r\n?/g, "\n")
112
+ .replace(/^[ \t]*\n/gm, "")
113
+ .trimEnd();
112
114
  }
113
115
 
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);
116
+ generatedSnippets.push({ ...result, label });
117
+ } catch (error) {
118
+ const message = error instanceof Error ? error.message : String(error);
119
+ console.error(`Failed to generate ${lang}/${target} snippet:`, message);
118
120
  }
119
121
  });
120
122
 
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
123
  const displayLabelMap: Record<string, string> = {
140
124
  shell: "cURL",
141
125
  javascript: "JavaScript",
@@ -143,166 +127,331 @@ const displayLabelMap: Record<string, string> = {
143
127
  go: "Go",
144
128
  };
145
129
 
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
- };
130
+ const CURL_ICON_SVG = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" class="size-3.5 shrink-0 self-center rounded-[4px]" xmlns="http://www.w3.org/2000/svg"><path d="M432 32H80a64.07 64.07 0 0 0-64 64v320a64.07 64.07 0 0 0 64 64h352a64.07 64.07 0 0 0 64-64V96a64.07 64.07 0 0 0-64-64zM96 256a16 16 0 0 1-10-28.49L150.39 176 86 124.49a16 16 0 1 1 20-25l80 64a16 16 0 0 1 0 25l-80 64A16 16 0 0 1 96 256zm160 0h-64a16 16 0 0 1 0-32h64a16 16 0 0 1 0 32z"></path></svg>`;
153
131
 
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
- }
132
+ function buildTokenStyle(token: {
133
+ color?: string;
134
+ bgColor?: string;
135
+ fontStyle?: number;
136
+ htmlStyle?: Record<string, string>;
137
+ }): string | undefined {
138
+ const styleSegments: string[] = [];
159
139
 
160
- // Prepare snippets with display labels
161
- const snippetsWithIcons = snippets.map((snippet) => {
162
- if (!snippet.highlightMode) {
163
- return { ...snippet, iconSvg: null, displayLabel: snippet.highlightMode };
140
+ if (token.color) styleSegments.push(`color:${token.color}`);
141
+ if (token.bgColor) styleSegments.push(`background-color:${token.bgColor}`);
142
+
143
+ const fontStyle = typeof token.fontStyle === "number" ? token.fontStyle : 0;
144
+ if ((fontStyle & 1) === 1) styleSegments.push("font-style:italic");
145
+ if ((fontStyle & 2) === 2) styleSegments.push("font-weight:600");
146
+ if ((fontStyle & 4) === 4) styleSegments.push("text-decoration:underline");
147
+
148
+ if (token.htmlStyle && typeof token.htmlStyle === "object") {
149
+ for (const [property, value] of Object.entries(token.htmlStyle)) {
150
+ styleSegments.push(`${property}:${value}`);
151
+ }
164
152
  }
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
- });
153
+
154
+ if (!styleSegments.length) return undefined;
155
+ return styleSegments.join(";");
156
+ }
157
+
158
+ function escapeHtml(value: string): string {
159
+ return value
160
+ .replaceAll("&", "&amp;")
161
+ .replaceAll("<", "&lt;")
162
+ .replaceAll(">", "&gt;");
163
+ }
164
+
165
+ function escapeAttribute(value: string): string {
166
+ return escapeHtml(value).replaceAll('"', "&quot;");
167
+ }
168
+
169
+ async function renderCodeLinesHtml(
170
+ code: string,
171
+ language: string,
172
+ ): Promise<string> {
173
+ const normalizedCode = code.replace(/\r\n?/g, "\n");
174
+ const { lines: tokenLines } = await getCodeLineTokens({
175
+ code: normalizedCode,
176
+ language,
177
+ });
178
+
179
+ const rawLines = normalizedCode.split("\n");
180
+ const normalizedRawLines = rawLines.length > 0 ? rawLines : [""];
181
+ const lineCount = Math.max(1, normalizedRawLines.length, tokenLines.length);
182
+ const normalizedTokenLines = Array.from({ length: lineCount }, (_, index) => {
183
+ return tokenLines[index] ?? [];
184
+ });
185
+
186
+ return normalizedTokenLines
187
+ .map((lineTokens, lineIndex) => {
188
+ const fallbackLineContent = normalizedRawLines[lineIndex] ?? "";
189
+ const tokenHtml =
190
+ lineTokens.length > 0
191
+ ? lineTokens
192
+ .map((token) => {
193
+ const tokenStyle = buildTokenStyle(token);
194
+ const tokenStyleAttribute = tokenStyle
195
+ ? ` style="${escapeAttribute(tokenStyle)}"`
196
+ : "";
197
+ return `<span${tokenStyleAttribute}>${escapeHtml(token.content)}</span>`;
198
+ })
199
+ .join("")
200
+ : fallbackLineContent.length > 0
201
+ ? escapeHtml(fallbackLineContent)
202
+ : "&nbsp;";
203
+
204
+ return `<span class="flex min-w-full"><span class="flex-1 whitespace-pre pr-4 pl-4">${tokenHtml}</span></span>`;
205
+ })
206
+ .join("");
207
+ }
208
+
209
+ const requestSnippetItems = await Promise.all(
210
+ generatedSnippets
211
+ .filter(
212
+ (
213
+ snippet,
214
+ ): snippet is {
215
+ code: string;
216
+ highlightMode: string;
217
+ install: string | false;
218
+ } => {
219
+ return (
220
+ typeof snippet.code === "string" &&
221
+ typeof snippet.highlightMode === "string"
222
+ );
223
+ },
224
+ )
225
+ .map(async (snippet) => {
226
+ const normalizedLanguage = normalizeCodeLanguageValue(
227
+ snippet.highlightMode,
228
+ );
229
+ const displayLabel =
230
+ snippet.label ??
231
+ displayLabelMap[normalizedLanguage] ??
232
+ snippet.highlightMode;
233
+ const iconSvg =
234
+ displayLabel === "cURL"
235
+ ? CURL_ICON_SVG
236
+ : await getLanguageIconSvg({
237
+ language: normalizedLanguage,
238
+ fileName: buildDefaultCodeFileName(normalizedLanguage),
239
+ className: "size-3.5 shrink-0 self-center rounded-[4px]",
240
+ });
241
+
242
+ return {
243
+ label: displayLabel,
244
+ code: snippet.code,
245
+ iconSvg,
246
+ renderedCodeLinesHtml: await renderCodeLinesHtml(
247
+ snippet.code,
248
+ normalizedLanguage,
249
+ ),
250
+ };
251
+ }),
252
+ );
253
+
254
+ if (requestSnippetItems.length === 0) {
255
+ const fallbackLanguage = "plaintext";
256
+ const fallbackCode = "Unable to generate request snippets.";
257
+ requestSnippetItems.push({
258
+ label: "Plaintext",
259
+ code: fallbackCode,
260
+ iconSvg: await getLanguageIconSvg({
261
+ language: fallbackLanguage,
262
+ fileName: buildDefaultCodeFileName(fallbackLanguage),
263
+ className: "size-3.5 shrink-0 self-center rounded-[4px]",
264
+ }),
265
+ renderedCodeLinesHtml: await renderCodeLinesHtml(
266
+ fallbackCode,
267
+ fallbackLanguage,
268
+ ),
269
+ });
270
+ }
271
+
272
+ const clientSnippets = requestSnippetItems.map((snippet) => ({
273
+ label: snippet.label,
274
+ code: snippet.code,
275
+ iconSvg: snippet.iconSvg,
276
+ }));
277
+ const snippetsData = JSON.stringify(clientSnippets).replaceAll("`", "\\`");
278
+ const firstSnippet = requestSnippetItems[0];
279
+ const hasMultipleRequests = requestSnippetItems.length > 1;
171
280
  ---
172
281
 
173
282
  <div
174
283
  x-data={`{
175
- snippets: ${JSON.stringify(snippetsWithIcons)},
284
+ snippets: ${snippetsData},
176
285
  selected: 0,
286
+ hasMultiple: ${hasMultipleRequests ? "true" : "false"},
287
+ copied: false,
288
+ pillLeft: 0,
289
+ pillWidth: 0,
290
+ pillVisible: false,
291
+ tabSyncHandler: null,
292
+ copyTimeoutId: null,
293
+ init() {
294
+ if (!this.hasMultiple) return;
295
+ const sync = () => this.syncPill();
296
+ this.tabSyncHandler = sync;
297
+ this.$nextTick(() => {
298
+ this.syncPill();
299
+ this.$refs.tabList?.addEventListener("scroll", sync, {
300
+ passive: true,
301
+ });
302
+ window.addEventListener("resize", sync);
303
+ });
304
+ },
305
+ syncPill() {
306
+ if (!this.hasMultiple) return;
307
+ const tabs = this.$refs.tabList;
308
+ if (!tabs) return;
309
+ const activeTab = tabs.querySelector(
310
+ '[data-rd-snippet-tab="' + this.selected + '"]',
311
+ );
312
+ if (!activeTab) {
313
+ this.pillVisible = false;
314
+ return;
315
+ }
316
+ const tabsRect = tabs.getBoundingClientRect();
317
+ const activeRect = activeTab.getBoundingClientRect();
318
+ this.pillLeft = activeRect.left - tabsRect.left + tabs.scrollLeft;
319
+ this.pillWidth = activeRect.width;
320
+ this.pillVisible = true;
321
+ },
177
322
  select(index) {
178
323
  this.selected = index;
179
- this.$refs.button?.focus();
180
- this.close();
324
+ this.$nextTick(() => {
325
+ this.syncPill();
326
+ window.dispatchEvent(new CustomEvent("rd:snippet-content-change"));
327
+ });
181
328
  },
182
- close(focusAfter) {
183
- if (!this.open) return;
184
- this.open = false;
185
- focusAfter?.focus();
329
+ async copySelected() {
330
+ const snippet = this.snippets[this.selected];
331
+ if (!snippet) return;
332
+ if (!navigator?.clipboard?.writeText) {
333
+ this.copied = false;
334
+ return;
335
+ }
336
+ try {
337
+ await navigator.clipboard.writeText(snippet.code ?? "");
338
+ this.copied = true;
339
+ if (this.copyTimeoutId) window.clearTimeout(this.copyTimeoutId);
340
+ this.copyTimeoutId = window.setTimeout(() => {
341
+ this.copied = false;
342
+ this.copyTimeoutId = null;
343
+ }, 1200);
344
+ } catch {
345
+ this.copied = false;
346
+ }
186
347
  }
187
348
  }`}
188
- class="request-code-snippets bg-neutral-100 rounded-[14px] border border-neutral-200 p-[3px] inset-shadow-xs"
349
+ class="request-code-snippets h-full min-h-0 w-full min-w-0"
189
350
  >
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>
351
+ <div class="group/prose-code not-prose relative h-full min-h-0 w-full max-w-full min-w-0">
199
352
  <div
200
- x-data="{
201
- open: false,
202
- toggle() {
203
- if (this.open) {
204
- return this.close()
205
- }
206
-
207
- this.$refs.button.focus()
353
+ class="flex h-full min-h-0 w-full max-w-full min-w-0 flex-col overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-xs"
354
+ >
355
+ <div
356
+ class="flex items-center justify-between gap-2 border-b border-neutral-200 bg-neutral-50 rounded-t-xl inset-shadow-sm inset-shadow-neutral-100/80"
357
+ >
358
+ {
359
+ hasMultipleRequests ? (
360
+ <div class="min-w-0 flex-1 overflow-hidden rounded-t-xl">
361
+ <div
362
+ x-ref="tabList"
363
+ class="relative flex min-w-0 items-end gap-1 overflow-x-auto pl-1 pr-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
364
+ >
365
+ <div
366
+ aria-hidden="true"
367
+ class="pointer-events-none absolute top-1/2 z-0 h-[28px] -translate-y-1/2 rounded-lg border-[0.5px] border-neutral-200 bg-white shadow-xs transition-[left,width,opacity] duration-200 ease-out"
368
+ x-bind:class="pillVisible ? 'opacity-100' : 'opacity-0'"
369
+ x-bind:style="'left:' + pillLeft + 'px;width:' + pillWidth + 'px;'"
370
+ />
208
371
 
209
- this.open = true
210
- },
211
- close(focusAfter) {
212
- if (! this.open) return
372
+ <template
373
+ x-for="(snippet, index) in snippets"
374
+ :key="snippet.label + index"
375
+ >
376
+ <button
377
+ type="button"
378
+ x-bind:data-rd-snippet-tab="index"
379
+ x-on:click="select(index)"
380
+ class="relative z-10 inline-flex h-9 items-center gap-2 border-0 bg-transparent px-3 py-1.5 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer"
381
+ x-bind:class="selected === index ? 'text-neutral-900' : 'text-neutral-600'"
382
+ >
383
+ <span
384
+ x-show="snippet.iconSvg"
385
+ x-html="snippet.iconSvg"
386
+ class="pointer-events-none inline-flex size-3.5 shrink-0 items-center rounded-[4px] transition-opacity duration-150"
387
+ x-bind:class="selected === index ? 'opacity-100' : 'opacity-70'"></span>
388
+ <span class="whitespace-pre leading-none" x-text="snippet.label"></span>
389
+ </button>
390
+ </template>
391
+ </div>
392
+ </div>
393
+ ) : (
394
+ <div class="min-w-0 flex-1">
395
+ <div class="relative h-9 w-fit max-w-full rounded-tl-xl bg-white">
396
+ <div class="absolute inset-x-0 -bottom-px h-px bg-white"></div>
397
+ <CodeTabEdge
398
+ className="pointer-events-none absolute -top-px left-full z-10 h-[calc(100%+2px)]"
399
+ />
213
400
 
214
- this.open = false
401
+ <div class="relative z-20 inline-flex h-9 max-w-full items-center gap-2 pl-5 pr-3 py-1.5 text-xs font-medium text-neutral-700">
402
+ <span
403
+ class="size-3.5 shrink-0 self-center rounded-[4px]"
404
+ set:html={firstSnippet.iconSvg}
405
+ />
406
+ <span class="truncate leading-none">{firstSnippet.label}</span>
407
+ </div>
408
+ </div>
409
+ </div>
410
+ )
411
+ }
215
412
 
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 || ""}
413
+ <div class="relative h-9 w-5 shrink-0 rounded-tr-xl bg-white">
414
+ <div class="absolute inset-x-0 -bottom-px h-px bg-white"></div>
415
+ <CodeTabEdge
416
+ className="pointer-events-none absolute -top-px right-full z-10 h-[calc(100%+2px)] rotate-y-180"
242
417
  />
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>
418
+ <button
419
+ x-on:click="copySelected()"
420
+ type="button"
421
+ class="absolute right-2 top-1/2 z-20 inline-flex size-7 -translate-y-1/2 appearance-none items-center justify-center rounded-md border-0 bg-transparent text-neutral-500/80 shadow-none outline-none ring-0 transition-colors duration-150 hover:bg-neutral-50 hover:text-neutral-600 focus:outline-none focus-visible:outline-none focus:ring-0 focus-visible:ring-0 cursor-pointer"
422
+ aria-label="Copy code"
423
+ >
424
+ <Icon
425
+ name="lucide:copy"
426
+ class="size-3.5 origin-center transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
427
+ x-bind:class="copied ? 'scale-50 opacity-0 -rotate-6' : 'scale-100 opacity-100 rotate-0'"
428
+ aria-hidden="true"
429
+ />
430
+ <Icon
431
+ name="lucide:check"
432
+ class="absolute size-3.5 stroke-3 origin-center text-green-700/80 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
433
+ x-bind:class="copied ? 'scale-110 opacity-100 rotate-0' : 'scale-25 opacity-0 rotate-6'"
434
+ aria-hidden="true"
435
+ />
436
+ </button>
437
+ </div>
438
+ </div>
439
+
440
+ <div class="relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-b-xl">
441
+ <div class="relative h-full overflow-auto [scrollbar-width:thin] [scrollbar-color:var(--color-neutral-300)_transparent] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-neutral-300/70 hover:[&::-webkit-scrollbar-thumb]:bg-neutral-300/90">
442
+ <div class="pointer-events-none sticky top-0 z-10 -mb-4 h-4 w-full bg-linear-to-b from-white to-transparent"></div>
443
+ {
444
+ requestSnippetItems.map((snippet, index) => (
445
+ <pre
446
+ class="relative m-0 min-w-full bg-white p-0 text-[13px] leading-6"
447
+ x-show={`selected === ${index}`}
448
+ {...(index !== 0 ? { "x-cloak": true } : {})}
449
+ data-snippet-index={index}
450
+ ><code class="block min-w-full py-2.5 font-mono text-neutral-800"><Fragment set:html={snippet.renderedCodeLinesHtml} /></code></pre>
451
+ ))
452
+ }
453
+ </div>
454
+ </div>
275
455
  </div>
276
456
  </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
457
  </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>