radiant-docs 0.1.37 → 0.1.39

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 (38) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +2 -0
  3. package/template/src/components/Footer.astro +1 -1
  4. package/template/src/components/Header.astro +8 -8
  5. package/template/src/components/OpenApiPage.astro +18 -18
  6. package/template/src/components/Search.astro +18 -18
  7. package/template/src/components/Sidebar.astro +4 -2
  8. package/template/src/components/SidebarDropdown.astro +82 -79
  9. package/template/src/components/SidebarSegmented.astro +5 -5
  10. package/template/src/components/TableOfContents.astro +24 -15
  11. package/template/src/components/ThemeSwitcher.astro +15 -8
  12. package/template/src/components/chat/AskAiWidget.tsx +4 -3
  13. package/template/src/components/endpoint/PlaygroundBar.astro +3 -3
  14. package/template/src/components/endpoint/PlaygroundButton.astro +2 -2
  15. package/template/src/components/endpoint/PlaygroundField.astro +53 -53
  16. package/template/src/components/endpoint/PlaygroundForm.astro +38 -22
  17. package/template/src/components/endpoint/RequestSnippets.astro +54 -21
  18. package/template/src/components/endpoint/ResponseDisplay.astro +24 -24
  19. package/template/src/components/endpoint/ResponseFieldTree.astro +12 -12
  20. package/template/src/components/endpoint/ResponseFields.astro +19 -19
  21. package/template/src/components/endpoint/ResponseSnippets.astro +66 -29
  22. package/template/src/components/ui/CodeTabEdge.astro +6 -4
  23. package/template/src/components/ui/Field.astro +7 -7
  24. package/template/src/components/ui/demo/Demo.astro +1 -1
  25. package/template/src/components/user/Accordion.astro +3 -3
  26. package/template/src/components/user/Callout.astro +8 -8
  27. package/template/src/components/user/CodeBlock.astro +63 -25
  28. package/template/src/components/user/CodeGroup.astro +259 -22
  29. package/template/src/components/user/ComponentPreviewBlock.astro +36 -10
  30. package/template/src/components/user/Image.astro +2 -2
  31. package/template/src/components/user/Step.astro +4 -4
  32. package/template/src/components/user/Tab.astro +1 -1
  33. package/template/src/components/user/Tabs.astro +142 -42
  34. package/template/src/layouts/Layout.astro +1 -1
  35. package/template/src/lib/code/code-block.ts +150 -15
  36. package/template/src/lib/mdx/remark-resolve-internal-links.ts +639 -0
  37. package/template/src/pages/404.astro +44 -0
  38. package/template/src/styles/global.css +51 -19
