radiant-docs 0.1.41 → 0.1.42

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 (32) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +42 -40
  3. package/template/package-lock.json +7 -0
  4. package/template/package.json +1 -0
  5. package/template/src/components/Header.astro +150 -16
  6. package/template/src/components/MdxPage.astro +76 -22
  7. package/template/src/components/PagePagination.astro +44 -8
  8. package/template/src/components/Sidebar.astro +10 -1
  9. package/template/src/components/TableOfContents.astro +159 -53
  10. package/template/src/components/chat/AssistantDocsWidget.tsx +221 -8
  11. package/template/src/components/chat/AssistantEmbedPanel.tsx +1090 -104
  12. package/template/src/components/user/Accordion.astro +2 -2
  13. package/template/src/components/user/AccordionGroup.astro +1 -1
  14. package/template/src/components/user/Callout.astro +2 -2
  15. package/template/src/components/user/Card.astro +488 -0
  16. package/template/src/components/user/CardGradient.astro +964 -0
  17. package/template/src/components/user/CodeBlock.astro +1 -1
  18. package/template/src/components/user/CodeGroup.astro +1 -1
  19. package/template/src/components/user/Column.astro +25 -0
  20. package/template/src/components/user/Columns.astro +200 -0
  21. package/template/src/components/user/ComponentPreviewBlock.astro +1 -1
  22. package/template/src/components/user/Image.astro +1 -1
  23. package/template/src/components/user/Step.astro +1 -1
  24. package/template/src/components/user/Steps.astro +1 -1
  25. package/template/src/components/user/Tab.astro +1 -3
  26. package/template/src/components/user/Tabs.astro +2 -2
  27. package/template/src/layouts/Layout.astro +2 -4
  28. package/template/src/lib/assistant-chrome-defaults.ts +12 -0
  29. package/template/src/lib/assistant-embed-script.ts +209 -18
  30. package/template/src/lib/validation.ts +325 -75
  31. package/template/src/styles/global.css +81 -4
  32. package/template/src/components/chat/AskAiWidget.tsx +0 -2011
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import type { MarkdownHeading } from "astro";
3
+ import { Icon } from "astro-icon/components";
3
4
 
