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
@@ -0,0 +1,400 @@
1
+ ---
2
+ import CodeLanguageIcon from "../ui/CodeLanguageIcon.astro";
3
+ import CodeTabEdge from "../ui/CodeTabEdge.astro";
4
+ import { Icon } from "astro-icon/components";
5
+ import {
6
+ buildDefaultCodeFileName,
7
+ getCodeLineTokens,
8
+ normalizeCodeLanguageValue,
9
+ } from "../../lib/code/code-block";
10
+
11
+ interface Props {
12
+ language?: string;
13
+ raw?: string;
14
+ filename?: string;
15
+ showFilename?: boolean | string;
16
+ showLineNumbers?: boolean | string;
17
+ hideLanguageIcon?: boolean | string;
18
+ inCodeGroup?: boolean | string;
19
+ highlightedLines?: string;
20
+ collapsedLines?: string;
21
+ }
22
+
23
+ const {
24
+ language = "plaintext",
25
+ raw = "",
26
+ filename = "",
27
+ showFilename = false,
28
+ showLineNumbers = false,
29
+ hideLanguageIcon = false,
30
+ inCodeGroup = false,
31
+ highlightedLines = "",
32
+ collapsedLines = "",
33
+ } = Astro.props as Props;
34
+
35
+ function toBoolean(value: boolean | string | undefined, defaultValue = false) {
36
+ if (typeof value === "boolean") return value;
37
+ if (typeof value === "string") {
38
+ const normalized = value.trim().toLowerCase();
39
+ if (normalized === "true") return true;
40
+ if (normalized === "false") return false;
41
+ }
42
+ return defaultValue;
43
+ }
44
+
45
+ function normalizeHighlightedLinesSpec(value: string): string | null {
46
+ const normalizedValue = value.trim();
47
+ if (!normalizedValue) return null;
48
+
49
+ const segments = normalizedValue.split(",");
50
+ const normalizedSegments: string[] = [];
51
+
52
+ for (const segment of segments) {
53
+ const trimmedSegment = segment.trim();
54
+ if (!trimmedSegment) continue;
55
+
56
+ if (/^\d+$/.test(trimmedSegment)) {
57
+ const lineNumber = Number.parseInt(trimmedSegment, 10);
58
+ if (lineNumber > 0) normalizedSegments.push(String(lineNumber));
59
+ continue;
60
+ }
61
+
62
+ const rangeMatch = trimmedSegment.match(/^(\d+)\s*-\s*(\d+)$/);
63
+ if (!rangeMatch) continue;
64
+
65
+ const start = Number.parseInt(rangeMatch[1], 10);
66
+ const end = Number.parseInt(rangeMatch[2], 10);
67
+ if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
68
+ if (start <= 0 || end <= 0) continue;
69
+
70
+ const rangeStart = Math.min(start, end);
71
+ const rangeEnd = Math.max(start, end);
72
+ normalizedSegments.push(
73
+ rangeStart === rangeEnd
74
+ ? String(rangeStart)
75
+ : `${rangeStart}-${rangeEnd}`,
76
+ );
77
+ }
78
+
79
+ if (!normalizedSegments.length) return null;
80
+ return normalizedSegments.join(",");
81
+ }
82
+
83
+ function parseHighlightedLineNumbers(
84
+ lineSpec: string,
85
+ lineCount: number,
86
+ ): Set<number> {
87
+ const normalizedSpec = normalizeHighlightedLinesSpec(lineSpec);
88
+ if (!normalizedSpec) return new Set();
89
+
90
+ const highlightedLineSet = new Set<number>();
91
+ for (const segment of normalizedSpec.split(",")) {
92
+ if (/^\d+$/.test(segment)) {
93
+ const lineNumber = Number.parseInt(segment, 10);
94
+ if (lineNumber >= 1 && lineNumber <= lineCount) {
95
+ highlightedLineSet.add(lineNumber);
96
+ }
97
+ continue;
98
+ }
99
+
100
+ const rangeMatch = segment.match(/^(\d+)-(\d+)$/);
101
+ if (!rangeMatch) continue;
102
+
103
+ const start = Number.parseInt(rangeMatch[1], 10);
104
+ const end = Number.parseInt(rangeMatch[2], 10);
105
+ if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
106
+
107
+ const rangeStart = Math.max(1, Math.min(start, end));
108
+ const rangeEnd = Math.min(lineCount, Math.max(start, end));
109
+ for (let lineNumber = rangeStart; lineNumber <= rangeEnd; lineNumber += 1) {
110
+ highlightedLineSet.add(lineNumber);
111
+ }
112
+ }
113
+
114
+ return highlightedLineSet;
115
+ }
116
+
117
+ function buildTokenStyle(token: {
118
+ color?: string;
119
+ bgColor?: string;
120
+ fontStyle?: number;
121
+ htmlStyle?: Record<string, string>;
122
+ }): string | undefined {
123
+ const styleSegments: string[] = [];
124
+
125
+ if (token.color) styleSegments.push(`color:${token.color}`);
126
+ if (token.bgColor) styleSegments.push(`background-color:${token.bgColor}`);
127
+
128
+ const fontStyle = typeof token.fontStyle === "number" ? token.fontStyle : 0;
129
+ if ((fontStyle & 1) === 1) styleSegments.push("font-style:italic");
130
+ if ((fontStyle & 2) === 2) styleSegments.push("font-weight:600");
131
+ if ((fontStyle & 4) === 4) styleSegments.push("text-decoration:underline");
132
+
133
+ if (token.htmlStyle && typeof token.htmlStyle === "object") {
134
+ for (const [property, value] of Object.entries(token.htmlStyle)) {
135
+ styleSegments.push(`${property}:${value}`);
136
+ }
137
+ }
138
+
139
+ if (!styleSegments.length) return undefined;
140
+ return styleSegments.join(";");
141
+ }
142
+
143
+ function escapeHtml(value: string): string {
144
+ return value
145
+ .replaceAll("&", "&amp;")
146
+ .replaceAll("<", "&lt;")
147
+ .replaceAll(">", "&gt;");
148
+ }
149
+
150
+ function escapeAttribute(value: string): string {
151
+ return escapeHtml(value).replaceAll('"', "&quot;");
152
+ }
153
+
154
+ const normalizedLanguage = normalizeCodeLanguageValue(language);
155
+ const parsedInCodeGroup = toBoolean(inCodeGroup, false);
156
+ const parsedShowFilename = parsedInCodeGroup
157
+ ? true
158
+ : toBoolean(showFilename, false);
159
+ const parsedShowLineNumbers = toBoolean(showLineNumbers, false);
160
+ const parsedHideLanguageIcon = toBoolean(hideLanguageIcon, false);
161
+ const shouldRenderLanguageIcon = !parsedHideLanguageIcon;
162
+
163
+ const trimmedFilename = filename.trim();
164
+ const displayFilename =
165
+ trimmedFilename.length > 0
166
+ ? trimmedFilename
167
+ : buildDefaultCodeFileName(normalizedLanguage);
168
+
169
+ const { lines: tokenLines } = await getCodeLineTokens({
170
+ code: raw,
171
+ language: normalizedLanguage,
172
+ });
173
+
174
+ const rawLines = raw.split("\n");
175
+ const normalizedRawLines = rawLines.length > 0 ? rawLines : [""];
176
+ const lineCount = Math.max(1, normalizedRawLines.length, tokenLines.length);
177
+ const normalizedTokenLines = Array.from(
178
+ { length: lineCount },
179
+ (_, lineIndex) => {
180
+ return tokenLines[lineIndex] ?? [];
181
+ },
182
+ );
183
+
184
+ const highlightSet = parseHighlightedLineNumbers(highlightedLines, lineCount);
185
+ const encodedRaw = encodeURIComponent(raw);
186
+ const collapsedLinesValue = collapsedLines.trim();
187
+ const renderedCodeLinesHtml = normalizedTokenLines
188
+ .map((lineTokens, lineIndex) => {
189
+ const lineNumber = lineIndex + 1;
190
+ const isHighlighted = highlightSet.has(lineNumber);
191
+ const fallbackLineContent = normalizedRawLines[lineIndex] ?? "";
192
+
193
+ const tokenHtml =
194
+ lineTokens.length > 0
195
+ ? lineTokens
196
+ .map((token) => {
197
+ const tokenStyle = buildTokenStyle(token);
198
+ const tokenStyleAttribute = tokenStyle
199
+ ? ` style="${escapeAttribute(tokenStyle)}"`
200
+ : "";
201
+ return `<span${tokenStyleAttribute}>${escapeHtml(token.content)}</span>`;
202
+ })
203
+ .join("")
204
+ : fallbackLineContent.length > 0
205
+ ? escapeHtml(fallbackLineContent)
206
+ : "&nbsp;";
207
+
208
+ const lineNumberHtml = parsedShowLineNumbers
209
+ ? `<span class="w-10 shrink-0 select-none pl-4 pr-3 text-right tabular-nums text-neutral-400">${lineNumber}</span>`
210
+ : "";
211
+
212
+ const lineContentClass = parsedShowLineNumbers
213
+ ? "flex-1 whitespace-pre pr-4"
214
+ : "flex-1 whitespace-pre pr-4 pl-4";
215
+ const lineClass = isHighlighted
216
+ ? "flex min-w-full bg-neutral-100/80"
217
+ : "flex min-w-full";
218
+
219
+ return `<span class="${lineClass}">${lineNumberHtml}<span class="${lineContentClass}">${tokenHtml}</span></span>`;
220
+ })
221
+ .join("");
222
+ ---
223
+
224
+ <div
225
+ class:list={[
226
+ "group/prose-code not-prose relative w-full max-w-full min-w-0",
227
+ parsedInCodeGroup ? "my-0" : "my-6",
228
+ ]}
229
+ data-rd-code-block-root="true"
230
+ data-rd-collapsed-lines={collapsedLinesValue}
231
+ data-rd-copy-content={encodedRaw}
232
+ data-rd-code-group-item={parsedInCodeGroup ? "true" : undefined}
233
+ data-rd-tab-filename={parsedInCodeGroup ? displayFilename : undefined}
234
+ data-rd-tab-language={parsedInCodeGroup ? normalizedLanguage : undefined}
235
+ data-rd-tab-hide-icon={parsedInCodeGroup && parsedHideLanguageIcon
236
+ ? "true"
237
+ : undefined}
238
+ >
239
+ <div
240
+ class:list={[
241
+ "w-full max-w-full min-w-0 overflow-hidden border border-neutral-200 bg-white shadow-xs",
242
+ parsedInCodeGroup ? "rounded-t-none rounded-b-xl" : "rounded-xl",
243
+ ]}
244
+ >
245
+ {
246
+ !parsedInCodeGroup && parsedShowFilename ? (
247
+ <div class="flex items-center justify-between gap-2 border-b border-neutral-200 bg-neutral-50 inset-shadow-sm inset-shadow-neutral-100/80">
248
+ <div class="min-w-0 flex-1">
249
+ <div class="relative h-9 w-fit max-w-full rounded-tl-xl bg-white">
250
+ <div class="absolute inset-x-0 -bottom-px h-px bg-white" />
251
+ <CodeTabEdge className="pointer-events-none absolute -top-px left-full z-10 h-[calc(100%+2px)]" />
252
+ <div class="relative z-20 inline-flex h-9 max-w-full items-center gap-2 pl-5 py-1.5 text-xs font-medium text-neutral-700">
253
+ {shouldRenderLanguageIcon ? (
254
+ <CodeLanguageIcon
255
+ language={normalizedLanguage}
256
+ fileName={displayFilename}
257
+ className="size-3.5 shrink-0 self-center rounded-[4px]"
258
+ />
259
+ ) : null}
260
+ <span class="truncate leading-none">{displayFilename}</span>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ <div class="relative h-9 w-5 shrink-0 rounded-tr-xl bg-white">
265
+ <div class="absolute inset-x-0 -bottom-px h-px bg-white" />
266
+ <CodeTabEdge className="pointer-events-none absolute -top-px right-full z-10 h-[calc(100%+2px)] rotate-y-180" />
267
+ <button
268
+ type="button"
269
+ 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"
270
+ data-rd-copy-trigger="true"
271
+ data-rd-copy-content={encodedRaw}
272
+ aria-label="Copy code"
273
+ >
274
+ <Icon
275
+ name="lucide:copy"
276
+ class="size-3.5 origin-center scale-100 rotate-0 opacity-100 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
277
+ data-rd-copy-icon
278
+ aria-hidden="true"
279
+ />
280
+ <Icon
281
+ name="lucide:check"
282
+ class="absolute size-3.5 stroke-3 origin-center scale-25 rotate-6 opacity-0 text-green-700/80 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
283
+ data-rd-copy-check
284
+ aria-hidden="true"
285
+ />
286
+ </button>
287
+ </div>
288
+ </div>
289
+ ) : null
290
+ }
291
+
292
+ <div class="relative min-w-0">
293
+ {
294
+ !parsedInCodeGroup && !parsedShowFilename ? (
295
+ <div class="pointer-events-none absolute right-2 top-2 z-20">
296
+ <button
297
+ type="button"
298
+ class="pointer-events-auto inline-flex size-7 appearance-none items-center justify-center rounded-md border border-neutral-200/80 text-neutral-500/80 bg-white/80 backdrop-blur-xs 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"
299
+ data-rd-copy-trigger="true"
300
+ data-rd-copy-content={encodedRaw}
301
+ aria-label="Copy code"
302
+ >
303
+ <Icon
304
+ name="lucide:copy"
305
+ class="size-3.5 origin-center scale-100 rotate-0 opacity-100 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
306
+ data-rd-copy-icon
307
+ aria-hidden="true"
308
+ />
309
+ <Icon
310
+ name="lucide:check"
311
+ class="absolute size-3.5 stroke-3 origin-center scale-25 rotate-6 opacity-0 text-green-700/80 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
312
+ data-rd-copy-check
313
+ aria-hidden="true"
314
+ />
315
+ </button>
316
+ </div>
317
+ ) : null
318
+ }
319
+
320
+ {
321
+ parsedInCodeGroup ? (
322
+ <template data-rd-code-group-tab-icon>
323
+ {shouldRenderLanguageIcon ? (
324
+ <CodeLanguageIcon
325
+ language={normalizedLanguage}
326
+ fileName={displayFilename}
327
+ className="size-3.5 shrink-0 self-center rounded-[4px]"
328
+ />
329
+ ) : null}
330
+ </template>
331
+ ) : null
332
+ }
333
+
334
+ <div
335
+ data-rd-code-scroll-area
336
+ class="relative overflow-x-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"
337
+ >
338
+ <pre
339
+ class="relative m-0 min-w-full bg-white p-0 text-[13px] leading-6"><code class="block min-w-full py-2.5 font-mono text-neutral-800"><Fragment set:html={renderedCodeLinesHtml} /></code></pre>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </div>
344
+
345
+ <script is:inline>
346
+ (() => {
347
+ const script = document.currentScript;
348
+ if (!(script instanceof HTMLScriptElement)) return;
349
+
350
+ const root = script.previousElementSibling;
351
+ if (!(root instanceof HTMLElement)) return;
352
+
353
+ const copyButtons = root.querySelectorAll("[data-rd-copy-trigger='true']");
354
+ if (copyButtons.length === 0) return;
355
+
356
+ const setCopiedState = (button, copied) => {
357
+ const copyIcon = button.querySelector("[data-rd-copy-icon]");
358
+ const checkIcon = button.querySelector("[data-rd-copy-check]");
359
+
360
+ if (!copyIcon || !checkIcon) return;
361
+
362
+ if (copied) {
363
+ copyIcon.classList.add("scale-50", "opacity-0", "-rotate-6");
364
+ copyIcon.classList.remove("scale-100", "opacity-100", "rotate-0");
365
+
366
+ checkIcon.classList.remove("scale-50", "opacity-0", "rotate-6");
367
+ checkIcon.classList.add("scale-110", "opacity-100", "rotate-0");
368
+ return;
369
+ }
370
+
371
+ copyIcon.classList.remove("scale-50", "opacity-0", "-rotate-6");
372
+ copyIcon.classList.add("scale-100", "opacity-100", "rotate-0");
373
+
374
+ checkIcon.classList.remove("scale-110", "opacity-100", "rotate-0");
375
+ checkIcon.classList.add("scale-50", "opacity-0", "rotate-6");
376
+ };
377
+
378
+ copyButtons.forEach((button) => {
379
+ let timeoutId = null;
380
+ button.addEventListener("click", async () => {
381
+ const encodedCopyValue =
382
+ button.getAttribute("data-rd-copy-content") ?? "";
383
+ const copyValue = decodeURIComponent(encodedCopyValue);
384
+
385
+ try {
386
+ await navigator.clipboard.writeText(copyValue);
387
+ setCopiedState(button, true);
388
+
389
+ if (timeoutId) window.clearTimeout(timeoutId);
390
+ timeoutId = window.setTimeout(() => {
391
+ setCopiedState(button, false);
392
+ timeoutId = null;
393
+ }, 1200);
394
+ } catch {
395
+ setCopiedState(button, false);
396
+ }
397
+ });
398
+ });
399
+ })();
400
+ </script>
@@ -0,0 +1,225 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import CodeTabEdge from "../ui/CodeTabEdge.astro";
4
+ ---
5
+
6
+ <div
7
+ class="group/prose-code-group not-prose relative my-6 w-full max-w-full min-w-0"
8
+ data-rd-code-group-root="true"
9
+ >
10
+ <div
11
+ class="relative z-10 overflow-visible rounded-t-xl border border-b-0 border-neutral-200 bg-neutral-50 inset-shadow-sm inset-shadow-neutral-100/80"
12
+ >
13
+ <div class="flex min-w-0 items-end justify-between gap-2">
14
+ <div class="min-w-0 flex-1 overflow-hidden rounded-t-xl">
15
+ <div
16
+ data-rd-code-group-tabs
17
+ class="relative flex min-w-0 items-end gap-1 overflow-x-auto pl-1 pr-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
18
+ >
19
+ <div
20
+ data-rd-code-group-pill
21
+ aria-hidden="true"
22
+ class="pointer-events-none absolute top-1/2 z-0 h-[28px] -translate-y-1/2 rounded-[9px] border-[0.5px] border-neutral-200 bg-white shadow-xs opacity-0 transition-[left,width,opacity] duration-200 ease-out"
23
+ >
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="relative h-9 w-5 shrink-0 rounded-tr-xl bg-white">
29
+ <div class="absolute inset-x-0 -bottom-px h-px bg-white"></div>
30
+ <CodeTabEdge
31
+ className="pointer-events-none absolute -top-px right-full z-10 h-[calc(100%+2px)] rotate-y-180"
32
+ />
33
+ <button
34
+ type="button"
35
+ 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"
36
+ data-rd-copy-trigger="true"
37
+ aria-label="Copy code"
38
+ >
39
+ <Icon
40
+ name="lucide:copy"
41
+ class="size-3.5 origin-center scale-100 rotate-0 opacity-100 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
42
+ data-rd-copy-icon
43
+ aria-hidden="true"
44
+ />
45
+ <Icon
46
+ name="lucide:check"
47
+ class="absolute size-3.5 stroke-3 origin-center scale-25 rotate-6 opacity-0 text-green-700/80 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
48
+ data-rd-copy-check
49
+ aria-hidden="true"
50
+ />
51
+ </button>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div data-rd-code-group-content class="min-w-0">
57
+ <slot />
58
+ </div>
59
+ </div>
60
+
61
+ <script is:inline>
62
+ (() => {
63
+ const script = document.currentScript;
64
+ if (!(script instanceof HTMLScriptElement)) return;
65
+
66
+ const root = script.previousElementSibling;
67
+ if (!(root instanceof HTMLElement)) return;
68
+
69
+ const contentElement = root.querySelector("[data-rd-code-group-content]");
70
+ const tabsElement = root.querySelector("[data-rd-code-group-tabs]");
71
+ const pillElement = root.querySelector("[data-rd-code-group-pill]");
72
+ const copyButton = root.querySelector("[data-rd-copy-trigger='true']");
73
+
74
+ if (!contentElement || !tabsElement || !pillElement) return;
75
+
76
+ const codeItems = Array.from(
77
+ contentElement.querySelectorAll("[data-rd-code-group-item='true']"),
78
+ );
79
+ if (codeItems.length === 0) return;
80
+
81
+ let activeIndex = 0;
82
+
83
+ const setCopiedState = (button, copied) => {
84
+ const copyIcon = button.querySelector("[data-rd-copy-icon]");
85
+ const checkIcon = button.querySelector("[data-rd-copy-check]");
86
+ if (!copyIcon || !checkIcon) return;
87
+
88
+ if (copied) {
89
+ copyIcon.classList.add("scale-50", "opacity-0", "-rotate-6");
90
+ copyIcon.classList.remove("scale-100", "opacity-100", "rotate-0");
91
+
92
+ checkIcon.classList.remove("scale-50", "opacity-0", "rotate-6");
93
+ checkIcon.classList.add("scale-110", "opacity-100", "rotate-0");
94
+ return;
95
+ }
96
+
97
+ copyIcon.classList.remove("scale-50", "opacity-0", "-rotate-6");
98
+ copyIcon.classList.add("scale-100", "opacity-100", "rotate-0");
99
+
100
+ checkIcon.classList.remove("scale-110", "opacity-100", "rotate-0");
101
+ checkIcon.classList.add("scale-50", "opacity-0", "rotate-6");
102
+ };
103
+
104
+ const tabs = codeItems.map((itemElement, index) => {
105
+ const filename =
106
+ itemElement.getAttribute("data-rd-tab-filename") ??
107
+ `file-name-${index + 1}.txt`;
108
+ const tabIconTemplate = itemElement.querySelector(
109
+ "template[data-rd-code-group-tab-icon]",
110
+ );
111
+
112
+ const tabWrapper = document.createElement("div");
113
+ tabWrapper.className = "relative z-10 text-xs font-medium";
114
+
115
+ const tabButton = document.createElement("button");
116
+ tabButton.type = "button";
117
+ tabButton.className =
118
+ "relative inline-flex h-9 items-center gap-2 border-0 bg-transparent px-3 py-1.5 text-xs font-medium text-neutral-600 transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer";
119
+ tabButton.setAttribute("aria-label", filename);
120
+ tabButton.setAttribute("data-rd-code-group-tab", String(index));
121
+
122
+ const iconContainer = document.createElement("span");
123
+ iconContainer.className =
124
+ "pointer-events-none inline-flex shrink-0 items-center rounded-[4px] transition-opacity duration-150";
125
+
126
+ if (tabIconTemplate && tabIconTemplate.innerHTML.trim().length > 0) {
127
+ iconContainer.innerHTML = tabIconTemplate.innerHTML;
128
+ } else {
129
+ iconContainer.classList.add("hidden");
130
+ }
131
+
132
+ const labelElement = document.createElement("span");
133
+ labelElement.className = "whitespace-pre leading-none";
134
+ labelElement.textContent = filename;
135
+
136
+ tabButton.appendChild(iconContainer);
137
+ tabButton.appendChild(labelElement);
138
+ tabWrapper.appendChild(tabButton);
139
+ tabsElement.appendChild(tabWrapper);
140
+
141
+ return {
142
+ itemElement,
143
+ tabWrapper,
144
+ tabButton,
145
+ iconContainer,
146
+ };
147
+ });
148
+
149
+ const syncPill = () => {
150
+ const activeTab = tabs[activeIndex]?.tabWrapper;
151
+ if (!activeTab) {
152
+ pillElement.classList.add("opacity-0");
153
+ pillElement.classList.remove("opacity-100");
154
+ return;
155
+ }
156
+
157
+ const activeRect = activeTab.getBoundingClientRect();
158
+ const tabsRect = tabsElement.getBoundingClientRect();
159
+ const left = activeRect.left - tabsRect.left + tabsElement.scrollLeft;
160
+ const width = activeRect.width;
161
+ pillElement.style.left = `${left}px`;
162
+ pillElement.style.width = `${width}px`;
163
+ pillElement.classList.remove("opacity-0");
164
+ pillElement.classList.add("opacity-100");
165
+ };
166
+
167
+ const syncActiveTab = () => {
168
+ tabs.forEach(({ itemElement, tabButton, iconContainer }, index) => {
169
+ const isActive = index === activeIndex;
170
+ itemElement.style.display = isActive ? "" : "none";
171
+ tabButton.classList.toggle("text-neutral-900", isActive);
172
+ tabButton.classList.toggle("text-neutral-600", !isActive);
173
+
174
+ if (iconContainer.classList.contains("hidden")) return;
175
+ iconContainer.classList.toggle("opacity-100", isActive);
176
+ iconContainer.classList.toggle("opacity-80", !isActive);
177
+ });
178
+ syncPill();
179
+ };
180
+
181
+ tabs.forEach(({ tabButton }, index) => {
182
+ tabButton.addEventListener("click", () => {
183
+ if (activeIndex === index) return;
184
+ activeIndex = index;
185
+ syncActiveTab();
186
+ });
187
+ });
188
+
189
+ if (copyButton) {
190
+ let timeoutId = null;
191
+ copyButton.addEventListener("click", async () => {
192
+ const activeItem = tabs[activeIndex]?.itemElement;
193
+ const encodedCopyValue =
194
+ activeItem?.getAttribute("data-rd-copy-content") ?? "";
195
+ const copyValue = decodeURIComponent(encodedCopyValue);
196
+
197
+ try {
198
+ await navigator.clipboard.writeText(copyValue);
199
+ setCopiedState(copyButton, true);
200
+
201
+ if (timeoutId) window.clearTimeout(timeoutId);
202
+ timeoutId = window.setTimeout(() => {
203
+ setCopiedState(copyButton, false);
204
+ timeoutId = null;
205
+ }, 1200);
206
+ } catch {
207
+ setCopiedState(copyButton, false);
208
+ }
209
+ });
210
+ }
211
+
212
+ const resizeObserver =
213
+ typeof ResizeObserver !== "undefined"
214
+ ? new ResizeObserver(() => syncPill())
215
+ : null;
216
+ if (resizeObserver) {
217
+ resizeObserver.observe(tabsElement);
218
+ }
219
+
220
+ tabsElement.addEventListener("scroll", syncPill, { passive: true });
221
+ window.addEventListener("resize", syncPill);
222
+ syncActiveTab();
223
+ requestAnimationFrame(syncPill);
224
+ })();
225
+ </script>