@@ -116,25 +116,58 @@ function parseHighlightedLineNumbers(
116
116
 
117
117
  function buildTokenStyle(token: {
118
118
  color?: string;
119
+ darkColor?: string;
119
120
  bgColor?: string;
121
+ darkBgColor?: string;
120
122
  fontStyle?: number;
121
123
  htmlStyle?: Record<string, string>;
124
+ darkHtmlStyle?: Record<string, string>;
122
125
  }): string | undefined {
123
126
  const styleSegments: string[] = [];
124
127
 
125
- if (token.color) styleSegments.push(`color:${token.color}`);
126
- if (token.bgColor) styleSegments.push(`background-color:${token.bgColor}`);
128
+ const pushStyleVariable = (property: string, value?: string) => {
129
+ if (value) styleSegments.push(`${property}:${value}`);
130
+ };
131
+
132
+ pushStyleVariable("--rd-token-color", token.color);
133
+ pushStyleVariable("--rd-token-color-dark", token.darkColor);
134
+ pushStyleVariable("--rd-token-bg", token.bgColor);
135
+ pushStyleVariable("--rd-token-bg-dark", token.darkBgColor);
127
136
 
128
137
  const fontStyle = typeof token.fontStyle === "number" ? token.fontStyle : 0;
129
138
  if ((fontStyle & 1) === 1) styleSegments.push("font-style:italic");
130
139
  if ((fontStyle & 2) === 2) styleSegments.push("font-weight:600");
131
140
  if ((fontStyle & 4) === 4) styleSegments.push("text-decoration:underline");
132
141
 
133
- if (token.htmlStyle && typeof token.htmlStyle === "object") {
134
- for (const [property, value] of Object.entries(token.htmlStyle)) {
142
+ const pushHtmlStyle = (
143
+ styleObject: Record<string, string> | undefined,
144
+ isDarkStyle: boolean,
145
+ ) => {
146
+ if (!styleObject || typeof styleObject !== "object") return;
147
+
148
+ for (const [property, value] of Object.entries(styleObject)) {
149
+ const normalizedProperty = property.trim().toLowerCase();
150
+ if (normalizedProperty === "color") {
151
+ pushStyleVariable(
152
+ isDarkStyle ? "--rd-token-color-dark" : "--rd-token-color",
153
+ value,
154
+ );
155
+ continue;
156
+ }
157
+ if (normalizedProperty === "background-color") {
158
+ pushStyleVariable(
159
+ isDarkStyle ? "--rd-token-bg-dark" : "--rd-token-bg",
160
+ value,
161
+ );
162
+ continue;
163
+ }
164
+
135
165
  styleSegments.push(`${property}:${value}`);
136
166
  }
137
- }
167
+ };
168
+
169
+ pushHtmlStyle(token.htmlStyle, false);
170
+ pushHtmlStyle(token.darkHtmlStyle, true);
138
171
 
139
172
  if (!styleSegments.length) return undefined;
140
173
  return styleSegments.join(";");
@@ -198,7 +231,7 @@ const renderedCodeLinesHtml = normalizedTokenLines
198
231
  const tokenStyleAttribute = tokenStyle
199
232
  ? ` style="${escapeAttribute(tokenStyle)}"`
200
233
  : "";
201
- return `<span${tokenStyleAttribute}>${escapeHtml(token.content)}</span>`;
234
+ return `<span data-rd-token${tokenStyleAttribute}>${escapeHtml(token.content)}</span>`;
202
235
  })
203
236
  .join("")
204
237
  : fallbackLineContent.length > 0
@@ -206,14 +239,14 @@ const renderedCodeLinesHtml = normalizedTokenLines
206
239
  : "&nbsp;";
207
240
 
208
241
  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>`
242
+ ? `<span class="w-10 shrink-0 select-none pl-4 pr-3 text-right tabular-nums text-neutral-400 dark:text-neutral-500">${lineNumber}</span>`
210
243
  : "";
211
244
 
212
245
  const lineContentClass = parsedShowLineNumbers
213
246
  ? "flex-1 whitespace-pre pr-4"
214
247
  : "flex-1 whitespace-pre pr-4 pl-4";
215
248
  const lineClass = isHighlighted
216
- ? "flex min-w-full bg-neutral-100/80"
249
+ ? "flex min-w-full bg-neutral-100/80 dark:bg-neutral-800/70"
217
250
  : "flex min-w-full";
218
251
 
219
252
  return `<span class="${lineClass}">${lineNumberHtml}<span class="${lineContentClass}">${tokenHtml}</span></span>`;
@@ -223,8 +256,8 @@ const renderedCodeLinesHtml = normalizedTokenLines
223
256
 
224
257
  <div
225
258
  class:list={[
226
- "group/prose-code not-prose relative w-full max-w-full min-w-0",
227
- parsedInCodeGroup ? "my-0" : "my-6",
259
+ "group/prose-code not-prose relative w-full max-w-full min-w-0 rounded-xl",
260
+ parsedInCodeGroup ? "my-0" : "my-6 shadow-xs",
228
261
  ]}
229
262
  data-rd-code-block-root="true"
230
263
  data-rd-collapsed-lines={collapsedLinesValue}
@@ -238,18 +271,18 @@ const renderedCodeLinesHtml = normalizedTokenLines
238
271
  >
239
272
  <div
240
273
  class:list={[
241
- "w-full max-w-full min-w-0 overflow-hidden border border-neutral-200 bg-white shadow-xs",
274
+ "w-full max-w-full min-w-0 overflow-hidden border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-(--rd-code-surface)",
242
275
  parsedInCodeGroup ? "rounded-t-none rounded-b-xl" : "rounded-xl",
243
276
  ]}
244
277
  >
245
278
  {
246
279
  !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">
280
+ <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 dark:border-neutral-800 dark:bg-neutral-900/60 dark:inset-shadow-neutral-900/80">
248
281
  <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" />
282
+ <div class="relative h-9 w-fit max-w-full rounded-tl-xl bg-white dark:bg-(--rd-code-surface)">
283
+ <div class="absolute inset-x-0 -bottom-px h-px bg-white dark:bg-(--rd-code-surface)" />
251
284
  <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">
285
+ <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 dark:text-neutral-300">
253
286
  {shouldRenderLanguageIcon ? (
254
287
  <CodeLanguageIcon
255
288
  language={normalizedLanguage}
@@ -261,12 +294,12 @@ const renderedCodeLinesHtml = normalizedTokenLines
261
294
  </div>
262
295
  </div>
263
296
  </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" />
297
+ <div class="relative h-9 w-5 shrink-0 rounded-tr-xl bg-white dark:bg-(--rd-code-surface)">
298
+ <div class="absolute inset-x-0 -bottom-px h-px bg-white dark:bg-(--rd-code-surface)" />
266
299
  <CodeTabEdge className="pointer-events-none absolute -top-px right-full z-10 h-[calc(100%+2px)] rotate-y-180" />
267
300
  <button
268
301
  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"
302
+ 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 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200"
270
303
  data-rd-copy-trigger="true"
271
304
  data-rd-copy-content={encodedRaw}
272
305
  aria-label="Copy code"
@@ -279,7 +312,7 @@ const renderedCodeLinesHtml = normalizedTokenLines
279
312
  />
280
313
  <Icon
281
314
  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"
315
+ 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 dark:text-green-400/90"
283
316
  data-rd-copy-check
284
317
  aria-hidden="true"
285
318
  />
@@ -289,13 +322,16 @@ const renderedCodeLinesHtml = normalizedTokenLines
289
322
  ) : null
290
323
  }
291
324
 
292
- <div class="relative min-w-0">
325
+ <div
326
+ class="relative min-w-0"
327
+ data-rd-code-group-panel={parsedInCodeGroup ? "true" : undefined}
328
+ >
293
329
  {
294
330
  !parsedInCodeGroup && !parsedShowFilename ? (
295
331
  <div class="pointer-events-none absolute right-2 top-2 z-20">
296
332
  <button
297
333
  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"
334
+ 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 dark:border-neutral-700/50 dark:bg-(--rd-code-surface) dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200"
299
335
  data-rd-copy-trigger="true"
300
336
  data-rd-copy-content={encodedRaw}
301
337
  aria-label="Copy code"
@@ -308,7 +344,7 @@ const renderedCodeLinesHtml = normalizedTokenLines
308
344
  />
309
345
  <Icon
310
346
  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"
347
+ 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 dark:text-green-400/90"
312
348
  data-rd-copy-check
313
349
  aria-hidden="true"
314
350
  />
@@ -333,10 +369,10 @@ const renderedCodeLinesHtml = normalizedTokenLines
333
369
 
334
370
  <div
335
371
  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"
372
+ 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 dark:[scrollbar-color:var(--color-neutral-700)_transparent] dark:[&::-webkit-scrollbar-thumb]:bg-neutral-700/70 dark:hover:[&::-webkit-scrollbar-thumb]:bg-neutral-700/90"
337
373
  >
338
374
  <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>
375
+ class="relative m-0 min-w-full bg-white p-0 text-[13px] leading-6 dark:bg-(--rd-code-surface)"><code data-rd-code-theme class="block min-w-full py-2.5 font-mono text-neutral-800 dark:text-neutral-200"><Fragment set:html={renderedCodeLinesHtml} /></code></pre>
340
376
  </div>
341
377
  </div>
342
378
  </div>
@@ -348,7 +384,9 @@ const renderedCodeLinesHtml = normalizedTokenLines
348
384
  const root = script.closest("[data-rd-code-block-root='true']");
349
385
  if (!(root instanceof HTMLElement)) return;
350
386
 
351
- const copyButtons = root.querySelectorAll("[data-rd-copy-trigger='true']");
387
+ const copyButtons = root.querySelectorAll(
388
+ "[data-rd-copy-trigger='true']",
389
+ );
352
390
  if (copyButtons.length === 0) return;
353
391
 
354
392
  const setCopiedState = (button, copied) => {
@@ -4,11 +4,11 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
4
4
  ---
5
5
 
6
6
  <div
7
- class="group/prose-code-group not-prose relative my-6 w-full max-w-full min-w-0"
7
+ class="group/prose-code-group not-prose relative my-6 w-full max-w-full min-w-0 shadow-xs rounded-xl"
8
8
  data-rd-code-group-root="true"
9
9
  >
10
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"
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 dark:border-neutral-800 dark:bg-neutral-900/60 dark:inset-shadow-neutral-900/80"
12
12
  >
13
13
  <div class="flex min-w-0 items-end justify-between gap-2">
14
14
  <div class="min-w-0 flex-1 overflow-hidden rounded-t-xl">
@@ -19,20 +19,25 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
19
19
  <div
20
20
  data-rd-code-group-pill
21
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"
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 opacity-0 transition-[left,width,opacity] duration-200 ease-out dark:border-neutral-700/70 dark:bg-(--rd-code-surface)"
23
23
  >
24
24
  </div>
25
25
  </div>
26
26
  </div>
27
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>
28
+ <div
29
+ class="relative h-9 w-5 shrink-0 rounded-tr-xl bg-white dark:bg-(--rd-code-surface)"
30
+ >
31
+ <div
32
+ class="absolute inset-x-0 -bottom-px h-px bg-white dark:bg-(--rd-code-surface)"
33
+ >
34
+ </div>
30
35
  <CodeTabEdge
31
36
  className="pointer-events-none absolute -top-px right-full z-10 h-[calc(100%+2px)] rotate-y-180"
32
37
  />
33
38
  <button
34
39
  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"
40
+ 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 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200"
36
41
  data-rd-copy-trigger="true"
37
42
  aria-label="Copy code"
38
43
  >
@@ -44,7 +49,7 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
44
49
  />
45
50
  <Icon
46
51
  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"
52
+ 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 dark:text-green-400/90"
48
53
  data-rd-copy-check
49
54
  aria-hidden="true"
50
55
  />
@@ -53,7 +58,10 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
53
58
  </div>
54
59
  </div>
55
60
 
56
- <div data-rd-code-group-content class="min-w-0">
61
+ <div
62
+ data-rd-code-group-content
63
+ class="relative min-w-0 overflow-hidden transition-[height] duration-300 ease-in-out"
64
+ >
57
65
  <slot />
58
66
  </div>
59
67
  <script is:inline>
@@ -69,14 +77,27 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
69
77
  const pillElement = root.querySelector("[data-rd-code-group-pill]");
70
78
  const copyButton = root.querySelector("[data-rd-copy-trigger='true']");
71
79
 
72
- if (!contentElement || !tabsElement || !pillElement) return;
80
+ if (
81
+ !(contentElement instanceof HTMLElement) ||
82
+ !(tabsElement instanceof HTMLElement) ||
83
+ !(pillElement instanceof HTMLElement)
84
+ ) {
85
+ return;
86
+ }
73
87
 
74
88
  const codeItems = Array.from(
75
89
  contentElement.querySelectorAll("[data-rd-code-group-item='true']"),
76
- );
90
+ ).filter((item) => item instanceof HTMLElement);
77
91
  if (codeItems.length === 0) return;
78
92
 
93
+ const prefersReducedMotion =
94
+ typeof window.matchMedia === "function" &&
95
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
96
+ const TRANSITION_DURATION_MS = prefersReducedMotion ? 0 : 300;
97
+
79
98
  let activeIndex = 0;
99
+ let transitionTimeoutId = null;
100
+ let activeItemResizeObserver = null;
80
101
 
81
102
  const setCopiedState = (button, copied) => {
82
103
  const copyIcon = button.querySelector("[data-rd-copy-icon]");
@@ -118,8 +139,7 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
118
139
 
119
140
  const tabButton = document.createElement("button");
120
141
  tabButton.type = "button";
121
- tabButton.className =
122
- `relative inline-flex h-9 items-center 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 ${hasTabIcon ? "gap-2" : ""}`;
142
+ tabButton.className = `relative inline-flex h-9 items-center border-0 bg-transparent px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer ${hasTabIcon ? "gap-2" : ""}`;
123
143
  tabButton.setAttribute("aria-label", filename);
124
144
  tabButton.setAttribute("data-rd-code-group-tab", String(index));
125
145
 
@@ -142,14 +162,28 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
142
162
  tabWrapper.appendChild(tabButton);
143
163
  tabsElement.appendChild(tabWrapper);
144
164
 
165
+ const panelCandidate = itemElement.querySelector(
166
+ "[data-rd-code-group-panel='true']",
167
+ );
168
+ const panelElement =
169
+ panelCandidate instanceof HTMLElement ? panelCandidate : itemElement;
170
+ const frameElement =
171
+ itemElement.firstElementChild instanceof HTMLElement
172
+ ? itemElement.firstElementChild
173
+ : null;
174
+
145
175
  return {
146
176
  itemElement,
177
+ panelElement,
178
+ frameElement,
147
179
  tabWrapper,
148
180
  tabButton,
149
181
  iconContainer,
150
182
  };
151
183
  });
152
184
 
185
+ contentElement.style.transitionDuration = `${TRANSITION_DURATION_MS}ms`;
186
+
153
187
  const syncPill = () => {
154
188
  const activeTab = tabs[activeIndex]?.tabWrapper;
155
189
  if (!activeTab) {
@@ -168,12 +202,11 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
168
202
  pillElement.classList.add("opacity-100");
169
203
  };
170
204
 
171
- const syncActiveTab = () => {
172
- tabs.forEach(({ itemElement, tabButton, iconContainer }, index) => {
205
+ const updateTabButtonStates = () => {
206
+ tabs.forEach(({ tabButton, iconContainer }, index) => {
173
207
  const isActive = index === activeIndex;
174
- itemElement.style.display = isActive ? "" : "none";
175
- tabButton.classList.toggle("text-neutral-900", isActive);
176
- tabButton.classList.toggle("text-neutral-600", !isActive);
208
+ tabButton.classList.toggle("text-foreground", isActive);
209
+ tabButton.classList.toggle("text-muted-foreground", !isActive);
177
210
 
178
211
  if (!iconContainer) return;
179
212
  iconContainer.classList.toggle("opacity-100", isActive);
@@ -182,11 +215,163 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
182
215
  syncPill();
183
216
  };
184
217
 
218
+ const getItemOuterHeight = (itemElement) => {
219
+ const frameElement =
220
+ itemElement.firstElementChild instanceof HTMLElement
221
+ ? itemElement.firstElementChild
222
+ : null;
223
+ if (frameElement) {
224
+ const frameRectHeight = frameElement.getBoundingClientRect().height;
225
+ if (Number.isFinite(frameRectHeight) && frameRectHeight > 0) {
226
+ return Math.ceil(frameRectHeight);
227
+ }
228
+
229
+ const frameOffsetHeight = frameElement.offsetHeight;
230
+ if (Number.isFinite(frameOffsetHeight) && frameOffsetHeight > 0) {
231
+ return frameOffsetHeight;
232
+ }
233
+ }
234
+
235
+ const rectHeight = itemElement.getBoundingClientRect().height;
236
+ if (Number.isFinite(rectHeight) && rectHeight > 0) {
237
+ return Math.ceil(rectHeight);
238
+ }
239
+
240
+ const offsetHeight = itemElement.offsetHeight;
241
+ if (Number.isFinite(offsetHeight) && offsetHeight > 0) {
242
+ return offsetHeight;
243
+ }
244
+
245
+ return itemElement.scrollHeight;
246
+ };
247
+
248
+ const syncContentHeight = () => {
249
+ const activeItem = tabs[activeIndex]?.itemElement;
250
+ if (!activeItem) return;
251
+ contentElement.style.height = `${getItemOuterHeight(activeItem)}px`;
252
+ };
253
+
254
+ const observeActiveItemHeight = () => {
255
+ if (activeItemResizeObserver) {
256
+ activeItemResizeObserver.disconnect();
257
+ activeItemResizeObserver = null;
258
+ }
259
+
260
+ if (typeof ResizeObserver === "undefined") return;
261
+
262
+ const activeItem = tabs[activeIndex]?.itemElement;
263
+ if (!activeItem) return;
264
+
265
+ activeItemResizeObserver = new ResizeObserver(() => {
266
+ if (transitionTimeoutId !== null) return;
267
+ syncContentHeight();
268
+ });
269
+ activeItemResizeObserver.observe(activeItem);
270
+ };
271
+
272
+ const setPanelsToRestState = () => {
273
+ tabs.forEach(({ itemElement, panelElement, frameElement }, index) => {
274
+ const isActive = index === activeIndex;
275
+ itemElement.style.position = isActive ? "relative" : "absolute";
276
+ itemElement.style.inset = isActive ? "" : "0";
277
+ itemElement.style.opacity = isActive ? "1" : "0";
278
+ itemElement.style.visibility = isActive ? "visible" : "hidden";
279
+ itemElement.style.pointerEvents = isActive ? "auto" : "none";
280
+ itemElement.style.zIndex = isActive ? "1" : "0";
281
+ panelElement.style.transform = "translateX(0)";
282
+ panelElement.style.animation = "none";
283
+ panelElement.style.willChange = "auto";
284
+ if (frameElement) {
285
+ frameElement.style.height = "";
286
+ }
287
+ });
288
+ };
289
+
290
+ const normalizeToCurrentState = () => {
291
+ setPanelsToRestState();
292
+ syncContentHeight();
293
+ observeActiveItemHeight();
294
+ };
295
+
296
+ const transitionToIndex = (nextIndex) => {
297
+ if (nextIndex === activeIndex) return;
298
+
299
+ if (transitionTimeoutId !== null) {
300
+ window.clearTimeout(transitionTimeoutId);
301
+ transitionTimeoutId = null;
302
+ normalizeToCurrentState();
303
+ }
304
+
305
+ const previousIndex = activeIndex;
306
+ const direction = nextIndex > previousIndex ? 1 : -1;
307
+ activeIndex = nextIndex;
308
+ updateTabButtonStates();
309
+
310
+ const previousTab = tabs[previousIndex];
311
+ const nextTab = tabs[nextIndex];
312
+ const previousItem = previousTab?.itemElement;
313
+ const nextItem = nextTab?.itemElement;
314
+ const previousPanel = previousTab?.panelElement;
315
+ const nextPanel = nextTab?.panelElement;
316
+ const previousFrame = previousTab?.frameElement;
317
+ const nextFrame = nextTab?.frameElement;
318
+ if (!previousItem || !nextItem || !previousPanel || !nextPanel) {
319
+ normalizeToCurrentState();
320
+ return;
321
+ }
322
+
323
+ const previousHeight = getItemOuterHeight(previousItem);
324
+ const nextHeight = getItemOuterHeight(nextItem);
325
+ contentElement.style.height = `${previousHeight}px`;
326
+ if (TRANSITION_DURATION_MS === 0) {
327
+ contentElement.style.height = `${nextHeight}px`;
328
+ } else {
329
+ requestAnimationFrame(() => {
330
+ contentElement.style.height = `${nextHeight}px`;
331
+ });
332
+ }
333
+
334
+ previousItem.style.position = "absolute";
335
+ previousItem.style.inset = "0";
336
+ previousItem.style.opacity = "1";
337
+ previousItem.style.visibility = "visible";
338
+ previousItem.style.pointerEvents = "none";
339
+ previousItem.style.zIndex = "1";
340
+ previousPanel.style.transform = "translateX(0)";
341
+ previousPanel.style.willChange = "transform";
342
+ if (previousFrame) {
343
+ previousFrame.style.height = "100%";
344
+ }
345
+ previousPanel.style.animation =
346
+ direction === 1
347
+ ? `rd-code-group-slide-out-to-left ${TRANSITION_DURATION_MS}ms ease-in-out both`
348
+ : `rd-code-group-slide-out-to-right ${TRANSITION_DURATION_MS}ms ease-in-out both`;
349
+
350
+ nextItem.style.position = "absolute";
351
+ nextItem.style.inset = "0";
352
+ nextItem.style.opacity = "1";
353
+ nextItem.style.visibility = "visible";
354
+ nextItem.style.pointerEvents = "auto";
355
+ nextItem.style.zIndex = "2";
356
+ nextPanel.style.transform = "translateX(0)";
357
+ nextPanel.style.willChange = "transform";
358
+ if (nextFrame) {
359
+ nextFrame.style.height = "100%";
360
+ }
361
+ nextPanel.style.animation =
362
+ direction === 1
363
+ ? `rd-code-group-slide-in-from-right ${TRANSITION_DURATION_MS}ms ease-in-out both`
364
+ : `rd-code-group-slide-in-from-left ${TRANSITION_DURATION_MS}ms ease-in-out both`;
365
+
366
+ transitionTimeoutId = window.setTimeout(() => {
367
+ transitionTimeoutId = null;
368
+ normalizeToCurrentState();
369
+ }, TRANSITION_DURATION_MS);
370
+ };
371
+
185
372
  tabs.forEach(({ tabButton }, index) => {
186
373
  tabButton.addEventListener("click", () => {
187
- if (activeIndex === index) return;
188
- activeIndex = index;
189
- syncActiveTab();
374
+ transitionToIndex(index);
190
375
  });
191
376
  });
192
377
 
@@ -222,9 +407,61 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
222
407
  }
223
408
 
224
409
  tabsElement.addEventListener("scroll", syncPill, { passive: true });
225
- window.addEventListener("resize", syncPill);
226
- syncActiveTab();
410
+ window.addEventListener("resize", () => {
411
+ syncPill();
412
+ if (transitionTimeoutId === null) {
413
+ syncContentHeight();
414
+ }
415
+ });
416
+ updateTabButtonStates();
417
+ normalizeToCurrentState();
227
418
  requestAnimationFrame(syncPill);
228
419
  })();
229
420
  </script>
230
421
  </div>
422
+
423
+ <style>
424
+ @keyframes rd-code-group-slide-in-from-right {
425
+ from {
426
+ transform: translateX(100%);
427
+ opacity: 0;
428
+ }
429
+ to {
430
+ transform: translateX(0);
431
+ opacity: 1;
432
+ }
433
+ }
434
+
435
+ @keyframes rd-code-group-slide-in-from-left {
436
+ from {
437
+ transform: translateX(-100%);
438
+ opacity: 0;
439
+ }
440
+ to {
441
+ transform: translateX(0);
442
+ opacity: 1;
443
+ }
444
+ }
445
+
446
+ @keyframes rd-code-group-slide-out-to-left {
447
+ from {
448
+ transform: translateX(0);
449
+ opacity: 1;
450
+ }
451
+ to {
452
+ transform: translateX(-100%);
453
+ opacity: 0;
454
+ }
455
+ }
456
+
457
+ @keyframes rd-code-group-slide-out-to-right {
458
+ from {
459
+ transform: translateX(0);
460
+ opacity: 1;
461
+ }
462
+ to {
463
+ transform: translateX(100%);
464
+ opacity: 0;
465
+ }
466
+ }
467
+ </style>
@@ -64,12 +64,12 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
64
64
  data-rd-component-preview-root="true"
65
65
  >
66
66
  <div
67
- class="w-full max-w-full min-w-0 overflow-x-auto rounded-t-xl border border-b-0 border-neutral-200 bg-white p-4 xs:p-6 sm:p-12 shadow-xs [&>:first-child]:mt-0! [&>:last-child]:mb-0!"
67
+ class="w-full max-w-full min-w-0 overflow-x-auto rounded-t-xl border border-b-0 bg-white dark:bg-neutral-800/15 p-4 xs:p-6 sm:p-12 shadow-xs [&>:first-child]:mt-0! [&>:last-child]:mb-0!"
68
68
  >
69
69
  <slot />
70
70
  </div>
71
71
  <div
72
- class="rd-component-preview__code not-prose relative"
72
+ class="rd-component-preview__code not-prose relative overflow-hidden rounded-b-xl bg-neutral-50 dark:bg-(--rd-code-surface)"
73
73
  data-rd-preview-expanded={isInitiallyExpanded ? "true" : "false"}
74
74
  style={{ "--rd-preview-visible-lines": String(visibleLines) }}
75
75
  >
@@ -84,17 +84,19 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
84
84
  collapsedLines={collapsedLines}
85
85
  />
86
86
  <div
87
- class="rd-component-preview__overlay pointer-events-none absolute inset-x-px inset-y-px hidden items-end justify-center rounded-b-xl bg-linear-to-t from-neutral-50/90 to-neutral-50/30 pb-4"
87
+ class="rd-component-preview__overlay pointer-events-none absolute inset-x-px inset-y-px hidden items-end justify-center rounded-b-xl pb-4"
88
88
  data-rd-preview-overlay
89
89
  aria-hidden="true"
90
90
  >
91
- <button
92
- type="button"
93
- class="pointer-events-auto inline-flex h-8 items-center justify-center rounded-md border border-neutral-200 bg-white px-3 text-sm font-medium text-neutral-700 shadow-md shadow-neutral-200 transition-colors duration-150 hover:bg-neutral-50 cursor-pointer"
94
- data-rd-preview-expand
95
- >
96
- View code
97
- </button>
91
+ <div class="bg-white dark:bg-neutral-800">
92
+ <button
93
+ type="button"
94
+ class="pointer-events-auto inline-flex h-8 items-center justify-center rounded-md border border-neutral-200 bg-white px-3 text-sm font-medium text-neutral-700 shadow-xl transition-colors duration-150 hover:bg-neutral-50 cursor-pointer dark:border-neutral-700 dark:bg-neutral-700/30 dark:text-neutral-200 dark:hover:bg-neutral-700/50"
95
+ data-rd-preview-expand
96
+ >
97
+ View code
98
+ </button>
99
+ </div>
98
100
  </div>
99
101
  </div>
100
102
  </div>
@@ -145,14 +147,38 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
145
147
  border-top-right-radius: 0 !important;
146
148
  }
147
149
 
150
+ :global(.dark) .rd-component-preview__code :global(.group\/prose-code > div) {
151
+ background-color: var(--rd-code-surface) !important;
152
+ }
153
+
148
154
  .rd-component-preview__code :global(.group\/prose-code pre),
149
155
  .rd-component-preview__code :global(.group\/prose-code code) {
150
156
  background-color: var(--color-neutral-50) !important;
151
157
  }
152
158
 
159
+ :global(.dark) .rd-component-preview__code :global(.group\/prose-code pre),
160
+ :global(.dark) .rd-component-preview__code :global(.group\/prose-code code) {
161
+ background-color: var(--rd-code-surface) !important;
162
+ }
163
+
153
164
  .rd-component-preview__code[data-rd-preview-expanded="false"]
154
165
  .rd-component-preview__overlay {
155
166
  display: flex;
167
+ background: linear-gradient(
168
+ to top,
169
+ rgb(250 250 250 / 90%),
170
+ rgb(250 250 250 / 30%)
171
+ );
172
+ }
173
+
174
+ :global(.dark)
175
+ .rd-component-preview__code[data-rd-preview-expanded="false"]
176
+ .rd-component-preview__overlay {
177
+ background: linear-gradient(
178
+ to top,
179
+ color-mix(in srgb, var(--rd-code-surface) 90%, transparent),
180
+ color-mix(in srgb, var(--rd-code-surface) 35%, transparent)
181
+ );
156
182
  }
157
183
 
158
184
  .rd-component-preview__code :global([data-rd-code-scroll-area]) {