4
5
  interface Props {
5
6
  headings: MarkdownHeading[];
@@ -29,19 +30,32 @@ for (const heading of headings) {
29
30
  ---
30
31
 
31
32
  <nav
32
- class="sticky top-16 text-[14px]/5 py-16 -mt-16"
33
+ class="sticky top-[calc(68px+64px)] text-[13px]/5"
33
34
  aria-label="Table of Contents"
34
35
  data-slugs={JSON.stringify(headings.map((h) => h.slug))}
35
36
  >
36
- <div class="text-sm font-medium mb-3 text-neutral-900 dark:text-neutral-100">On this page</div>
37
+ <div
38
+ class="pl-[22px] relative font-medium mb-3 text-neutral-900 dark:text-neutral-100"
39
+ >
40
+ <Icon
41
+ name="lucide:text-align-start"
42
+ class="absolute left-0 top-1/2 -translate-y-1/2 size-3"
43
+ />On this page
44
+ </div>
37
45
  <div class="flex text-neutral-600/90 dark:text-neutral-300/90">
38
- <svg viewBox="0 0 22 100" width="22px" height="100" class="shrink-0">
46
+ <svg
47
+ data-rd-toc-rail
48
+ viewBox="0 0 22 100"
49
+ width="22px"
50
+ height="100"
51
+ class="shrink-0"
52
+ >
39
53
  <path
40
54
  id="toc-bg-path"
41
55
  d=""
42
- stroke-width="1.2"
56
+ stroke-width="1"
43
57
  vector-effect="non-scaling-stroke"
44
- class="stroke-neutral-200 dark:stroke-neutral-700/70"
58
+ class="stroke-neutral-200/80 dark:stroke-neutral-700/70"
45
59
  fill="none"
46
60
  stroke-linecap="round"
47
61
  stroke-linejoin="round"></path>
@@ -50,14 +64,13 @@ for (const heading of headings) {
50
64
  d=""
51
65
  stroke-width="1.4"
52
66
  vector-effect="non-scaling-stroke"
53
- class="transition-[stroke-dasharray] duration-200"
54
- style="stroke: var(--color-theme);"
67
+ class="transition-[stroke-dasharray] duration-200 stroke-(--color-theme)/50"
55
68
  fill="none"
56
69
  stroke-linecap="round"
57
70
  stroke-linejoin="round"></path>
58
71
  <g id="toc-circles"></g>
59
72
  </svg>
60
- <ol id="toc-list">
73
+ <ol id="toc-list" class="text-neutral-500/80 dark:text-neutral-400/70">
61
74
  {
62
75
  groupedHeadings.map((heading) => (
63
76
  <li class="mt-3 first:mt-0">
@@ -94,7 +107,7 @@ for (const heading of headings) {
94
107
  <script>
95
108
  function initTableOfContents() {
96
109
  const nav = document.querySelector('nav[aria-label="Table of Contents"]');
97
- const svg = nav?.querySelector("svg");
110
+ const svg = nav?.querySelector("[data-rd-toc-rail]");
98
111
  const ol = nav?.querySelector("#toc-list");
99
112
 
100
113
  if (!svg || !ol) return;
@@ -109,12 +122,15 @@ for (const heading of headings) {
109
122
  let path = "";
110
123
  let currentY = 0;
111
124
  const gap = 12; // mt-3 = 12px
112
- const linkPositions: Map<string, { pathLengthAtMid: number }> = new Map();
125
+ const markerOffset = 10; // Half of the inherited 20px line-height
126
+ const linkPositions: Map<string, { pathLengthAtMarker: number }> =
127
+ new Map();
113
128
 
114
129
  // First pass: collect all items with their positions
115
130
  interface ItemInfo {
116
131
  x: number;
117
132
  height: number;
133
+ markerY: number;
118
134
  slug?: string;
119
135
  yStart: number;
120
136
  }
@@ -123,7 +139,7 @@ for (const heading of headings) {
123
139
  // Iterate through top-level items to collect data
124
140
  ol.querySelectorAll(":scope > li").forEach((li) => {
125
141
  const topLink = li.querySelector(
126
- ":scope > a"
142
+ ":scope > a",
127
143
  ) as HTMLAnchorElement | null;
128
144
  const subOl = li.querySelector(":scope > ol");
129
145
 
@@ -134,6 +150,7 @@ for (const heading of headings) {
134
150
  items.push({
135
151
  x: 0.5,
136
152
  height,
153
+ markerY: currentY + Math.min(markerOffset, height / 2),
137
154
  slug: topLink.dataset.slug,
138
155
  yStart: currentY,
139
156
  });
@@ -144,7 +161,7 @@ for (const heading of headings) {
144
161
  if (subOl) {
145
162
  subOl.querySelectorAll(":scope > li").forEach((subLi) => {
146
163
  const subLink = subLi.querySelector(
147
- "a"
164
+ "a",
148
165
  ) as HTMLAnchorElement | null;
149
166
  if (!subLink) return;
150
167
 
@@ -155,6 +172,7 @@ for (const heading of headings) {
155
172
  items.push({
156
173
  x: 12,
157
174
  height,
175
+ markerY: currentY + Math.min(markerOffset, height / 2),
158
176
  slug: subLink.dataset.slug,
159
177
  yStart: currentY,
160
178
  });
@@ -163,59 +181,121 @@ for (const heading of headings) {
163
181
  }
164
182
  });
165
183
 
166
- // Second pass: build path with midpoint start/end, tracking cumulative path length
184
+ // Second pass: build path with first-line anchors, tracking cumulative path length
167
185
  let pathLength = 0;
168
186
  let prevX: number | null = null;
169
187
  let prevY: number | null = null;
170
188
 
189
+ function getCubicPoint(
190
+ start: number,
191
+ control1: number,
192
+ control2: number,
193
+ end: number,
194
+ progress: number,
195
+ ) {
196
+ const inverse = 1 - progress;
197
+
198
+ return (
199
+ inverse ** 3 * start +
200
+ 3 * inverse ** 2 * progress * control1 +
201
+ 3 * inverse * progress ** 2 * control2 +
202
+ progress ** 3 * end
203
+ );
204
+ }
205
+
206
+ function getCubicLength(
207
+ x1: number,
208
+ y1: number,
209
+ c1x: number,
210
+ c1y: number,
211
+ c2x: number,
212
+ c2y: number,
213
+ x2: number,
214
+ y2: number,
215
+ ) {
216
+ let length = 0;
217
+ let previousX = x1;
218
+ let previousY = y1;
219
+
220
+ for (let step = 1; step <= 12; step += 1) {
221
+ const progress = step / 12;
222
+ const x = getCubicPoint(x1, c1x, c2x, x2, progress);
223
+ const y = getCubicPoint(y1, c1y, c2y, y2, progress);
224
+ length += Math.hypot(x - previousX, y - previousY);
225
+ previousX = x;
226
+ previousY = y;
227
+ }
228
+
229
+ return length;
230
+ }
231
+
232
+ function appendConnector(x: number, y: number) {
233
+ if (prevX === null || prevY === null) return;
234
+
235
+ const dx = x - prevX;
236
+ const dy = y - prevY;
237
+
238
+ if (Math.abs(dx) < 0.01 || Math.abs(dy) < 0.01) {
239
+ path += `L ${x} ${y} `;
240
+ pathLength += Math.hypot(dx, dy);
241
+ } else {
242
+ const bendOffset = Math.min(Math.abs(dy) * 0.6, 8);
243
+ const direction = dy > 0 ? 1 : -1;
244
+ const c1x = prevX;
245
+ const c1y = prevY + bendOffset * direction;
246
+ const c2x = x;
247
+ const c2y = y - bendOffset * direction;
248
+
249
+ path += `C ${c1x} ${c1y} ${c2x} ${c2y} ${x} ${y} `;
250
+ pathLength += getCubicLength(prevX, prevY, c1x, c1y, c2x, c2y, x, y);
251
+ }
252
+
253
+ prevX = x;
254
+ prevY = y;
255
+ }
256
+
171
257
  items.forEach((item, index) => {
172
258
  const isFirst = index === 0;
173
259
  const isLast = index === items.length - 1;
174
- const midY = item.yStart + item.height / 2;
175
260
 
176
261
  if (isFirst) {
177
- // Start at midpoint of first item
178
- path = `M ${item.x} ${midY} `;
262
+ // Start at the center of the first rendered line.
263
+ path = `M ${item.x} ${item.markerY} `;
179
264
  prevX = item.x;
180
- prevY = midY;
265
+ prevY = item.markerY;
181
266
 
182
- // Path length at first item's midpoint is 0
267
+ // Path length at first item's marker is 0
183
268
  if (item.slug) {
184
- linkPositions.set(item.slug, { pathLengthAtMid: 0 });
269
+ linkPositions.set(item.slug, { pathLengthAtMarker: 0 });
185
270
  }
186
271
 
187
272
  if (!isLast) {
188
- // Draw from midpoint to bottom
273
+ // Draw from first-line center to bottom
189
274
  path += `L ${item.x} ${item.yStart + item.height} `;
190
- pathLength += item.height / 2; // vertical segment
275
+ pathLength += item.yStart + item.height - item.markerY;
191
276
  prevY = item.yStart + item.height;
192
277
  }
193
278
  } else {
194
279
  // Segment from previous position to this item's top
195
- const dx = item.x - prevX!;
196
- const dy = item.yStart - prevY!;
197
- pathLength += Math.sqrt(dx * dx + dy * dy);
198
- path += `L ${item.x} ${item.yStart} `;
199
- prevX = item.x;
200
- prevY = item.yStart;
280
+ appendConnector(item.x, item.yStart);
201
281
 
202
282
  if (isLast) {
203
- // Draw from top to midpoint
204
- const midLength = item.height / 2;
205
- pathLength += midLength;
206
- path += `L ${item.x} ${midY} `;
207
- prevY = midY;
283
+ // Draw from top to first-line center
284
+ const markerLength = item.markerY - item.yStart;
285
+ pathLength += markerLength;
286
+ path += `L ${item.x} ${item.markerY} `;
287
+ prevY = item.markerY;
208
288
 
209
289
  if (item.slug) {
210
- linkPositions.set(item.slug, { pathLengthAtMid: pathLength });
290
+ linkPositions.set(item.slug, { pathLengthAtMarker: pathLength });
211
291
  }
212
292
  } else {
213
- // Middle item: path length at midpoint is current + half height
214
- const pathLengthAtMid = pathLength + item.height / 2;
293
+ // Middle item: path length at marker is current + first-line offset
294
+ const pathLengthAtMarker = pathLength + item.markerY - item.yStart;
215
295
 
216
296
  if (item.slug) {
217
297
  linkPositions.set(item.slug, {
218
- pathLengthAtMid: pathLengthAtMid,
298
+ pathLengthAtMarker,
219
299
  });
220
300
  }
221
301
 
@@ -227,26 +307,25 @@ for (const heading of headings) {
227
307
  }
228
308
  });
229
309
 
230
- // Create circles at each item's midpoint
310
+ // Create circles at each item's first-line center
231
311
  const circlesGroup = svg.querySelector("#toc-circles");
232
312
  if (circlesGroup) {
233
313
  // Clear existing circles before creating new ones
234
314
  circlesGroup.innerHTML = "";
235
315
  items.forEach((item) => {
236
- const midY = item.yStart + item.height / 2;
237
316
  const circle = document.createElementNS(
238
317
  "http://www.w3.org/2000/svg",
239
- "circle"
318
+ "circle",
240
319
  );
241
320
  circle.setAttribute("cx", String(item.x));
242
- circle.setAttribute("cy", String(midY));
321
+ circle.setAttribute("cy", String(item.markerY));
243
322
  circle.setAttribute("r", "2.25");
244
323
  circle.setAttribute("fill", "currentColor");
245
324
  circle.classList.add(
246
- "fill-neutral-300",
247
- "dark:fill-neutral-600",
325
+ "fill-neutral-200",
326
+ "dark:fill-neutral-700",
248
327
  "transition-all",
249
- "duration-200"
328
+ "duration-200",
250
329
  );
251
330
  if (item.slug) {
252
331
  circle.dataset.slug = item.slug;
@@ -273,7 +352,7 @@ for (const heading of headings) {
273
352
  // Initially hide the highlight (use same pattern as "0 visible" state)
274
353
  fgPath.setAttribute(
275
354
  "stroke-dasharray",
276
- `0 ${totalLength} 0 ${totalLength}`
355
+ `0 ${totalLength} 0 ${totalLength}`,
277
356
  );
278
357
  fgPath.setAttribute("stroke-dashoffset", "0");
279
358
  }
@@ -289,8 +368,8 @@ for (const heading of headings) {
289
368
  visibleSlugs.forEach((slug) => {
290
369
  const pos = linkPositions.get(slug);
291
370
  if (pos) {
292
- minPathLength = Math.min(minPathLength, pos.pathLengthAtMid);
293
- maxPathLength = Math.max(maxPathLength, pos.pathLengthAtMid);
371
+ minPathLength = Math.min(minPathLength, pos.pathLengthAtMarker);
372
+ maxPathLength = Math.max(maxPathLength, pos.pathLengthAtMarker);
294
373
  }
295
374
  });
296
375
 
@@ -312,14 +391,14 @@ for (const heading of headings) {
312
391
  // 1 visible heading: position gap at this item, ready to grow down
313
392
  fgPath.setAttribute(
314
393
  "stroke-dasharray",
315
- `0 ${minPathLength} 0 ${totalLength}`
394
+ `0 ${minPathLength} 0 ${totalLength}`,
316
395
  );
317
396
  fgPath.setAttribute("stroke-dashoffset", "0");
318
397
  } else {
319
398
  // 0 visible headings: fully hidden
320
399
  fgPath.setAttribute(
321
400
  "stroke-dasharray",
322
- `0 ${totalLength} 0 ${totalLength}`
401
+ `0 ${totalLength} 0 ${totalLength}`,
323
402
  );
324
403
  fgPath.setAttribute("stroke-dashoffset", "0");
325
404
  }
@@ -331,9 +410,11 @@ for (const heading of headings) {
331
410
  const slug = (circle as SVGCircleElement).dataset.slug;
332
411
  if (slug && visibleSlugs.has(slug)) {
333
412
  (circle as SVGCircleElement).style.fill = "var(--color-theme)";
413
+ circle.setAttribute("fill-opacity", "0.8");
334
414
  circle.setAttribute("r", "2.75");
335
415
  } else {
336
416
  (circle as SVGCircleElement).style.fill = "";
417
+ circle.removeAttribute("fill-opacity");
337
418
  circle.setAttribute("r", "2.25");
338
419
  }
339
420
  });
@@ -343,21 +424,46 @@ for (const heading of headings) {
343
424
  tocLinks?.forEach((link) => {
344
425
  const slug = (link as HTMLAnchorElement).dataset.slug;
345
426
  if (slug && visibleSlugs.has(slug)) {
346
- link.classList.add("text-neutral-900", "dark:text-neutral-100");
427
+ link.classList.add("text-neutral-950", "dark:text-neutral-100");
347
428
  } else {
348
- link.classList.remove("text-neutral-900", "dark:text-neutral-100");
429
+ link.classList.remove("text-neutral-950", "dark:text-neutral-100");
349
430
  }
350
431
  });
351
432
  }
352
433
 
353
434
  // Intersection Observer to track headings in view
354
435
  const slugs: string[] = JSON.parse(
355
- (nav as HTMLElement).dataset.slugs || "[]"
436
+ (nav as HTMLElement).dataset.slugs || "[]",
356
437
  );
357
438
  const headings = slugs
358
439
  .map((slug) => document.getElementById(slug))
359
440
  .filter((el): el is HTMLElement => el !== null);
360
441
 
442
+ if ((ol as HTMLElement).dataset.smoothScrollBound !== "true") {
443
+ (ol as HTMLElement).dataset.smoothScrollBound = "true";
444
+
445
+ ol.addEventListener("click", (event) => {
446
+ if (!(event.target instanceof Element)) return;
447
+
448
+ const link = event.target.closest(
449
+ "a[data-slug]",
450
+ ) as HTMLAnchorElement | null;
451
+ if (!link) return;
452
+
453
+ const slug = link.dataset.slug;
454
+ const target = slug ? document.getElementById(slug) : null;
455
+ if (!target) return;
456
+
457
+ event.preventDefault();
458
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
459
+
460
+ const hash = link.getAttribute("href");
461
+ if (hash) {
462
+ window.history.replaceState(null, "", hash);
463
+ }
464
+ });
465
+ }
466
+
361
467
  const headerOffset = 112;
362
468
 
363
469
  // Calculate active headings based on scroll position
@@ -402,7 +508,7 @@ for (const heading of headings) {
402
508
  {
403
509
  rootMargin: `-${headerOffset}px 0px 0px 0px`,
404
510
  threshold: 0,
405
- }
511
+ },
406
512
  );
407
513
 
408
514
  headings.forEach((heading) => observer.observe(heading));
@@ -455,7 +561,7 @@ for (const heading of headings) {
455
561
 
456
562
  // Re-initialize when TOC becomes visible (responsive breakpoint)
457
563
  const tocNav = document.querySelector(
458
- 'nav[aria-label="Table of Contents"]'
564
+ 'nav[aria-label="Table of Contents"]',
459
565
  );
460
566
  if (tocNav) {
461
567
  let wasVisible = tocNav.getBoundingClientRect().width > 0;