radiant-docs 0.1.48 → 0.1.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "radiant-docs",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "description": "CLI tool for previewing Radiant documentation locally",
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,7 +64,7 @@ const darkLogoContainerStyle = `padding-top: ${darkLogo.paddingTop}px; padding-b
64
64
 
65
65
  <a
66
66
  href={logoHref}
67
- class="h-full flex items-center justify-center gap-2 lg:gap-3 text-xl font-bold text-neutral-800 dark:text-neutral-100 overflow-hidden"
67
+ class="h-full flex items-center justify-center gap-2 lg:gap-2.5 text-xl font-bold text-neutral-800 dark:text-neutral-100 overflow-hidden"
68
68
  >
69
69
  {
70
70
  lightLogoUrl || darkLogoUrl ? (
@@ -95,7 +95,7 @@ const darkLogoContainerStyle = `padding-top: ${darkLogo.paddingTop}px; padding-b
95
95
  logoPillText && (
96
96
  <span
97
97
  class:list={[
98
- "text-[10px] text-neutral-500 font-semibold bg-neutral-100 dark:bg-neutral-800 px-2 py-px rounded-full border border-neutral-200 dark:border-neutral-700/70 shadow-xs",
98
+ "text-[10px] text-neutral-500 dark:text-neutral-400 font-normal bg-neutral-100 dark:bg-neutral-800 px-2 py-px rounded-full border-[0.5px] border-neutral-900/8 dark:border-neutral-50/8",
99
99
  ]}
100
100
  >
101
101
  {logoPillText}
@@ -325,13 +325,26 @@ const hasMultipleRequests = requestSnippetItems.length > 1;
325
325
  isTransitioning: false,
326
326
  isManagedSlot: false,
327
327
  transitionDirection: 1,
328
- transitionDurationMs: 360,
328
+ transitionDurationMs: 400,
329
329
  transitionEasing: "cubic-bezier(0.22, 1, 0.36, 1)",
330
330
  tabSyncHandler: null,
331
331
  transitionTimeoutId: null,
332
332
  copyTimeoutId: null,
333
+ readMotionTokens() {
334
+ const styles = window.getComputedStyle(document.documentElement);
335
+ const configuredDurationMs = Number.parseFloat(
336
+ styles.getPropertyValue("--rd-panel-transition-duration-ms"),
337
+ );
338
+ this.transitionDurationMs = Number.isFinite(configuredDurationMs)
339
+ ? configuredDurationMs
340
+ : this.transitionDurationMs;
341
+ this.transitionEasing =
342
+ styles.getPropertyValue("--rd-panel-transition-easing").trim() ||
343
+ this.transitionEasing;
344
+ },
333
345
  init() {
334
346
  this.isManagedSlot = !!this.$root?.closest("[data-snippet-slot]");
347
+ this.readMotionTokens();
335
348
  if (
336
349
  typeof window.matchMedia === "function" &&
337
350
  window.matchMedia("(prefers-reduced-motion: reduce)").matches
@@ -348,6 +361,8 @@ const hasMultipleRequests = requestSnippetItems.length > 1;
348
361
  if (this.$refs.snippetPanels) {
349
362
  this.$refs.snippetPanels.style.transitionDuration =
350
363
  this.transitionDurationMs + "ms";
364
+ this.$refs.snippetPanels.style.transitionTimingFunction =
365
+ this.transitionEasing;
351
366
  }
352
367
  this.syncPill();
353
368
  this.syncSnippetHeight();
@@ -492,18 +507,18 @@ const hasMultipleRequests = requestSnippetItems.length > 1;
492
507
  class="flex h-full min-h-0 w-full max-w-full min-w-0 flex-col rounded-xl"
493
508
  >
494
509
  <div
495
- class="flex items-center justify-between gap-2 bg-(--rd-code-header-surface)"
510
+ class="flex items-center justify-between gap-2"
496
511
  >
497
512
  {
498
513
  hasMultipleRequests ? (
499
514
  <div class="min-w-0 flex-1 overflow-hidden rounded-t-xl">
500
515
  <div
501
516
  x-ref="tabList"
502
- class="relative flex min-w-0 items-end gap-1 overflow-x-auto pl-1 pr-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
517
+ class="relative flex min-w-0 items-end gap-1 overflow-x-auto overscroll-x-contain pl-1 pr-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
503
518
  >
504
519
  <div
505
520
  aria-hidden="true"
506
- class="pointer-events-none absolute top-1/2 z-0 h-[28px] -translate-y-1/2 rounded-[9px] border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface) transition-[left,width,opacity] duration-200 ease-out"
521
+ class="pointer-events-none absolute top-0 z-0 h-[28px] rounded-[9px] border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface) transition-[left,width,opacity] duration-200 ease-out"
507
522
  x-bind:class="pillVisible ? 'opacity-100' : 'opacity-0'"
508
523
  x-bind:style="'left:' + pillLeft + 'px;width:' + pillWidth + 'px;'"
509
524
  />
@@ -516,15 +531,17 @@ const hasMultipleRequests = requestSnippetItems.length > 1;
516
531
  type="button"
517
532
  x-bind:data-rd-snippet-tab="index"
518
533
  x-on:click="select(index)"
519
- class="relative z-10 inline-flex h-9 items-center gap-2 border-0 bg-transparent px-3 py-1.5 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer"
534
+ class="relative z-10 inline-flex h-9 items-start border-0 bg-transparent px-3 py-0 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer"
520
535
  x-bind:class="selected === index ? 'text-foreground' : 'text-muted-foreground'"
521
536
  >
522
- <span
523
- x-show="snippet.iconSvg"
524
- x-html="snippet.iconSvg"
525
- class="pointer-events-none inline-flex size-3.5 shrink-0 items-center rounded-[4px] transition-opacity duration-150"
526
- x-bind:class="selected === index ? 'opacity-100' : 'opacity-70'"></span>
527
- <span class="whitespace-pre leading-none" x-text="snippet.label"></span>
537
+ <span class="pointer-events-none inline-flex h-[28px] items-center gap-2">
538
+ <span
539
+ x-show="snippet.iconSvg"
540
+ x-html="snippet.iconSvg"
541
+ class="inline-flex size-3.5 shrink-0 items-center rounded-[4px] transition-opacity duration-150"
542
+ x-bind:class="selected === index ? 'opacity-100' : 'opacity-70'"></span>
543
+ <span class="whitespace-pre leading-none" x-text="snippet.label"></span>
544
+ </span>
528
545
  </button>
529
546
  </template>
530
547
  </div>
@@ -579,7 +596,8 @@ const hasMultipleRequests = requestSnippetItems.length > 1;
579
596
  <div class="relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-b-xl rounded-tl-xl border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface)">
580
597
  <div
581
598
  x-ref="snippetPanels"
582
- class="relative h-full overflow-auto transition-[height] duration-[360ms] ease-[cubic-bezier(0.22,1,0.36,1)] [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"
599
+ class="relative h-full overflow-auto transition-[height] motion-reduce:transition-none [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"
600
+ style="transition-duration: var(--rd-panel-transition-duration); transition-timing-function: var(--rd-panel-transition-easing);"
583
601
  >
584
602
  {
585
603
  requestSnippetItems.map((snippet, index) => (
@@ -74,9 +74,7 @@ import { Icon } from "astro-icon/components";
74
74
  <div
75
75
  class="flex h-full min-h-0 w-full max-w-full min-w-0 flex-col rounded-xl"
76
76
  >
77
- <div
78
- class="flex items-center justify-between gap-2 bg-(--rd-code-header-surface)"
79
- >
77
+ <div class="flex items-center justify-between gap-2">
80
78
  <div class="min-w-0 flex-1 overflow-hidden rounded-t-xl">
81
79
  <div
82
80
  x-ref="tabList"
@@ -84,7 +82,7 @@ import { Icon } from "astro-icon/components";
84
82
  >
85
83
  <div
86
84
  aria-hidden="true"
87
- class="pointer-events-none absolute top-1/2 z-0 h-[28px] -translate-y-1/2 rounded-[9px] border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface) transition-[left,width,opacity] duration-200 ease-out"
85
+ class="pointer-events-none absolute top-0 z-0 h-[28px] rounded-[9px] border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface) transition-[left,width,opacity] duration-200 ease-out"
88
86
  x-bind:class="pillVisible ? 'opacity-100' : 'opacity-0'"
89
87
  x-bind:style="'left:' + pillLeft + 'px;width:' + pillWidth + 'px;'"
90
88
  >
@@ -95,11 +93,12 @@ import { Icon } from "astro-icon/components";
95
93
  type="button"
96
94
  x-bind:data-rd-response-tab="tab.id"
97
95
  x-on:click="select(tab.id)"
98
- class="relative z-10 inline-flex h-9 items-center gap-2 border-0 bg-transparent px-3 py-1.5 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer"
96
+ class="relative z-10 inline-flex h-9 items-start border-0 bg-transparent px-3 py-0 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer"
99
97
  x-bind:class="selected === tab.id ? 'text-foreground' : 'text-muted-foreground'"
100
98
  >
101
- <span class="whitespace-pre leading-none" x-text="tab.label"
102
- ></span>
99
+ <span
100
+ class="pointer-events-none inline-flex h-[28px] items-center whitespace-pre leading-none"
101
+ x-text="tab.label"></span>
103
102
  </button>
104
103
  </template>
105
104
  </div>
@@ -147,7 +146,7 @@ import { Icon } from "astro-icon/components";
147
146
  x-show="!response || !response.highlightedData"
148
147
  class="text-sm px-4 py-4 xs:py-8 flex flex-col items-center justify-center h-full text-foreground"
149
148
  >
150
- <div class="bg-(--rd-code-header-surface) p-2 rounded-xl mb-1">
149
+ <div class="p-2 rounded-xl mb-1">
151
150
  <Icon
152
151
  class="size-6 text-neutral-300 dark:text-neutral-600"
153
152
  name="lucide:square-arrow-up-right"
@@ -171,7 +170,7 @@ import { Icon } from "astro-icon/components";
171
170
  x-show="!response || !response.highlightedHeaders"
172
171
  class="text-sm px-4 py-4 xs:py-8 flex flex-col items-center justify-center h-full text-foreground"
173
172
  >
174
- <div class="bg-(--rd-code-header-surface) p-2 rounded-xl mb-1">
173
+ <div class="p-2 rounded-xl mb-1">
175
174
  <Icon
176
175
  class="size-6 text-neutral-300 dark:text-neutral-600"
177
176
  name="lucide:square-arrow-up-right"
@@ -248,13 +248,26 @@ const hasMultipleResponses = responseSnippetItems.length > 1;
248
248
  isTransitioning: false,
249
249
  isManagedSlot: false,
250
250
  transitionDirection: 1,
251
- transitionDurationMs: 360,
251
+ transitionDurationMs: 400,
252
252
  transitionEasing: "cubic-bezier(0.22, 1, 0.36, 1)",
253
253
  tabSyncHandler: null,
254
254
  transitionTimeoutId: null,
255
255
  copyTimeoutId: null,
256
+ readMotionTokens() {
257
+ const styles = window.getComputedStyle(document.documentElement);
258
+ const configuredDurationMs = Number.parseFloat(
259
+ styles.getPropertyValue("--rd-panel-transition-duration-ms"),
260
+ );
261
+ this.transitionDurationMs = Number.isFinite(configuredDurationMs)
262
+ ? configuredDurationMs
263
+ : this.transitionDurationMs;
264
+ this.transitionEasing =
265
+ styles.getPropertyValue("--rd-panel-transition-easing").trim() ||
266
+ this.transitionEasing;
267
+ },
256
268
  init() {
257
269
  this.isManagedSlot = !!this.$root?.closest("[data-snippet-slot]");
270
+ this.readMotionTokens();
258
271
  if (
259
272
  typeof window.matchMedia === "function" &&
260
273
  window.matchMedia("(prefers-reduced-motion: reduce)").matches
@@ -271,6 +284,8 @@ const hasMultipleResponses = responseSnippetItems.length > 1;
271
284
  if (this.$refs.snippetPanels) {
272
285
  this.$refs.snippetPanels.style.transitionDuration =
273
286
  this.transitionDurationMs + "ms";
287
+ this.$refs.snippetPanels.style.transitionTimingFunction =
288
+ this.transitionEasing;
274
289
  }
275
290
  this.syncPill();
276
291
  this.syncSnippetHeight();
@@ -415,7 +430,7 @@ const hasMultipleResponses = responseSnippetItems.length > 1;
415
430
  class="flex h-full min-h-0 w-full max-w-full min-w-0 flex-col rounded-xl"
416
431
  >
417
432
  <div
418
- class="flex items-center justify-between gap-2 bg-(--rd-code-header-surface)"
433
+ class="flex items-center justify-between gap-2"
419
434
  >
420
435
  {
421
436
  hasMultipleResponses ? (
@@ -426,7 +441,7 @@ const hasMultipleResponses = responseSnippetItems.length > 1;
426
441
  >
427
442
  <div
428
443
  aria-hidden="true"
429
- class="pointer-events-none absolute top-1/2 z-0 h-[28px] -translate-y-1/2 rounded-[9px] border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface) transition-[left,width,opacity] duration-200 ease-out"
444
+ class="pointer-events-none absolute top-0 z-0 h-[28px] rounded-[9px] border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface) transition-[left,width,opacity] duration-200 ease-out"
430
445
  x-bind:class="pillVisible ? 'opacity-100' : 'opacity-0'"
431
446
  x-bind:style="'left:' + pillLeft + 'px;width:' + pillWidth + 'px;'"
432
447
  />
@@ -439,13 +454,15 @@ const hasMultipleResponses = responseSnippetItems.length > 1;
439
454
  type="button"
440
455
  x-bind:data-rd-snippet-tab="index"
441
456
  x-on:click="select(index)"
442
- class="relative z-10 inline-flex h-9 items-center gap-2 border-0 bg-transparent px-3 py-1.5 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer"
457
+ class="relative z-10 inline-flex h-9 items-start border-0 bg-transparent px-3 py-0 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer"
443
458
  x-bind:class="selected === index ? 'text-foreground' : 'text-muted-foreground'"
444
459
  >
445
- <span
446
- class="size-[7px] shrink-0 rounded-full transition-opacity duration-150"
447
- x-bind:class="(selected === index ? 'opacity-100 ' : 'opacity-70 ') + snippet.dotClass"></span>
448
- <span class="whitespace-pre leading-none" x-text="snippet.statusCode"></span>
460
+ <span class="pointer-events-none inline-flex h-[28px] items-center gap-2">
461
+ <span
462
+ class="size-[7px] shrink-0 rounded-full transition-opacity duration-150"
463
+ x-bind:class="(selected === index ? 'opacity-100 ' : 'opacity-70 ') + snippet.dotClass"></span>
464
+ <span class="whitespace-pre leading-none" x-text="snippet.statusCode"></span>
465
+ </span>
449
466
  </button>
450
467
  </template>
451
468
  </div>
@@ -499,7 +516,8 @@ const hasMultipleResponses = responseSnippetItems.length > 1;
499
516
  <div class="relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-b-xl rounded-tl-xl border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface)">
500
517
  <div
501
518
  x-ref="snippetPanels"
502
- class="relative h-full overflow-auto transition-[height] duration-[360ms] ease-[cubic-bezier(0.22,1,0.36,1)] [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"
519
+ class="relative h-full overflow-auto transition-[height] motion-reduce:transition-none [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"
520
+ style="transition-duration: var(--rd-panel-transition-duration); transition-timing-function: var(--rd-panel-transition-easing);"
503
521
  >
504
522
  {
505
523
  responseSnippetItems.map((snippet, index) => (
@@ -50,7 +50,7 @@ validateProps(
50
50
  }`
51
51
  x-init="id = (typeof register === 'function') ? register() : Math.random()"
52
52
  role="region"
53
- class="rd-accordion block border-b border-neutral-800/10 dark:border-neutral-700/50 last:border-b-0. [&:first-child>h4>button]:pt-0 [&:last-child>div>div]:pb-0"
53
+ class="rd-accordion block border-b border-neutral-800/10 dark:border-neutral-700/50 last:border-b-0. [&:first-child>h4>button]:pt-0"
54
54
  >
55
55
  <h4 class="not-prose">
56
56
  <button
@@ -70,9 +70,23 @@ validateProps(
70
70
  />
71
71
  </button>
72
72
  </h4>
73
- <div x-show="expanded" x-collapse>
74
- <div class="prose-rules max-w-none! *:max-w-none! pb-4!">
75
- <slot />
73
+ <div
74
+ class:list={[
75
+ "grid overflow-hidden transition-[grid-template-rows,opacity] motion-reduce:transition-none",
76
+ defaultOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
77
+ ]}
78
+ :class="expanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'"
79
+ :aria-hidden="(!expanded).toString()"
80
+ x-bind:inert="!expanded"
81
+ style="transition-duration: var(--rd-panel-transition-duration); transition-timing-function: var(--rd-panel-transition-easing);"
82
+ >
83
+ <div class="min-h-0 overflow-hidden">
84
+ <div
85
+ data-rd-accordion-content-inner
86
+ class="prose-rules max-w-none! *:max-w-none! pb-4!"
87
+ >
88
+ <slot />
89
+ </div>
76
90
  </div>
77
91
  </div>
78
92
  </div>
@@ -16,6 +16,7 @@ interface Props {
16
16
  showLineNumbers?: boolean | string;
17
17
  hideLanguageIcon?: boolean | string;
18
18
  inCodeGroup?: boolean | string;
19
+ flushTop?: boolean | string;
19
20
  highlightedLines?: string;
20
21
  collapsedLines?: string;
21
22
  }
@@ -28,6 +29,7 @@ const {
28
29
  showLineNumbers = false,
29
30
  hideLanguageIcon = false,
30
31
  inCodeGroup = false,
32
+ flushTop = false,
31
33
  highlightedLines = "",
32
34
  collapsedLines = "",
33
35
  } = Astro.props as Props;
@@ -191,6 +193,7 @@ const parsedShowFilename = parsedInCodeGroup
191
193
  : toBoolean(showFilename, false);
192
194
  const parsedShowLineNumbers = toBoolean(showLineNumbers, false);
193
195
  const parsedHideLanguageIcon = toBoolean(hideLanguageIcon, false);
196
+ const parsedFlushTop = !parsedInCodeGroup && toBoolean(flushTop, false);
194
197
  const shouldRenderLanguageIcon = !parsedHideLanguageIcon;
195
198
 
196
199
  const trimmedFilename = filename.trim();
@@ -262,7 +265,8 @@ const renderedCodeLinesHtml = normalizedTokenLines
262
265
 
263
266
  <div
264
267
  class:list={[
265
- "group/prose-code not-prose relative w-full max-w-full min-w-0 rounded-xl",
268
+ "group/prose-code not-prose relative w-full max-w-full min-w-0",
269
+ parsedFlushTop ? "rounded-b-xl" : "rounded-xl",
266
270
  parsedInCodeGroup ? "my-0" : "rd-prose-block",
267
271
  ]}
268
272
  data-rd-code-block-root="true"
@@ -279,12 +283,16 @@ const renderedCodeLinesHtml = normalizedTokenLines
279
283
  <div
280
284
  class:list={[
281
285
  "w-full max-w-full min-w-0 bg-(--rd-code-surface)",
282
- parsedInCodeGroup ? "rounded-tr-none rounded-xl" : "rounded-xl",
286
+ parsedInCodeGroup
287
+ ? "rounded-tr-none rounded-xl"
288
+ : parsedFlushTop
289
+ ? "rounded-b-xl"
290
+ : "rounded-xl",
283
291
  ]}
284
292
  >
285
293
  {
286
294
  !parsedInCodeGroup && parsedShowFilename ? (
287
- <div class="flex items-center justify-between gap-2 border-b-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-header-surface)">
295
+ <div class="flex items-center justify-between gap-2 border-b-[0.5px] border-(--rd-code-tab-edge-border)">
288
296
  <div class="min-w-0 flex-1">
289
297
  <div class="relative h-9 w-fit max-w-full rounded-tl-xl bg-(--rd-code-surface)">
290
298
  <div class="absolute inset-x-0 -bottom-px h-px bg-(--rd-code-surface)" />
@@ -331,7 +339,9 @@ const renderedCodeLinesHtml = normalizedTokenLines
331
339
 
332
340
  <div
333
341
  class:list={[
334
- "relative min-w-0 border-[0.5px] rounded-xl overflow-hidden border-(--rd-code-tab-edge-border)",
342
+ "relative min-w-0 border-[0.5px] overflow-hidden border-(--rd-code-tab-edge-border)",
343
+ parsedFlushTop ? "rounded-b-xl" : "rounded-xl",
344
+ parsedFlushTop && "border-t-0",
335
345
  parsedShowFilename && "border-t-0 rounded-t-none",
336
346
  parsedInCodeGroup && "rounded-tl-xl border-0!",
337
347
  ]}
@@ -7,19 +7,17 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
7
7
  class="rd-prose-block group/prose-code-group not-prose relative w-full max-w-full min-w-0 rounded-xl"
8
8
  data-rd-code-group-root="true"
9
9
  >
10
- <div
11
- class="relative z-10 overflow-visible rounded-t-xl bg-(--rd-code-header-surface)"
12
- >
10
+ <div class="relative z-10 overflow-visible rounded-t-xl">
13
11
  <div class="flex min-w-0 items-end justify-between gap-2">
14
12
  <div class="min-w-0 flex-1 overflow-hidden rounded-t-xl">
15
13
  <div
16
14
  data-rd-code-group-tabs
17
- class="relative flex min-w-0 items-end gap-1 overflow-x-auto pl-1 pr-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
15
+ class="relative flex min-w-0 items-end gap-1 overflow-x-auto overscroll-x-contain pl-1 pr-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
18
16
  >
19
17
  <div
20
18
  data-rd-code-group-pill
21
19
  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-(--rd-code-tab-edge-border) bg-(--rd-code-surface) opacity-0 transition-[left,width,opacity] duration-200 ease-out"
20
+ class="pointer-events-none absolute top-0 z-0 h-[28px] rounded-[9px] border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface) opacity-0 transition-[left,width,opacity] duration-200 ease-out"
23
21
  >
24
22
  </div>
25
23
  </div>
@@ -58,7 +56,8 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
58
56
 
59
57
  <div
60
58
  data-rd-code-group-content
61
- class="relative min-w-0 overflow-hidden transition-[height] duration-[360ms] ease-[cubic-bezier(0.22,1,0.36,1)] bg-(--rd-code-surface) rounded-xl rounded-tr-none border-[0.5px] border-(--rd-code-tab-edge-border)"
59
+ class="relative min-w-0 overflow-hidden transition-[height] bg-(--rd-code-surface) rounded-xl rounded-tr-none border-[0.5px] border-(--rd-code-tab-edge-border)"
60
+ style="transition-duration: var(--rd-panel-transition-duration); transition-timing-function: var(--rd-panel-transition-easing);"
62
61
  >
63
62
  <slot />
64
63
  </div>
@@ -91,8 +90,20 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
91
90
  const prefersReducedMotion =
92
91
  typeof window.matchMedia === "function" &&
93
92
  window.matchMedia("(prefers-reduced-motion: reduce)").matches;
94
- const TRANSITION_DURATION_MS = prefersReducedMotion ? 0 : 360;
95
- const TRANSITION_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
93
+ const rootStyles = window.getComputedStyle(document.documentElement);
94
+ const configuredDurationMs = Number.parseFloat(
95
+ rootStyles.getPropertyValue("--rd-panel-transition-duration-ms"),
96
+ );
97
+ const configuredEasing = rootStyles
98
+ .getPropertyValue("--rd-panel-transition-easing")
99
+ .trim();
100
+ const TRANSITION_DURATION_MS = prefersReducedMotion
101
+ ? 0
102
+ : Number.isFinite(configuredDurationMs)
103
+ ? configuredDurationMs
104
+ : 400;
105
+ const TRANSITION_EASING =
106
+ configuredEasing || "cubic-bezier(0.22, 1, 0.36, 1)";
96
107
 
97
108
  let activeIndex = 0;
98
109
  let transitionTimeoutId = null;
@@ -138,10 +149,14 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
138
149
 
139
150
  const tabButton = document.createElement("button");
140
151
  tabButton.type = "button";
141
- tabButton.className = `relative inline-flex h-9 items-center border-0 bg-transparent px-3 py-1.5 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer ${hasTabIcon ? "gap-2" : ""}`;
152
+ tabButton.className =
153
+ "relative inline-flex h-9 items-start border-0 bg-transparent px-3 py-0 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer";
142
154
  tabButton.setAttribute("aria-label", filename);
143
155
  tabButton.setAttribute("data-rd-code-group-tab", String(index));
144
156
 
157
+ const tabContent = document.createElement("span");
158
+ tabContent.className = `pointer-events-none inline-flex h-[28px] items-center ${hasTabIcon ? "gap-2" : ""}`;
159
+
145
160
  let iconContainer = null;
146
161
  if (hasTabIcon) {
147
162
  iconContainer = document.createElement("span");
@@ -155,9 +170,10 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
155
170
  labelElement.textContent = filename;
156
171
 
157
172
  if (iconContainer) {
158
- tabButton.appendChild(iconContainer);
173
+ tabContent.appendChild(iconContainer);
159
174
  }
160
- tabButton.appendChild(labelElement);
175
+ tabContent.appendChild(labelElement);
176
+ tabButton.appendChild(tabContent);
161
177
  tabWrapper.appendChild(tabButton);
162
178
  tabsElement.appendChild(tabWrapper);
163
179
 
@@ -182,6 +198,7 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
182
198
  });
183
199
 
184
200
  contentElement.style.transitionDuration = `${TRANSITION_DURATION_MS}ms`;
201
+ contentElement.style.transitionTimingFunction = TRANSITION_EASING;
185
202
 
186
203
  const syncPill = () => {
187
204
  const activeTab = tabs[activeIndex]?.tabWrapper;
@@ -60,16 +60,20 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
60
60
  ---
61
61
 
62
62
  <div
63
- class="rd-prose-block rd-component-preview flex w-full max-w-full min-w-0 flex-col"
63
+ class="rd-prose-block rd-component-preview isolate flex w-full max-w-full min-w-0 flex-col"
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 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!"
67
+ class="relative z-10 w-full max-w-full min-w-0 rounded-t-xl border-[0.5px] border-b-0 border-(--rd-code-tab-edge-border) bg-(--rd-code-surface) p-2"
68
68
  >
69
- <slot />
69
+ <div
70
+ class="bg-white dark:bg-neutral-900/70 rounded-xl border-[0.5px] border-neutral-900/10 dark:border-white/8 p-4 xs:p-6 sm:p-9 sm:px-12 *:first:mt-0! *:last:mb-0! shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-8px_rgba(0,0,0,0.08)] dark:shadow-[0_-0.5px_1px_rgba(255,255,255,0.1),0_5px_12px_-4px_rgba(0,0,0,0.15)] overflow-x-auto"
71
+ >
72
+ <slot />
73
+ </div>
70
74
  </div>
71
75
  <div
72
- class="rd-component-preview__code not-prose relative overflow-hidden rounded-b-xl bg-neutral-50 dark:bg-(--rd-code-surface)"
76
+ class="rd-component-preview__code not-prose relative z-0 overflow-hidden rounded-b-xl"
73
77
  data-rd-preview-expanded={isInitiallyExpanded ? "true" : "false"}
74
78
  style={{ "--rd-preview-visible-lines": String(visibleLines) }}
75
79
  >
@@ -82,16 +86,19 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
82
86
  hideLanguageIcon={hideLanguageIcon}
83
87
  highlightedLines={highlightedLines}
84
88
  collapsedLines={collapsedLines}
89
+ flushTop
85
90
  />
86
91
  <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 pb-4"
92
+ class="rd-component-preview__overlay pointer-events-none absolute inset-0 hidden items-end justify-center rounded-b-xl pb-4"
88
93
  data-rd-preview-overlay
89
94
  aria-hidden="true"
90
95
  >
91
- <div class="bg-white dark:bg-neutral-800">
96
+ <div
97
+ class="bg-white dark:bg-neutral-800 rounded-xl [corner-shape:superellipse(1.2)]"
98
+ >
92
99
  <button
93
100
  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"
101
+ class="pointer-events-auto inline-flex h-8 items-center justify-center rounded-xl [corner-shape:superellipse(1.2)] border-[0.5px] border-neutral-900/10 dark:border-white/8 bg-linear-to-br from-white via-white/10 to-neutral-900/5 dark:from-white/7 dark:via-white/6 dark:to-white/2 px-3 text-sm font-medium text-neutral-600 hover:text-neutral-900 dark:text-neutral-300/90 hover:dark:text-neutral-200 transition-colors duration-200 hover:bg-neutral-50/80 cursor-pointer dark:hover:bg-neutral-700/30 shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-4px_rgba(0,0,0,0.08)] dark:shadow-[0_-0.5px_0px_rgba(255,255,255,0.15),0_5px_12px_-4px_rgba(0,0,0,0.6)]"
95
102
  data-rd-preview-expand
96
103
  >
97
104
  View code
@@ -174,36 +181,37 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
174
181
  .rd-component-preview__code[data-rd-preview-expanded="false"]
175
182
  .rd-component-preview__overlay {
176
183
  display: flex;
177
- background: linear-gradient(
178
- to top,
179
- rgb(250 250 250 / 90%),
180
- rgb(250 250 250 / 30%)
181
- );
182
- }
183
-
184
- :global(.dark)
185
- .rd-component-preview__code[data-rd-preview-expanded="false"]
186
- .rd-component-preview__overlay {
187
- background: linear-gradient(
188
- to top,
189
- color-mix(in srgb, var(--rd-code-surface) 90%, transparent),
190
- color-mix(in srgb, var(--rd-code-surface) 35%, transparent)
191
- );
192
184
  }
193
185
 
194
186
  .rd-component-preview__code :global([data-rd-code-scroll-area]) {
195
187
  overflow-y: hidden;
196
- transition: max-height 280ms cubic-bezier(0.22, 1, 0.36, 1);
188
+ transition:
189
+ max-height 280ms cubic-bezier(0.22, 1, 0.36, 1),
190
+ mask-image 180ms ease-out;
197
191
  }
198
192
 
199
193
  .rd-component-preview__code[data-rd-preview-expanded="false"]
200
194
  :global([data-rd-code-scroll-area]) {
201
- max-height: calc(var(--rd-preview-visible-lines, 5) * 1.5rem + 1.25rem);
195
+ max-height: calc(var(--rd-preview-visible-lines, 5) * 1.5rem + 0.75rem);
196
+ -webkit-mask-image: linear-gradient(
197
+ to bottom,
198
+ rgb(0 0 0 / 80%) 0%,
199
+ rgb(0 0 0 / 32%) 82%,
200
+ transparent 110%
201
+ );
202
+ mask-image: linear-gradient(
203
+ to bottom,
204
+ rgb(0 0 0 / 80%) 0%,
205
+ rgb(0 0 0 / 32%) 82%,
206
+ transparent 110%
207
+ );
202
208
  }
203
209
 
204
210
  .rd-component-preview__code[data-rd-preview-expanded="true"]
205
211
  :global([data-rd-code-scroll-area]) {
206
212
  max-height: var(--rd-preview-expanded-height, 200rem);
213
+ -webkit-mask-image: none;
214
+ mask-image: none;
207
215
  }
208
216
  </style>
209
217
 
@@ -1,10 +1,9 @@
1
1
  ---
2
2
  import { validateNoUnknownProps, validateProps } from "../../lib/component-error";
3
- import { renderMarkdown } from "../../lib/utils";
4
3
  import { resolveStaticAssetUrl } from "../../lib/static-asset-url";
5
4
 
6
5
  interface Props {
7
- src: string;
6
+ src: string | { light?: string; dark?: string };
8
7
  alt?: string;
9
8
  title?: string;
10
9
  width?: number | string;
@@ -24,7 +23,7 @@ validateProps(
24
23
  "Image",
25
24
  imageProps,
26
25
  {
27
- src: { required: true, type: "string" },
26
+ src: { required: true, type: ["string", "object"] },
28
27
  alt: { type: "string" },
29
28
  title: { type: "string" },
30
29
  width: { type: ["number", "string"] },
@@ -35,17 +34,51 @@ validateProps(
35
34
 
36
35
  const { src, alt, title, width, zoom = true } = imageProps as Props;
37
36
  const zoomEnabled = zoom !== false;
38
- const resolvedSrc = resolveStaticAssetUrl(src);
39
37
  const rawWidth = width;
40
- const imageAttrs: Record<string, unknown> = { src: resolvedSrc };
41
- if (typeof alt === "string") {
42
- imageAttrs.alt = alt;
38
+
39
+ function normalizeImageSource(value: Props["src"]): {
40
+ light: string;
41
+ dark?: string;
42
+ } {
43
+ if (typeof value === "string") {
44
+ return { light: value };
45
+ }
46
+
47
+ const light = typeof value.light === "string" ? value.light : "";
48
+ const dark = typeof value.dark === "string" ? value.dark : undefined;
49
+
50
+ return { light, dark };
51
+ }
52
+
53
+ const normalizedSource = normalizeImageSource(src);
54
+ if (!normalizedSource.light.trim()) {
55
+ throw new Error(
56
+ `[USER_ERROR]: <Image>: Invalid prop "src": object form requires a non-empty "light" string (in ${Astro.url.pathname})`,
57
+ );
43
58
  }
44
- if (width !== undefined) {
45
- imageAttrs.width = width;
59
+
60
+ const resolvedLightSrc = resolveStaticAssetUrl(normalizedSource.light);
61
+ const resolvedDarkSrc =
62
+ typeof normalizedSource.dark === "string" && normalizedSource.dark.trim()
63
+ ? resolveStaticAssetUrl(normalizedSource.dark)
64
+ : undefined;
65
+ const hasDarkSource =
66
+ typeof resolvedDarkSrc === "string" && resolvedDarkSrc !== resolvedLightSrc;
67
+
68
+ function createImageAttrs(source: string): Record<string, unknown> {
69
+ const attrs: Record<string, unknown> = { src: source };
70
+ if (typeof alt === "string") {
71
+ attrs.alt = alt;
72
+ }
73
+ if (width !== undefined) {
74
+ attrs.width = width;
75
+ }
76
+ return attrs;
46
77
  }
47
78
 
48
- const zoomAttrs: Record<string, unknown> = { src: resolvedSrc };
79
+ const lightImageAttrs = createImageAttrs(resolvedLightSrc);
80
+ const darkImageAttrs = hasDarkSource ? createImageAttrs(resolvedDarkSrc!) : null;
81
+ const zoomAttrs: Record<string, unknown> = { src: resolvedLightSrc };
49
82
  if (typeof alt === "string") {
50
83
  zoomAttrs.alt = alt;
51
84
  }
@@ -83,15 +116,15 @@ function isConstrainedWidthValue(value: unknown): boolean {
83
116
 
84
117
  const hasCustomImageWidth = isConstrainedWidthValue(rawWidth);
85
118
 
86
- const captionHtml =
87
- typeof title === "string" && title.trim().length > 0
88
- ? (await renderMarkdown(title)).replace(/^<p>([\s\S]*)<\/p>\n?$/, "$1")
89
- : "";
119
+ const slotCaptionHtml = Astro.slots.has("default")
120
+ ? (await Astro.slots.render("default")).trim()
121
+ : "";
122
+ const hasCaption = slotCaptionHtml.length > 0;
90
123
  ---
91
124
 
92
125
  <figure
93
126
  class:list={[
94
- "rd-prose-block p-1.5 pb-1 xs:pb-1.5 group border border-neutral-200 dark:border-neutral-800 shadow-xs bg-neutral-50 dark:bg-(--rd-code-surface) rounded-2xl",
127
+ "rd-prose-block p-1.5 pb-1 xs:pb-1.5 group border-[0.5px] border-neutral-900/8 dark:border-white/6 bg-(--rd-code-surface) rounded-2xl shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-8px_rgba(0,0,0,0.08)] dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-8px_rgba(0,0,0,0.2)]",
95
128
  hasCustomImageWidth ? "w-fit max-w-full mx-auto" : "w-full",
96
129
  ]}
97
130
  x-data="{
@@ -102,8 +135,16 @@ const captionHtml =
102
135
  style: 'visibility: hidden;',
103
136
  fullShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
104
137
  noShadow: '0 0 0 rgba(0, 0, 0, 0)',
138
+ zoomSrc: '',
139
+ activeImage() {
140
+ const darkImg = this.$refs.darkImg;
141
+ if (darkImg && window.getComputedStyle(darkImg).display !== 'none') {
142
+ return darkImg;
143
+ }
144
+ return this.$refs.lightImg;
145
+ },
105
146
  computeZoomSize() {
106
- const img = this.$refs.img;
147
+ const img = this.activeImage();
107
148
  const zoomContainer = this.$refs.zoomViewport;
108
149
  if (!img || !zoomContainer) {
109
150
  this.zoomSize = `width: min(92vw, ${this.zoomMaxWidth}px);`;
@@ -138,9 +179,12 @@ const captionHtml =
138
179
  return this.zoomSize;
139
180
  },
140
181
  async zoom() {
182
+ const img = this.activeImage();
183
+ if (!img) return;
141
184
  // 1. Lock scroll and measure
142
185
  document.body.style.overflow = 'hidden';
143
- const rect = this.$refs.img.getBoundingClientRect();
186
+ this.zoomSrc = img.currentSrc || img.src;
187
+ const rect = img.getBoundingClientRect();
144
188
 
145
189
  // 2. Prepare the zoomed image (hidden but in DOM)
146
190
  this.style = 'opacity: 0; transition: none;';
@@ -176,7 +220,9 @@ const captionHtml =
176
220
  },
177
221
  close() {
178
222
  document.body.style.overflow = 'auto';
179
- const rect = this.$refs.img.getBoundingClientRect();
223
+ const img = this.activeImage();
224
+ if (!img) return;
225
+ const rect = img.getBoundingClientRect();
180
226
  const zRect = this.$refs.zoomedImg.getBoundingClientRect();
181
227
 
182
228
  const scale = rect.width / zRect.width;
@@ -194,25 +240,42 @@ const captionHtml =
194
240
  }"
195
241
  >
196
242
  <div
197
- class="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-(--rd-code-surface)"
243
+ class="overflow-hidden rounded-[11px]"
198
244
  >
199
245
  <img
200
- {...imageAttrs}
201
- x-ref="img"
246
+ {...lightImageAttrs}
247
+ x-ref="lightImg"
202
248
  title={title}
203
249
  class:list={[
204
250
  "h-auto my-0! block transition-opacity",
251
+ hasDarkSource && "dark:hidden",
205
252
  zoomEnabled && "cursor-zoom-in",
206
253
  hasCustomImageWidth ? "max-w-full" : "w-full",
207
254
  ]}
208
255
  :class="showZoomed ? 'opacity-0 duration-0' : 'opacity-100 duration-300'"
209
256
  @click={zoomEnabled ? "zoom()" : undefined}
210
257
  />
258
+ {
259
+ darkImageAttrs && (
260
+ <img
261
+ {...darkImageAttrs}
262
+ x-ref="darkImg"
263
+ title={title}
264
+ class:list={[
265
+ "h-auto my-0! hidden transition-opacity dark:block",
266
+ zoomEnabled && "cursor-zoom-in",
267
+ hasCustomImageWidth ? "max-w-full" : "w-full",
268
+ ]}
269
+ :class="showZoomed ? 'opacity-0 duration-0' : 'opacity-100 duration-300'"
270
+ @click={zoomEnabled ? "zoom()" : undefined}
271
+ />
272
+ )
273
+ }
211
274
  </div>
212
275
  {
213
- title && (
214
- <figcaption class="mt-1! xs:mt-1.5! text-center text-xs! xs:text-sm! text-neutral-500 dark:text-neutral-400 font-medium whitespace-pre-line leading-relaxed px-2">
215
- <span set:html={captionHtml} />
276
+ hasCaption && (
277
+ <figcaption class="prose-rules mt-1! xs:mt-1.5! max-w-none! *:max-w-none! text-center text-xs! xs:text-sm! text-neutral-500 dark:text-neutral-400 leading-relaxed px-2">
278
+ <Fragment set:html={slotCaptionHtml} />
216
279
  </figcaption>
217
280
  )
218
281
  }
@@ -243,6 +306,7 @@ const captionHtml =
243
306
  <img
244
307
  {...zoomAttrs}
245
308
  x-ref="zoomedImg"
309
+ :src="zoomSrc || $el.getAttribute('src')"
246
310
  class="relative z-10 max-w-full max-h-full object-contain rounded-2xl shadow-none will-change-transform pointer-events-none"
247
311
  :style="style"
248
312
  />
@@ -21,14 +21,14 @@ validateProps(
21
21
  <div
22
22
  class:list={[
23
23
  "relative pl-10 step-item pb-4 last:pb-0 space-y-4",
24
- "before:absolute before:left-[10.5px] before:top-8 before:bottom-0 before:w-px before:bg-linear-[transparent,var(--color-neutral-200)_10%,var(--color-neutral-200)_90%,transparent] dark:before:bg-linear-[transparent,var(--color-neutral-700)_10%,var(--color-neutral-700)_90%,transparent]",
24
+ "before:mask-b-from-90% before:absolute before:left-[13.25px] before:top-[28px] before:bottom-0 before:w-[1.5px] before:bg-neutral-900/8 dark:before:bg-neutral-50/8",
25
25
  ]}
26
26
  data-step-panel
27
27
  >
28
28
  <div
29
29
  class:list={[
30
30
  "flex items-center gap-1.5 not-prose",
31
- "step-number before:size-6 before:bg-linear-to-b before:from-neutral-900/80 before:to-neutral-900 dark:before:from-neutral-100 dark:before:to-neutral-200 before:rounded-full before:text-white before:flex before:items-center before:justify-center before:text-xs before:font-bold dark:before:font-extrabold before:absolute before:left-px before:top-[3px] before:shadow-sm dark:before:bg-neutral-200 dark:before:text-neutral-900",
31
+ "step-number before:leading-none before:size-7 before:bg-neutral-900/6 dark:before:bg-neutral-50/6 before:rounded-full before:text-neutral-700 dark:before:text-neutral-100 before:flex before:items-center before:justify-center before:text-[13px] before:font-medium before:absolute before:left-0 before:top-0",
32
32
  ]}
33
33
  >
34
34
  <h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
@@ -30,12 +30,33 @@ if (labels.length === 0) {
30
30
  previousTab: null,
31
31
  transitionDirection: 1,
32
32
  isTransitioning: false,
33
- transitionDurationMs: 300,
33
+ transitionDurationMs: 400,
34
+ transitionEasing: 'cubic-bezier(0.22, 1, 0.36, 1)',
34
35
  transitionTimeout: null,
35
36
  containerHeight: 'auto',
36
37
  markerStyle: { left: null, width: null },
37
38
  resizeHandler: null,
39
+ readMotionTokens() {
40
+ const styles = window.getComputedStyle(document.documentElement);
41
+ const configuredDurationMs = Number.parseFloat(
42
+ styles.getPropertyValue('--rd-panel-transition-duration-ms'),
43
+ );
44
+ this.transitionDurationMs = Number.isFinite(configuredDurationMs)
45
+ ? configuredDurationMs
46
+ : this.transitionDurationMs;
47
+ this.transitionEasing =
48
+ styles.getPropertyValue('--rd-panel-transition-easing').trim() ||
49
+ this.transitionEasing;
50
+
51
+ if (
52
+ typeof window.matchMedia === 'function' &&
53
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
54
+ ) {
55
+ this.transitionDurationMs = 0;
56
+ }
57
+ },
38
58
  init() {
59
+ this.readMotionTokens();
39
60
  this.resizeHandler = () => {
40
61
  this.updateMarker(this.activeTab);
41
62
  this.updateHeight(this.isTransitioning);
@@ -68,7 +89,7 @@ if (labels.length === 0) {
68
89
  this.isTransitioning = true;
69
90
  this.activeTab = index;
70
91
  this.updateMarker(this.activeTab);
71
- this.updateHeight(true);
92
+ this.updateHeight();
72
93
 
73
94
  this.transitionTimeout = window.setTimeout(() => {
74
95
  this.isTransitioning = false;
@@ -87,7 +108,7 @@ if (labels.length === 0) {
87
108
  }
88
109
  },
89
110
  getPanelStyle(index) {
90
- const base = 'position: absolute; inset: 0;';
111
+ const base = 'position: absolute; top: 0; left: 0; right: 0; width: 100%;';
91
112
 
92
113
  const isActive = index === this.activeTab;
93
114
  const isPrevious = this.isTransitioning && index === this.previousTab;
@@ -105,29 +126,23 @@ if (labels.length === 0) {
105
126
  this.transitionDirection === 1
106
127
  ? 'rd-tabs-slide-in-from-right'
107
128
  : 'rd-tabs-slide-in-from-left';
108
- return `${base} opacity: 1; pointer-events: auto; visibility: visible; z-index: 2; animation: ${animationName} ${this.transitionDurationMs}ms ease-in-out both;`;
129
+ return `${base} opacity: 1; pointer-events: auto; visibility: visible; z-index: 2; animation: ${animationName} ${this.transitionDurationMs}ms ${this.transitionEasing} both;`;
109
130
  }
110
131
 
111
132
  const animationName =
112
133
  this.transitionDirection === 1
113
134
  ? 'rd-tabs-slide-out-to-left'
114
135
  : 'rd-tabs-slide-out-to-right';
115
- return `${base} opacity: 1; pointer-events: none; visibility: visible; z-index: 1; animation: ${animationName} ${this.transitionDurationMs}ms ease-in-out both;`;
136
+ return `${base} opacity: 1; pointer-events: none; visibility: visible; z-index: 1; animation: ${animationName} ${this.transitionDurationMs}ms ${this.transitionEasing} both;`;
116
137
  },
117
- updateHeight(includePrevious = false) {
138
+ updateHeight() {
118
139
  this.$nextTick(() => {
119
140
  const activeSlide = this.$refs['content-' + this.activeTab];
120
141
  if (!activeSlide) return;
142
+ const activeContent = activeSlide.querySelector('[data-rd-tabs-panel-content]');
143
+ const measuredElement = activeContent || activeSlide;
121
144
 
122
- let nextHeight = activeSlide.scrollHeight;
123
- if (includePrevious && this.previousTab !== null) {
124
- const previousSlide = this.$refs['content-' + this.previousTab];
125
- if (previousSlide) {
126
- nextHeight = Math.max(nextHeight, previousSlide.scrollHeight);
127
- }
128
- }
129
-
130
- this.containerHeight = nextHeight + 'px';
145
+ this.containerHeight = measuredElement.scrollHeight + 'px';
131
146
  });
132
147
  }
133
148
  }"
@@ -167,19 +182,28 @@ class="rd-prose-block">
167
182
  </ul>
168
183
 
169
184
  <div
170
- class="relative mt-4 overflow-hidden transition-[height] duration-300 ease-in-out"
171
- :style="'height: ' + containerHeight"
185
+ class="relative mt-4 overflow-visible transition-[height] motion-reduce:transition-none"
186
+ :style="'height: ' + containerHeight + '; transition-duration: ' + transitionDurationMs + 'ms; transition-timing-function: ' + transitionEasing + ';'"
172
187
  >
173
- { tabContents.map((content, index) => (
174
- <div
175
- {...(index !== 0 ? { "x-cloak": true } : {})}
176
- x-ref={`content-${index}`}
177
- class="prose-rules w-full max-w-none! *:max-w-none!"
178
- :style={`getPanelStyle(${index})`}
179
- style={index === 0 ? 'position: relative;' : ''}
180
- set:html={content}
181
- />
182
- )) }
188
+ <div class="relative -mx-4 h-full overflow-x-clip overflow-y-visible">
189
+ <div class="relative mx-4 h-full">
190
+ { tabContents.map((content, index) => (
191
+ <div
192
+ {...(index !== 0 ? { "x-cloak": true } : {})}
193
+ x-ref={`content-${index}`}
194
+ class="w-full"
195
+ :style={`getPanelStyle(${index})`}
196
+ style={index === 0 ? 'position: relative;' : ''}
197
+ >
198
+ <div
199
+ data-rd-tabs-panel-content
200
+ class="prose-rules w-full max-w-none! *:max-w-none!"
201
+ set:html={content}
202
+ />
203
+ </div>
204
+ )) }
205
+ </div>
206
+ </div>
183
207
  </div>
184
208
  </div>
185
209
 
@@ -187,36 +211,44 @@ class="rd-prose-block">
187
211
  @keyframes rd-tabs-slide-in-from-right {
188
212
  from {
189
213
  transform: translateX(100%);
214
+ opacity: 0;
190
215
  }
191
216
  to {
192
217
  transform: translateX(0);
218
+ opacity: 1;
193
219
  }
194
220
  }
195
221
 
196
222
  @keyframes rd-tabs-slide-in-from-left {
197
223
  from {
198
224
  transform: translateX(-100%);
225
+ opacity: 0;
199
226
  }
200
227
  to {
201
228
  transform: translateX(0);
229
+ opacity: 1;
202
230
  }
203
231
  }
204
232
 
205
233
  @keyframes rd-tabs-slide-out-to-left {
206
234
  from {
207
235
  transform: translateX(0);
236
+ opacity: 1;
208
237
  }
209
238
  to {
210
239
  transform: translateX(-100%);
240
+ opacity: 0;
211
241
  }
212
242
  }
213
243
 
214
244
  @keyframes rd-tabs-slide-out-to-right {
215
245
  from {
216
246
  transform: translateX(0);
247
+ opacity: 1;
217
248
  }
218
249
  to {
219
250
  transform: translateX(100%);
251
+ opacity: 0;
220
252
  }
221
253
  }
222
254
  </style>
@@ -55,12 +55,14 @@
55
55
  --border-light: var(--color-neutral-100);
56
56
  --input: oklch(0.922 0 0);
57
57
  --ring: oklch(0.708 0 0);
58
+ --rd-panel-transition-duration-ms: 400;
59
+ --rd-panel-transition-duration: 400ms;
60
+ --rd-panel-transition-easing: cubic-bezier(0.22, 1, 0.36, 1);
58
61
  --rd-code-surface: color-mix(
59
62
  in srgb,
60
63
  var(--color-neutral-100) 60%,
61
64
  var(--background) 40%
62
65
  );
63
- --rd-code-header-surface: var(--color-white);
64
66
  --rd-code-tab-edge-bg: var(--rd-code-surface);
65
67
  --rd-code-tab-edge-border: color-mix(
66
68
  in oklab,
@@ -94,11 +96,6 @@
94
96
  var(--color-neutral-800) 55%,
95
97
  var(--color-neutral-900) 45%
96
98
  );
97
- --rd-code-header-surface: color-mix(
98
- in srgb,
99
- var(--color-neutral-900) 100%,
100
- var(--rd-code-surface) 0%
101
- );
102
99
  --rd-code-tab-edge-bg: var(--rd-code-surface);
103
100
  --rd-code-tab-edge-border: color-mix(
104
101
  in oklab,