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,480 @@
1
+ ---
2
+ import type { MarkdownHeading } from "astro";
3
+
4
+ interface Props {
5
+ headings: MarkdownHeading[];
6
+ }
7
+
8
+ interface GroupedHeading extends MarkdownHeading {
9
+ sub: MarkdownHeading[];
10
+ }
11
+
12
+ const { headings } = Astro.props;
13
+
14
+ // Group depth 3 headings under their preceding depth 2 heading
15
+ const groupedHeadings: GroupedHeading[] = [];
16
+
17
+ for (const heading of headings) {
18
+ if (heading.depth === 2) {
19
+ groupedHeadings.push({ ...heading, sub: [] });
20
+ } else if (heading.depth === 3) {
21
+ // If no depth 2 exists yet, create a blank parent
22
+ if (groupedHeadings.length === 0) {
23
+ groupedHeadings.push({ depth: 2, text: "", slug: "", sub: [] });
24
+ }
25
+ // Add depth 3 to the last depth 2's sub array
26
+ groupedHeadings[groupedHeadings.length - 1].sub.push(heading);
27
+ }
28
+ }
29
+ ---
30
+
31
+ <nav
32
+ class="sticky top-16 text-[14px]/5 py-16 -mt-16"
33
+ aria-label="Table of Contents"
34
+ data-slugs={JSON.stringify(headings.map((h) => h.slug))}
35
+ >
36
+ <div class="text-sm font-medium mb-3">On this page</div>
37
+ <div class="flex text-neutral-600/90">
38
+ <svg viewBox="0 0 22 100" width="22px" height="100" class="shrink-0">
39
+ <path
40
+ id="toc-bg-path"
41
+ d=""
42
+ stroke-width="1.2"
43
+ vector-effect="non-scaling-stroke"
44
+ class="stroke-neutral-200"
45
+ fill="none"
46
+ stroke-linecap="round"
47
+ stroke-linejoin="round"></path>
48
+ <path
49
+ id="toc-fg-path"
50
+ d=""
51
+ stroke-width="1.4"
52
+ vector-effect="non-scaling-stroke"
53
+ class="stroke-blue-700/60 transition-[stroke-dasharray] duration-200"
54
+ fill="none"
55
+ stroke-linecap="round"
56
+ stroke-linejoin="round"></path>
57
+ <g id="toc-circles"></g>
58
+ </svg>
59
+ <ol id="toc-list">
60
+ {
61
+ groupedHeadings.map((heading) => (
62
+ <li class="mt-3 first:mt-0">
63
+ {heading.text && (
64
+ <a
65
+ href={`#${heading.slug}`}
66
+ class="block transition duration-200 hover:text-neutral-900"
67
+ data-slug={heading.slug}
68
+ >
69
+ {heading.text}
70
+ </a>
71
+ )}
72
+ {heading.sub.length > 0 && (
73
+ <ol class="ml-4">
74
+ {heading.sub.map((subHeading) => (
75
+ <li class:list={["", heading.text ? "mt-3" : "mt-0"]}>
76
+ <a
77
+ href={`#${subHeading.slug}`}
78
+ class="block transition duration-200 hover:text-neutral-900"
79
+ data-slug={subHeading.slug}
80
+ >
81
+ {subHeading.text}
82
+ </a>
83
+ </li>
84
+ ))}
85
+ </ol>
86
+ )}
87
+ </li>
88
+ ))
89
+ }
90
+ </ol>
91
+ </div>
92
+
93
+ <script>
94
+ function initTableOfContents() {
95
+ const nav = document.querySelector('nav[aria-label="Table of Contents"]');
96
+ const svg = nav?.querySelector("svg");
97
+ const ol = nav?.querySelector("#toc-list");
98
+
99
+ if (!svg || !ol) return;
100
+
101
+ // Check if TOC is visible (not hidden by responsive class)
102
+ const firstLink = ol.querySelector("a");
103
+ if (firstLink && firstLink.offsetHeight === 0) {
104
+ return; // TOC is hidden, skip initialization
105
+ }
106
+
107
+ // Build path and track link positions for progress indicator
108
+ let path = "";
109
+ let currentY = 0;
110
+ const gap = 12; // mt-3 = 12px
111
+ const linkPositions: Map<string, { pathLengthAtMid: number }> = new Map();
112
+
113
+ // First pass: collect all items with their positions
114
+ interface ItemInfo {
115
+ x: number;
116
+ height: number;
117
+ slug?: string;
118
+ yStart: number;
119
+ }
120
+ const items: ItemInfo[] = [];
121
+
122
+ // Iterate through top-level items to collect data
123
+ ol.querySelectorAll(":scope > li").forEach((li) => {
124
+ const topLink = li.querySelector(
125
+ ":scope > a"
126
+ ) as HTMLAnchorElement | null;
127
+ const subOl = li.querySelector(":scope > ol");
128
+
129
+ // Handle depth-2 heading (only if it has text/link)
130
+ if (topLink) {
131
+ const height = topLink.offsetHeight;
132
+ if (items.length > 0) currentY += gap;
133
+ items.push({
134
+ x: 0.5,
135
+ height,
136
+ slug: topLink.dataset.slug,
137
+ yStart: currentY,
138
+ });
139
+ currentY += height;
140
+ }
141
+
142
+ // Handle depth-3 subheadings
143
+ if (subOl) {
144
+ subOl.querySelectorAll(":scope > li").forEach((subLi) => {
145
+ const subLink = subLi.querySelector(
146
+ "a"
147
+ ) as HTMLAnchorElement | null;
148
+ if (!subLink) return;
149
+
150
+ const height = subLink.offsetHeight;
151
+ const hasMt0 = subLi.classList.contains("mt-0");
152
+
153
+ if (items.length > 0 && !hasMt0) currentY += gap;
154
+ items.push({
155
+ x: 12,
156
+ height,
157
+ slug: subLink.dataset.slug,
158
+ yStart: currentY,
159
+ });
160
+ currentY += height;
161
+ });
162
+ }
163
+ });
164
+
165
+ // Second pass: build path with midpoint start/end, tracking cumulative path length
166
+ let pathLength = 0;
167
+ let prevX: number | null = null;
168
+ let prevY: number | null = null;
169
+
170
+ items.forEach((item, index) => {
171
+ const isFirst = index === 0;
172
+ const isLast = index === items.length - 1;
173
+ const midY = item.yStart + item.height / 2;
174
+
175
+ if (isFirst) {
176
+ // Start at midpoint of first item
177
+ path = `M ${item.x} ${midY} `;
178
+ prevX = item.x;
179
+ prevY = midY;
180
+
181
+ // Path length at first item's midpoint is 0
182
+ if (item.slug) {
183
+ linkPositions.set(item.slug, { pathLengthAtMid: 0 });
184
+ }
185
+
186
+ if (!isLast) {
187
+ // Draw from midpoint to bottom
188
+ path += `L ${item.x} ${item.yStart + item.height} `;
189
+ pathLength += item.height / 2; // vertical segment
190
+ prevY = item.yStart + item.height;
191
+ }
192
+ } else {
193
+ // Segment from previous position to this item's top
194
+ const dx = item.x - prevX!;
195
+ const dy = item.yStart - prevY!;
196
+ pathLength += Math.sqrt(dx * dx + dy * dy);
197
+ path += `L ${item.x} ${item.yStart} `;
198
+ prevX = item.x;
199
+ prevY = item.yStart;
200
+
201
+ if (isLast) {
202
+ // Draw from top to midpoint
203
+ const midLength = item.height / 2;
204
+ pathLength += midLength;
205
+ path += `L ${item.x} ${midY} `;
206
+ prevY = midY;
207
+
208
+ if (item.slug) {
209
+ linkPositions.set(item.slug, { pathLengthAtMid: pathLength });
210
+ }
211
+ } else {
212
+ // Middle item: path length at midpoint is current + half height
213
+ const pathLengthAtMid = pathLength + item.height / 2;
214
+
215
+ if (item.slug) {
216
+ linkPositions.set(item.slug, {
217
+ pathLengthAtMid: pathLengthAtMid,
218
+ });
219
+ }
220
+
221
+ // Draw full height
222
+ path += `L ${item.x} ${item.yStart + item.height} `;
223
+ pathLength += item.height;
224
+ prevY = item.yStart + item.height;
225
+ }
226
+ }
227
+ });
228
+
229
+ // Create circles at each item's midpoint
230
+ const circlesGroup = svg.querySelector("#toc-circles");
231
+ if (circlesGroup) {
232
+ // Clear existing circles before creating new ones
233
+ circlesGroup.innerHTML = "";
234
+ items.forEach((item) => {
235
+ const midY = item.yStart + item.height / 2;
236
+ const circle = document.createElementNS(
237
+ "http://www.w3.org/2000/svg",
238
+ "circle"
239
+ );
240
+ circle.setAttribute("cx", String(item.x));
241
+ circle.setAttribute("cy", String(midY));
242
+ circle.setAttribute("r", "2.5");
243
+ circle.setAttribute("fill", "currentColor");
244
+ circle.classList.add(
245
+ "fill-neutral-300",
246
+ "transition-all",
247
+ "duration-200"
248
+ );
249
+ if (item.slug) {
250
+ circle.dataset.slug = item.slug;
251
+ }
252
+ circlesGroup.appendChild(circle);
253
+ });
254
+ }
255
+
256
+ const bgPath = svg.querySelector("#toc-bg-path");
257
+ const fgPath = svg.querySelector("#toc-fg-path") as SVGPathElement | null;
258
+
259
+ bgPath?.setAttribute("d", path);
260
+ fgPath?.setAttribute("d", path);
261
+
262
+ svg.setAttribute("height", String(currentY));
263
+ svg.setAttribute("viewBox", `-3 0 22 ${currentY}`);
264
+
265
+ // Get total path length for calculations
266
+ let totalLength = 0;
267
+ if (fgPath) {
268
+ totalLength = fgPath.getTotalLength();
269
+ // Disable transition during initial setup to prevent flash
270
+ fgPath.style.transition = "none";
271
+ // Initially hide the highlight (use same pattern as "0 visible" state)
272
+ fgPath.setAttribute(
273
+ "stroke-dasharray",
274
+ `0 ${totalLength} 0 ${totalLength}`
275
+ );
276
+ fgPath.setAttribute("stroke-dashoffset", "0");
277
+ }
278
+
279
+ // Track all visible headings
280
+ const visibleSlugs = new Set<string>();
281
+
282
+ function updateHighlight() {
283
+ // Find the range of visible headings by path length
284
+ let minPathLength = Infinity;
285
+ let maxPathLength = -Infinity;
286
+
287
+ visibleSlugs.forEach((slug) => {
288
+ const pos = linkPositions.get(slug);
289
+ if (pos) {
290
+ minPathLength = Math.min(minPathLength, pos.pathLengthAtMid);
291
+ maxPathLength = Math.max(maxPathLength, pos.pathLengthAtMid);
292
+ }
293
+ });
294
+
295
+ // Update the SVG highlight
296
+ if (fgPath) {
297
+ // Only show line if 2+ items visible (line connects between circles)
298
+ if (
299
+ visibleSlugs.size >= 2 &&
300
+ minPathLength !== Infinity &&
301
+ minPathLength !== maxPathLength
302
+ ) {
303
+ // Calculate the visible segment length along the path
304
+ const visibleLength = maxPathLength - minPathLength;
305
+ // Use dasharray pattern: 0 (no initial dash), gap to start, visible length, large gap to hide rest
306
+ const dasharray = `0 ${minPathLength} ${visibleLength} ${totalLength}`;
307
+ fgPath.setAttribute("stroke-dasharray", dasharray);
308
+ fgPath.setAttribute("stroke-dashoffset", "0");
309
+ } else if (visibleSlugs.size === 1 && minPathLength !== Infinity) {
310
+ // 1 visible heading: position gap at this item, ready to grow down
311
+ fgPath.setAttribute(
312
+ "stroke-dasharray",
313
+ `0 ${minPathLength} 0 ${totalLength}`
314
+ );
315
+ fgPath.setAttribute("stroke-dashoffset", "0");
316
+ } else {
317
+ // 0 visible headings: fully hidden
318
+ fgPath.setAttribute(
319
+ "stroke-dasharray",
320
+ `0 ${totalLength} 0 ${totalLength}`
321
+ );
322
+ fgPath.setAttribute("stroke-dashoffset", "0");
323
+ }
324
+ }
325
+
326
+ // Update circle colors based on visibility
327
+ const circles = circlesGroup?.querySelectorAll("circle");
328
+ circles?.forEach((circle) => {
329
+ const slug = (circle as SVGCircleElement).dataset.slug;
330
+ if (slug && visibleSlugs.has(slug)) {
331
+ circle.classList.remove("fill-neutral-300");
332
+ circle.classList.add("fill-blue-700", "delay-100");
333
+ circle.setAttribute("r", "3");
334
+ } else {
335
+ circle.classList.remove("fill-blue-700", "delay-100");
336
+ circle.classList.add("fill-neutral-300");
337
+ circle.setAttribute("r", "2.5");
338
+ }
339
+ });
340
+
341
+ // Update link text colors based on visibility
342
+ const tocLinks = ol?.querySelectorAll("a[data-slug]");
343
+ tocLinks?.forEach((link) => {
344
+ const slug = (link as HTMLAnchorElement).dataset.slug;
345
+ if (slug && visibleSlugs.has(slug)) {
346
+ link.classList.add("text-neutral-900");
347
+ } else {
348
+ link.classList.remove("text-neutral-900");
349
+ }
350
+ });
351
+ }
352
+
353
+ // Intersection Observer to track headings in view
354
+ const slugs: string[] = JSON.parse(
355
+ (nav as HTMLElement).dataset.slugs || "[]"
356
+ );
357
+ const headings = slugs
358
+ .map((slug) => document.getElementById(slug))
359
+ .filter((el): el is HTMLElement => el !== null);
360
+
361
+ const headerOffset = 112;
362
+
363
+ // Calculate active headings based on scroll position
364
+ function getActiveHeadings(): Set<string> {
365
+ const active = new Set<string>();
366
+
367
+ // Add all headings currently in viewport
368
+
369
+ for (const heading of headings) {
370
+ const rect = heading.getBoundingClientRect();
371
+ if (rect.top >= headerOffset && rect.top < window.innerHeight) {
372
+ active.add(heading.id);
373
+ }
374
+ }
375
+
376
+ // Add the "current section" - last heading that scrolled above the header
377
+ // (user is still reading this section's content)
378
+ let currentSection: string | null = null;
379
+ for (const heading of headings) {
380
+ const rect = heading.getBoundingClientRect();
381
+ if (rect.top <= headerOffset) {
382
+ currentSection = heading.id;
383
+ } else {
384
+ break; // headings are in order, stop once we find one below
385
+ }
386
+ }
387
+ if (currentSection) {
388
+ active.add(currentSection);
389
+ }
390
+
391
+ return active;
392
+ }
393
+
394
+ const observer = new IntersectionObserver(
395
+ () => {
396
+ // Recalculate active headings whenever any heading crosses the threshold
397
+ const newVisible = getActiveHeadings();
398
+ visibleSlugs.clear();
399
+ newVisible.forEach((slug) => visibleSlugs.add(slug));
400
+ updateHighlight();
401
+ },
402
+ {
403
+ rootMargin: `-${headerOffset}px 0px 0px 0px`,
404
+ threshold: 0,
405
+ }
406
+ );
407
+
408
+ headings.forEach((heading) => observer.observe(heading));
409
+
410
+ // Set initial state
411
+ const initialVisible = getActiveHeadings();
412
+ initialVisible.forEach((slug) => visibleSlugs.add(slug));
413
+ updateHighlight();
414
+
415
+ // Re-enable transition after initial state is set
416
+ if (fgPath) {
417
+ // Force a reflow to apply the non-transitioned state first
418
+ fgPath.getBoundingClientRect();
419
+ fgPath.style.transition = "";
420
+ }
421
+
422
+ // Also listen to scroll to catch cases where no heading crosses the threshold
423
+ let scrollTimeout: ReturnType<typeof setTimeout> | null = null;
424
+ function handleScroll() {
425
+ // Throttle scroll updates
426
+ if (scrollTimeout) return;
427
+ scrollTimeout = setTimeout(() => {
428
+ scrollTimeout = null;
429
+ const newVisible = getActiveHeadings();
430
+ // Only update if the set has changed
431
+ const hasChanged =
432
+ newVisible.size !== visibleSlugs.size ||
433
+ [...newVisible].some((slug) => !visibleSlugs.has(slug));
434
+ if (hasChanged) {
435
+ visibleSlugs.clear();
436
+ newVisible.forEach((slug) => visibleSlugs.add(slug));
437
+ updateHighlight();
438
+ }
439
+ }, 50);
440
+ }
441
+
442
+ window.addEventListener("scroll", handleScroll, { passive: true });
443
+
444
+ // Cleanup on navigation
445
+ document.addEventListener("astro:before-swap", () => {
446
+ observer.disconnect();
447
+ window.removeEventListener("scroll", handleScroll);
448
+ if (scrollTimeout) clearTimeout(scrollTimeout);
449
+ });
450
+ }
451
+
452
+ // Run on initial load and after Astro navigation
453
+ initTableOfContents();
454
+ document.addEventListener("astro:after-swap", initTableOfContents);
455
+
456
+ // Re-initialize when TOC becomes visible (responsive breakpoint)
457
+ const tocNav = document.querySelector(
458
+ 'nav[aria-label="Table of Contents"]'
459
+ );
460
+ if (tocNav) {
461
+ let wasVisible = tocNav.getBoundingClientRect().width > 0;
462
+ const resizeObserver = new ResizeObserver((entries) => {
463
+ for (const entry of entries) {
464
+ const isVisible = entry.contentRect.width > 0;
465
+ // Only re-init when transitioning from hidden to visible
466
+ if (isVisible && !wasVisible) {
467
+ initTableOfContents();
468
+ }
469
+ wasVisible = isVisible;
470
+ }
471
+ });
472
+ resizeObserver.observe(tocNav);
473
+
474
+ // Cleanup on navigation
475
+ document.addEventListener("astro:before-swap", () => {
476
+ resizeObserver.disconnect();
477
+ });
478
+ }
479
+ </script>
480
+ </nav>
@@ -0,0 +1,84 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ ---
4
+
5
+ <div
6
+ x-data="{
7
+ theme: localStorage.getItem('theme') || 'system',
8
+ init() {
9
+ // 1. Initial positioning
10
+ this.$nextTick(() => this.updateMarker());
11
+
12
+ // Watch for changes to the 'theme' state
13
+ this.$watch('theme', val => {
14
+ localStorage.setItem('theme', val);
15
+ this.updateDOM();
16
+ this.$nextTick(() => this.updateMarker())
17
+ });
18
+
19
+ this.$watch('open', val => {
20
+ this.$nextTick(() => this.updateMarker())
21
+ });
22
+
23
+ // Listen for System OS changes (sunset/sunrise)
24
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
25
+ if (this.theme === 'system') this.updateDOM();
26
+ });
27
+ },
28
+ updateDOM() {
29
+ document.documentElement.classList.add('is-switching-theme')
30
+ const isDark = this.theme === 'dark' ||
31
+ (this.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
32
+
33
+ document.documentElement.classList.toggle('dark', isDark);
34
+ setTimeout(() => {
35
+ document.documentElement.classList.remove('is-switching-theme');
36
+ }, 200);
37
+ },
38
+ markerStyle: { left: null, width: null },
39
+ updateMarker() {
40
+ // Use the theme name as the ref key
41
+ const el = this.$refs[this.theme];
42
+ if (el) {
43
+ this.markerStyle = {
44
+ left: el.offsetLeft + 'px',
45
+ width: el.offsetWidth + 'px',
46
+ };
47
+ }
48
+ }
49
+ }"
50
+ class="relative flex gap-[3px] p-0.5 bg-neutral-100 dark:bg-neutral-800 rounded-full w-fit inset-shadow-sm text-neutral-500"
51
+ >
52
+ <div
53
+ class="anchor-pill absolute top-[3px] bottom-[3px] bg-white dark:bg-neutral-700 rounded-full shadow-sm ease-out z-0 flex items-center justify-center animate-[scaleIn_0.5s_ease-out]"
54
+ style="left: 3px;"
55
+ :style="markerStyle.width ? `left: ${markerStyle.left}; width: ${markerStyle.width}` : ''"
56
+ >
57
+ </div>
58
+ <button
59
+ x-ref="light"
60
+ @click="theme = 'light'"
61
+ :class="theme === 'light' ? 'text-neutral-800' : 'text-neutral-500'"
62
+ class="p-[5px] rounded-full text-sm font-medium transition-all cursor-pointer z-10"
63
+ >
64
+ <Icon name="lucide:sun-medium" class="size-[13px]" />
65
+ </button>
66
+
67
+ <button
68
+ x-ref="dark"
69
+ @click="theme = 'dark'"
70
+ :class="theme === 'dark' ? 'text-neutral-300' : 'text-neutral-500'"
71
+ class="p-[5px] rounded-full text-sm font-medium transition-all cursor-pointer z-10"
72
+ >
73
+ <Icon name="lucide:moon" class="size-[13px]" />
74
+ </button>
75
+
76
+ <button
77
+ x-ref="system"
78
+ @click="theme = 'system'"
79
+ :class="theme === 'system' ? 'text-neutral-800 dark:text-neutral-300' : 'text-neutral-500'"
80
+ class="p-[5px] rounded-full text-sm font-medium transition-all cursor-pointer z-10"
81
+ >
82
+ <Icon name="lucide:monitor" class="size-[13px]" />
83
+ </button>
84
+ </div>
@@ -0,0 +1,68 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import { methodColors } from "../../lib/utils";
4
+ import type { OpenApiRoute } from "../../lib/routes";
5
+
6
+ interface Props {
7
+ route: OpenApiRoute;
8
+ serverUrl?: string;
9
+ }
10
+
11
+ const { route, serverUrl } = Astro.props;
12
+ ---
13
+
14
+ <div
15
+ class="min-w-0 flex items-center gap-[3px] p-[2px] bg-neutral-100/80 inset-shadow-xs rounded-[14px] border border-neutral-200"
16
+ >
17
+ <div
18
+ class="min-w-0 flex-1 flex items-center p-[3px] border border-neutral-200 bg-white rounded-xl shadow-xs overflow-hidden"
19
+ >
20
+ <span
21
+ class:list={[
22
+ "shrink-0 inline-block px-1.5 ml-1 text-sm font-semibold rounded-md uppercase border",
23
+ methodColors[route.openApiMethod.toLowerCase()] || methodColors.get,
24
+ ]}
25
+ >
26
+ {route.openApiMethod}
27
+ </span>
28
+ <code
29
+ class="group flex-1 mx-2 h-[30px] flex items-center text-[13px] text-neutral-600 min-w-0 relative cursor-pointer"
30
+ @click="copyPath()"
31
+ x-data=`{
32
+ copied: false,
33
+ path: '${serverUrl ? serverUrl + route.openApiPath : route.openApiPath}',
34
+ async copyPath() {
35
+ try {
36
+ await navigator.clipboard.writeText(this.path);
37
+ this.copied = true;
38
+ setTimeout(() => {
39
+ this.copied = false;
40
+ }, 2000);
41
+ } catch (err) {
42
+ console.error('Failed to copy:', err);
43
+ }
44
+ }
45
+ }`
46
+ >
47
+ <span class="truncate min-w-0 flex-1">
48
+ {route.openApiPath}
49
+ </span>
50
+ <div
51
+ class="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1 text-[12px] px-1.5 py-px bg-white border border-neutral-200 rounded-md duration-200 opacity-0 scale-75 group-hover:scale-100 group-hover:opacity-100 group-hover:duration-200 group-hover:ease-out group-hover:delay-75"
52
+ >
53
+ <Icon
54
+ name="lucide:copy"
55
+ class="**:stroke-[2.4]"
56
+ x-bind:class="copied ? 'scale-0 opacity-0 delay-0 duration-100':'duration-150 delay-75'"
57
+ />
58
+ <Icon
59
+ name="lucide:check"
60
+ class="absolute text-green-700 duration-150 **:stroke-3"
61
+ x-bind:class="copied ? 'scale-100 opacity-100 delay-75':'scale-0 opacity-0'"
62
+ />
63
+ Copy
64
+ </div>
65
+ </code>
66
+ <slot />
67
+ </div>
68
+ </div>
@@ -0,0 +1,44 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ ---
4
+
5
+ <div x-data="{ open: false }" class="shrink-0 ml-auto">
6
+ <button
7
+ x-on:click="open = true"
8
+ 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 transition-all whitespace-nowrap cursor-pointer relative before:absolute before:inset-0 before:shadow-sm before:rounded-lg before:bg-transparent"
9
+ >
10
+ Try it
11
+ <Icon class="ml-2" name="lucide:square-mouse-pointer" />
12
+ </button>
13
+ <div
14
+ x-show="open"
15
+ style="display: none"
16
+ x-on:keydown.escape.prevent.stop="open = false"
17
+ role="dialog"
18
+ aria-modal="true"
19
+ x-id="['modal-title']"
20
+ :aria-labelledby="$id('modal-title')"
21
+ class="fixed inset-0 z-50 overflow-y-auto"
22
+ >
23
+ <div
24
+ x-show="open"
25
+ x-transition.opacity
26
+ class="fixed z-50 inset-0 bg-neutral-400/40 dark:bg-neutral-950/80 backdrop-blur-[2px]"
27
+ >
28
+ </div>
29
+ <div
30
+ x-show="open"
31
+ x-transition
32
+ x-on:click="open = false"
33
+ class="relative z-50 flex min-h-screen items-center justify-center p-2 xs:p-4"
34
+ >
35
+ <div
36
+ x-on:click.stop
37
+ x-trap.noscroll.inert="open"
38
+ class="relative max-w-7xl w-full rounded-xl bg-white p-4 sm:p-6 shadow-lg"
39
+ >
40
+ <slot />
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>