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,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>
|