radiant-docs 0.1.47 → 0.1.48

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.
@@ -4,11 +4,11 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
4
4
  ---
5
5
 
6
6
  <div
7
- class="rd-prose-block group/prose-code-group not-prose relative w-full max-w-full min-w-0 shadow-xs rounded-xl"
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
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 dark:border-neutral-800 dark:bg-neutral-900/60 dark:inset-shadow-neutral-900/80"
11
+ class="relative z-10 overflow-visible rounded-t-xl bg-(--rd-code-header-surface)"
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,18 +19,16 @@ 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 opacity-0 transition-[left,width,opacity] duration-200 ease-out dark:border-neutral-700/70 dark:bg-(--rd-code-surface)"
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"
23
23
  >
24
24
  </div>
25
25
  </div>
26
26
  </div>
27
27
 
28
28
  <div
29
- class="relative h-9 w-5 shrink-0 rounded-tr-xl bg-white dark:bg-(--rd-code-surface)"
29
+ class="relative h-9 w-5 shrink-0 rounded-tr-xl bg-(--rd-code-surface) border-t-[0.5px] border-r-[0.5px] border-(--rd-code-tab-edge-border)"
30
30
  >
31
- <div
32
- class="absolute inset-x-0 -bottom-px h-px bg-white dark:bg-(--rd-code-surface)"
33
- >
31
+ <div class="absolute inset-x-0 -bottom-px h-px bg-(--rd-code-surface)">
34
32
  </div>
35
33
  <CodeTabEdge
36
34
  className="pointer-events-none absolute -top-px right-full z-10 h-[calc(100%+2px)] rotate-y-180"
@@ -49,7 +47,7 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
49
47
  />
50
48
  <Icon
51
49
  name="lucide:check"
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"
50
+ class="absolute size-3.5 stroke-3 origin-center scale-25 rotate-6 opacity-0 text-green-800/70 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"
53
51
  data-rd-copy-check
54
52
  aria-hidden="true"
55
53
  />
@@ -60,7 +58,7 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
60
58
 
61
59
  <div
62
60
  data-rd-code-group-content
63
- class="relative min-w-0 overflow-hidden transition-[height] duration-300 ease-in-out"
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)"
64
62
  >
65
63
  <slot />
66
64
  </div>
@@ -93,7 +91,8 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
93
91
  const prefersReducedMotion =
94
92
  typeof window.matchMedia === "function" &&
95
93
  window.matchMedia("(prefers-reduced-motion: reduce)").matches;
96
- const TRANSITION_DURATION_MS = prefersReducedMotion ? 0 : 300;
94
+ const TRANSITION_DURATION_MS = prefersReducedMotion ? 0 : 360;
95
+ const TRANSITION_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
97
96
 
98
97
  let activeIndex = 0;
99
98
  let transitionTimeoutId = null;
@@ -139,7 +138,7 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
139
138
 
140
139
  const tabButton = document.createElement("button");
141
140
  tabButton.type = "button";
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" : ""}`;
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" : ""}`;
143
142
  tabButton.setAttribute("aria-label", filename);
144
143
  tabButton.setAttribute("data-rd-code-group-tab", String(index));
145
144
 
@@ -205,12 +204,11 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
205
204
  const updateTabButtonStates = () => {
206
205
  tabs.forEach(({ tabButton, iconContainer }, index) => {
207
206
  const isActive = index === activeIndex;
208
- tabButton.classList.toggle("text-foreground", isActive);
209
- tabButton.classList.toggle("text-muted-foreground", !isActive);
210
-
207
+ tabButton.classList.toggle("text-neutral-500/80", !isActive);
208
+ tabButton.classList.toggle("dark:text-neutral-400/80", !isActive);
211
209
  if (!iconContainer) return;
212
- iconContainer.classList.toggle("opacity-100", isActive);
213
210
  iconContainer.classList.toggle("opacity-80", !isActive);
211
+ iconContainer.classList.toggle("grayscale-100", !isActive);
214
212
  });
215
213
  syncPill();
216
214
  };
@@ -269,15 +267,31 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
269
267
  activeItemResizeObserver.observe(activeItem);
270
268
  };
271
269
 
270
+ const setItemToTransitionPosition = (itemElement) => {
271
+ itemElement.style.position = "absolute";
272
+ itemElement.style.inset = "";
273
+ itemElement.style.top = "0";
274
+ itemElement.style.right = "0";
275
+ itemElement.style.bottom = "";
276
+ itemElement.style.left = "0";
277
+ };
278
+
272
279
  const setPanelsToRestState = () => {
273
280
  tabs.forEach(({ itemElement, panelElement, frameElement }, index) => {
274
281
  const isActive = index === activeIndex;
275
282
  itemElement.style.position = isActive ? "relative" : "absolute";
276
- itemElement.style.inset = isActive ? "" : "0";
283
+ itemElement.style.inset = "";
284
+ itemElement.style.top = isActive ? "" : "0";
285
+ itemElement.style.right = isActive ? "" : "0";
286
+ itemElement.style.bottom = isActive ? "" : "0";
287
+ itemElement.style.left = isActive ? "" : "0";
277
288
  itemElement.style.opacity = isActive ? "1" : "0";
278
289
  itemElement.style.visibility = isActive ? "visible" : "hidden";
279
290
  itemElement.style.pointerEvents = isActive ? "auto" : "none";
280
291
  itemElement.style.zIndex = isActive ? "1" : "0";
292
+ itemElement.style.transform = "translateX(0)";
293
+ itemElement.style.animation = "none";
294
+ itemElement.style.willChange = "auto";
281
295
  panelElement.style.transform = "translateX(0)";
282
296
  panelElement.style.animation = "none";
283
297
  panelElement.style.willChange = "auto";
@@ -313,8 +327,6 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
313
327
  const nextItem = nextTab?.itemElement;
314
328
  const previousPanel = previousTab?.panelElement;
315
329
  const nextPanel = nextTab?.panelElement;
316
- const previousFrame = previousTab?.frameElement;
317
- const nextFrame = nextTab?.frameElement;
318
330
  if (!previousItem || !nextItem || !previousPanel || !nextPanel) {
319
331
  normalizeToCurrentState();
320
332
  return;
@@ -331,37 +343,35 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
331
343
  });
332
344
  }
333
345
 
334
- previousItem.style.position = "absolute";
335
- previousItem.style.inset = "0";
346
+ setItemToTransitionPosition(previousItem);
336
347
  previousItem.style.opacity = "1";
337
348
  previousItem.style.visibility = "visible";
338
349
  previousItem.style.pointerEvents = "none";
339
350
  previousItem.style.zIndex = "1";
351
+ previousItem.style.transform = "translateX(0)";
352
+ previousItem.style.willChange = "transform, opacity";
340
353
  previousPanel.style.transform = "translateX(0)";
341
- previousPanel.style.willChange = "transform";
342
- if (previousFrame) {
343
- previousFrame.style.height = "100%";
344
- }
345
- previousPanel.style.animation =
354
+ previousPanel.style.animation = "none";
355
+ previousPanel.style.willChange = "auto";
356
+ previousItem.style.animation =
346
357
  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`;
358
+ ? `rd-code-group-slide-out-to-left ${TRANSITION_DURATION_MS}ms ${TRANSITION_EASING} both`
359
+ : `rd-code-group-slide-out-to-right ${TRANSITION_DURATION_MS}ms ${TRANSITION_EASING} both`;
349
360
 
350
- nextItem.style.position = "absolute";
351
- nextItem.style.inset = "0";
361
+ setItemToTransitionPosition(nextItem);
352
362
  nextItem.style.opacity = "1";
353
363
  nextItem.style.visibility = "visible";
354
364
  nextItem.style.pointerEvents = "auto";
355
365
  nextItem.style.zIndex = "2";
366
+ nextItem.style.transform = "translateX(0)";
367
+ nextItem.style.willChange = "transform, opacity";
356
368
  nextPanel.style.transform = "translateX(0)";
357
- nextPanel.style.willChange = "transform";
358
- if (nextFrame) {
359
- nextFrame.style.height = "100%";
360
- }
361
- nextPanel.style.animation =
369
+ nextPanel.style.animation = "none";
370
+ nextPanel.style.willChange = "auto";
371
+ nextItem.style.animation =
362
372
  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`;
373
+ ? `rd-code-group-slide-in-from-right ${TRANSITION_DURATION_MS}ms ${TRANSITION_EASING} both`
374
+ : `rd-code-group-slide-in-from-left ${TRANSITION_DURATION_MS}ms ${TRANSITION_EASING} both`;
365
375
 
366
376
  transitionTimeoutId = window.setTimeout(() => {
367
377
  transitionTimeoutId = null;
@@ -7,6 +7,12 @@ import {
7
7
  type ThemedToken,
8
8
  } from "shiki";
9
9
  import { DEFAULT_FILE, getIconForFile } from "vscode-icons-js";
10
+ import { getConfig } from "../validation";
11
+ import {
12
+ DEFAULT_SHIKI_DARK_THEME,
13
+ DEFAULT_SHIKI_LIGHT_THEME,
14
+ type CodeSyntaxThemeByMode,
15
+ } from "./shiki-theme-config";
10
16
 
11
17
  export const DEFAULT_CODE_BLOCK_LANGUAGE = "plaintext";
12
18
 
@@ -137,9 +143,8 @@ const CODE_BLOCK_LANGUAGE_ICON_FILE_BY_VALUE: Record<string, string> = {
137
143
  yaml: "file_type_yaml_official.svg",
138
144
  };
139
145
 
140
- const SHIKI_LIGHT_THEME = "github-light";
141
- const SHIKI_DARK_THEME = "github-dark";
142
- const SHIKI_THEMES = [SHIKI_LIGHT_THEME, SHIKI_DARK_THEME] as const;
146
+ const SHIKI_LIGHT_LINE_HIGHLIGHT_FALLBACK = "#f6f8fa";
147
+ const SHIKI_DARK_LINE_HIGHLIGHT_FALLBACK = "#2b3036";
143
148
  const BUNDLED_LANGUAGE_SET = new Set(Object.keys(bundledLanguages));
144
149
  const LANGUAGE_RUNTIME_DEPENDENCIES: Record<string, string[]> = {
145
150
  // MDX tokenization relies on TSX grammar injections for JSX-style tags.
@@ -159,6 +164,7 @@ let iconFileNameSetPromise: Promise<Set<string>> | null = null;
159
164
  let highlighterPromise:
160
165
  | Promise<Awaited<ReturnType<typeof getSingletonHighlighter>>>
161
166
  | null = null;
167
+ let highlighterThemeKey = "";
162
168
  const loadedLanguageSet = new Set<string>([DEFAULT_CODE_BLOCK_LANGUAGE]);
163
169
  const languageLoadPromiseByName = new Map<string, Promise<void>>();
164
170
 
@@ -371,10 +377,30 @@ function namespaceSvgIds(svg: string, namespace: string): string {
371
377
  return namespacedSvg;
372
378
  }
373
379
 
374
- async function getHighlighter() {
375
- if (!highlighterPromise) {
380
+ async function getConfiguredCodeSyntaxThemes(): Promise<CodeSyntaxThemeByMode> {
381
+ const config = await getConfig();
382
+ const configuredSyntaxTheme = config.theme?.code?.syntaxTheme;
383
+
384
+ if (typeof configuredSyntaxTheme === "string") {
385
+ return {
386
+ light: configuredSyntaxTheme,
387
+ dark: configuredSyntaxTheme,
388
+ };
389
+ }
390
+
391
+ return {
392
+ light: configuredSyntaxTheme?.light ?? DEFAULT_SHIKI_LIGHT_THEME,
393
+ dark: configuredSyntaxTheme?.dark ?? DEFAULT_SHIKI_DARK_THEME,
394
+ };
395
+ }
396
+
397
+ async function getHighlighter(syntaxThemes: CodeSyntaxThemeByMode) {
398
+ const nextThemeKey = `${syntaxThemes.light}\u0000${syntaxThemes.dark}`;
399
+
400
+ if (!highlighterPromise || highlighterThemeKey !== nextThemeKey) {
401
+ highlighterThemeKey = nextThemeKey;
376
402
  highlighterPromise = getSingletonHighlighter({
377
- themes: [...SHIKI_THEMES],
403
+ themes: Array.from(new Set([syntaxThemes.light, syntaxThemes.dark])),
378
404
  langs: [DEFAULT_CODE_BLOCK_LANGUAGE],
379
405
  });
380
406
  }
@@ -382,6 +408,40 @@ async function getHighlighter() {
382
408
  return highlighterPromise;
383
409
  }
384
410
 
411
+ function getThemeColor(
412
+ highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
413
+ themeName: string,
414
+ colorName: string,
415
+ fallback: string,
416
+ ): string {
417
+ const colorValue = highlighter.getTheme(themeName)?.colors?.[colorName];
418
+ return typeof colorValue === "string" && colorValue.trim().length > 0
419
+ ? colorValue.trim()
420
+ : fallback;
421
+ }
422
+
423
+ function getCodeThemeColors(
424
+ highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
425
+ syntaxThemes: CodeSyntaxThemeByMode,
426
+ ): CodeThemeColors {
427
+ return {
428
+ lineHighlightBackground: {
429
+ light: getThemeColor(
430
+ highlighter,
431
+ syntaxThemes.light,
432
+ "editor.lineHighlightBackground",
433
+ SHIKI_LIGHT_LINE_HIGHLIGHT_FALLBACK,
434
+ ),
435
+ dark: getThemeColor(
436
+ highlighter,
437
+ syntaxThemes.dark,
438
+ "editor.lineHighlightBackground",
439
+ SHIKI_DARK_LINE_HIGHLIGHT_FALLBACK,
440
+ ),
441
+ },
442
+ };
443
+ }
444
+
385
445
  function resolveLoadableLanguage(
386
446
  highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
387
447
  normalizedLanguage: string,
@@ -496,8 +556,11 @@ export async function getCodeLineTokens({
496
556
  }): Promise<{
497
557
  normalizedLanguage: string;
498
558
  lines: CodeLineToken[][];
559
+ themeColors: CodeThemeColors;
499
560
  }> {
500
- const highlighter = await getHighlighter();
561
+ const syntaxThemes = await getConfiguredCodeSyntaxThemes();
562
+ const highlighter = await getHighlighter(syntaxThemes);
563
+ const themeColors = getCodeThemeColors(highlighter, syntaxThemes);
501
564
  const normalizedLanguage = normalizeCodeLanguageValue(language);
502
565
  const loadableLanguage = resolveLoadableLanguage(
503
566
  highlighter,
@@ -525,26 +588,41 @@ export async function getCodeLineTokens({
525
588
  }
526
589
 
527
590
  try {
528
- const themedTokenLines = getThemedTokenLines(highlighter, code, targetLanguage);
591
+ const themedTokenLines = getThemedTokenLines(
592
+ highlighter,
593
+ code,
594
+ targetLanguage,
595
+ syntaxThemes,
596
+ );
529
597
 
530
598
  return {
531
599
  normalizedLanguage: targetLanguage,
532
600
  lines: mergeTokenLines(themedTokenLines.light, themedTokenLines.dark),
601
+ themeColors,
533
602
  };
534
603
  } catch {
535
604
  const themedTokenLines = getThemedTokenLines(
536
605
  highlighter,
537
606
  code,
538
607
  DEFAULT_CODE_BLOCK_LANGUAGE,
608
+ syntaxThemes,
539
609
  );
540
610
 
541
611
  return {
542
612
  normalizedLanguage: DEFAULT_CODE_BLOCK_LANGUAGE,
543
613
  lines: mergeTokenLines(themedTokenLines.light, themedTokenLines.dark),
614
+ themeColors,
544
615
  };
545
616
  }
546
617
  }
547
618
 
619
+ export type CodeThemeColors = {
620
+ lineHighlightBackground: {
621
+ light: string;
622
+ dark: string;
623
+ };
624
+ };
625
+
548
626
  export type CodeLineToken = {
549
627
  content: string;
550
628
  color?: string;
@@ -560,6 +638,7 @@ function getThemedTokenLines(
560
638
  highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
561
639
  code: string,
562
640
  lang: string,
641
+ syntaxThemes: CodeSyntaxThemeByMode,
563
642
  ): {
564
643
  light: ThemedToken[][];
565
644
  dark: ThemedToken[][];
@@ -567,11 +646,11 @@ function getThemedTokenLines(
567
646
  const shikiLanguage = lang as keyof typeof bundledLanguages;
568
647
  const lightTokenResult = highlighter.codeToTokens(code, {
569
648
  lang: shikiLanguage,
570
- theme: SHIKI_LIGHT_THEME,
649
+ theme: syntaxThemes.light,
571
650
  });
572
651
  const darkTokenResult = highlighter.codeToTokens(code, {
573
652
  lang: shikiLanguage,
574
- theme: SHIKI_DARK_THEME,
653
+ theme: syntaxThemes.dark,
575
654
  });
576
655
 
577
656
  return {
@@ -0,0 +1,16 @@
1
+ import { bundledThemes } from "shiki";
2
+
3
+ export const DEFAULT_SHIKI_LIGHT_THEME = "github-light";
4
+ export const DEFAULT_SHIKI_DARK_THEME = "github-dark";
5
+
6
+ export type CodeSyntaxThemeByMode = {
7
+ light: string;
8
+ dark: string;
9
+ };
10
+
11
+ export const SHIKI_BUNDLED_THEME_NAMES = Object.keys(bundledThemes).sort();
12
+ const SHIKI_BUNDLED_THEME_NAME_SET = new Set(SHIKI_BUNDLED_THEME_NAMES);
13
+
14
+ export function isBundledShikiThemeName(value: string): boolean {
15
+ return SHIKI_BUNDLED_THEME_NAME_SET.has(value);
16
+ }
@@ -6,6 +6,12 @@ import { oas } from "@stoplight/spectral-rulesets";
6
6
  import { compile } from "@mdx-js/mdx";
7
7
  import yaml from "yaml";
8
8
  import { docsSchema } from "./frontmatter-schema";
9
+ import {
10
+ DEFAULT_SHIKI_DARK_THEME,
11
+ DEFAULT_SHIKI_LIGHT_THEME,
12
+ SHIKI_BUNDLED_THEME_NAMES,
13
+ isBundledShikiThemeName,
14
+ } from "./code/shiki-theme-config";
9
15
 
10
16
  // --- Configuration Constants ---
11
17
  const CWD = process.cwd();
@@ -226,10 +232,20 @@ export type CardTheme = {
226
232
  cover?: CardCoverTheme;
227
233
  button?: CardButtonTheme;
228
234
  };
235
+ export type CodeSyntaxThemeConfig =
236
+ | string
237
+ | {
238
+ light?: string;
239
+ dark?: string;
240
+ };
241
+ export type CodeTheme = {
242
+ syntaxTheme?: CodeSyntaxThemeConfig;
243
+ };
229
244
  export type DocsTheme = {
230
245
  baseColor?: BaseColorOption | BaseColorByMode;
231
246
  themeColor?: string | ThemeColorByMode;
232
247
  card?: CardTheme;
248
+ code?: CodeTheme;
233
249
  };
234
250
  export type AssistantIcon = {
235
251
  src?: string;
@@ -1718,6 +1734,127 @@ function validateTheme(theme: DocsConfig["theme"]): void {
1718
1734
  }
1719
1735
  }
1720
1736
 
1737
+ const normalizeShikiThemeName = (
1738
+ value: unknown,
1739
+ currentPath: Path,
1740
+ label: string,
1741
+ ): string => {
1742
+ checkType(value, "string", currentPath, label);
1743
+ if (typeof value !== "string") {
1744
+ throwConfigError(`${label} must be a string.`, currentPath);
1745
+ }
1746
+
1747
+ const normalizedThemeName = value.trim().toLowerCase();
1748
+ if (normalizedThemeName.length === 0) {
1749
+ throwConfigError(`${label} cannot be empty.`, currentPath);
1750
+ }
1751
+
1752
+ if (!isBundledShikiThemeName(normalizedThemeName)) {
1753
+ throwConfigError(
1754
+ `${label} must be a bundled Shiki theme name. Supported themes include: ${SHIKI_BUNDLED_THEME_NAMES.join(", ")}.`,
1755
+ currentPath,
1756
+ );
1757
+ }
1758
+
1759
+ return normalizedThemeName;
1760
+ };
1761
+
1762
+ if (theme.code !== undefined) {
1763
+ checkType(theme.code, "object", ["theme", "code"], "Theme code");
1764
+ if (
1765
+ typeof theme.code !== "object" ||
1766
+ theme.code === null ||
1767
+ Array.isArray(theme.code)
1768
+ ) {
1769
+ throwConfigError("Theme code must be an object.", ["theme", "code"]);
1770
+ }
1771
+
1772
+ const codeTheme = theme.code as CodeTheme & Record<string, unknown>;
1773
+ const allowedCodeKeys = new Set(["syntaxTheme"]);
1774
+ for (const key of Object.keys(codeTheme)) {
1775
+ if (!allowedCodeKeys.has(key)) {
1776
+ throwConfigError(
1777
+ "Theme code configuration only supports 'syntaxTheme'.",
1778
+ ["theme", "code", key],
1779
+ );
1780
+ }
1781
+ }
1782
+
1783
+ if (codeTheme.syntaxTheme !== undefined) {
1784
+ if (typeof codeTheme.syntaxTheme === "string") {
1785
+ const themeName = normalizeShikiThemeName(
1786
+ codeTheme.syntaxTheme,
1787
+ ["theme", "code", "syntaxTheme"],
1788
+ "Theme code syntax theme",
1789
+ );
1790
+ codeTheme.syntaxTheme = {
1791
+ light: themeName,
1792
+ dark: themeName,
1793
+ };
1794
+ } else {
1795
+ checkType(
1796
+ codeTheme.syntaxTheme,
1797
+ "object",
1798
+ ["theme", "code", "syntaxTheme"],
1799
+ "Theme code syntax theme",
1800
+ );
1801
+ if (
1802
+ typeof codeTheme.syntaxTheme !== "object" ||
1803
+ codeTheme.syntaxTheme === null ||
1804
+ Array.isArray(codeTheme.syntaxTheme)
1805
+ ) {
1806
+ throwConfigError(
1807
+ "Theme code syntax theme must be a string or an object with light/dark values.",
1808
+ ["theme", "code", "syntaxTheme"],
1809
+ );
1810
+ }
1811
+
1812
+ const syntaxThemeByMode = codeTheme.syntaxTheme as Record<
1813
+ string,
1814
+ unknown
1815
+ >;
1816
+ const allowedSyntaxThemeKeys = new Set(["light", "dark"]);
1817
+ for (const key of Object.keys(syntaxThemeByMode)) {
1818
+ if (!allowedSyntaxThemeKeys.has(key)) {
1819
+ throwConfigError(
1820
+ "Theme code syntax theme object only supports 'light' and 'dark'.",
1821
+ ["theme", "code", "syntaxTheme", key],
1822
+ );
1823
+ }
1824
+ }
1825
+
1826
+ const light =
1827
+ syntaxThemeByMode.light !== undefined
1828
+ ? normalizeShikiThemeName(
1829
+ syntaxThemeByMode.light,
1830
+ ["theme", "code", "syntaxTheme", "light"],
1831
+ "Theme code syntax theme light",
1832
+ )
1833
+ : undefined;
1834
+ const dark =
1835
+ syntaxThemeByMode.dark !== undefined
1836
+ ? normalizeShikiThemeName(
1837
+ syntaxThemeByMode.dark,
1838
+ ["theme", "code", "syntaxTheme", "dark"],
1839
+ "Theme code syntax theme dark",
1840
+ )
1841
+ : undefined;
1842
+
1843
+ if (!light && !dark) {
1844
+ throwConfigError(
1845
+ "Theme code syntax theme object must include 'light', 'dark', or both.",
1846
+ ["theme", "code", "syntaxTheme"],
1847
+ );
1848
+ }
1849
+
1850
+ codeTheme.syntaxTheme = {
1851
+ light: light ?? DEFAULT_SHIKI_LIGHT_THEME,
1852
+ dark: dark ?? DEFAULT_SHIKI_DARK_THEME,
1853
+ };
1854
+ }
1855
+ }
1856
+ }
1857
+
1721
1858
  if (theme.card !== undefined) {
1722
1859
  checkType(theme.card, "object", ["theme", "card"], "Theme card");
1723
1860
  if (
@@ -55,9 +55,18 @@
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-code-surface: #ffffff;
58
+ --rd-code-surface: color-mix(
59
+ in srgb,
60
+ var(--color-neutral-100) 60%,
61
+ var(--background) 40%
62
+ );
63
+ --rd-code-header-surface: var(--color-white);
59
64
  --rd-code-tab-edge-bg: var(--rd-code-surface);
60
- --rd-code-tab-edge-border: var(--color-neutral-200);
65
+ --rd-code-tab-edge-border: color-mix(
66
+ in oklab,
67
+ var(--color-neutral-900) 4%,
68
+ var(--color-white) 96%
69
+ );
61
70
  }
62
71
 
63
72
  /* 3. Dark Mode */
@@ -85,8 +94,17 @@
85
94
  var(--color-neutral-800) 55%,
86
95
  var(--color-neutral-900) 45%
87
96
  );
97
+ --rd-code-header-surface: color-mix(
98
+ in srgb,
99
+ var(--color-neutral-900) 100%,
100
+ var(--rd-code-surface) 0%
101
+ );
88
102
  --rd-code-tab-edge-bg: var(--rd-code-surface);
89
- --rd-code-tab-edge-border: var(--color-neutral-800);
103
+ --rd-code-tab-edge-border: color-mix(
104
+ in oklab,
105
+ white 4%,
106
+ var(--rd-code-tab-edge-bg) 96%
107
+ );
90
108
  }
91
109
 
92
110
  @variant dark (&:where(.dark, .dark *));
@@ -197,6 +215,16 @@
197
215
  }
198
216
 
199
217
  @layer base {
218
+ .prose-rules > :first-child {
219
+ margin-block-start: 0 !important;
220
+ margin-top: 0 !important;
221
+ }
222
+
223
+ .prose-rules > :last-child {
224
+ margin-block-end: 0 !important;
225
+ margin-bottom: 0 !important;
226
+ }
227
+
200
228
  .prose-rules > .rd-prose-block:first-child,
201
229
  .prose-rules > .react-renderer:first-child > .rd-prose-block,
202
230
  .prose-rules > [data-node-view-content-react]:first-child > :first-child,
@@ -326,6 +354,26 @@
326
354
  background-color: var(--rd-token-bg, transparent);
327
355
  }
328
356
 
357
+ [data-rd-code-block-root] {
358
+ --rd-code-line-highlight-bg: color-mix(
359
+ in srgb,
360
+ var(--rd-code-line-highlight-theme-bg-light) 90%,
361
+ var(--color-neutral-400) 10%
362
+ );
363
+ }
364
+
365
+ .dark [data-rd-code-block-root] {
366
+ --rd-code-line-highlight-bg: color-mix(
367
+ in srgb,
368
+ var(--rd-code-line-highlight-theme-bg-dark) 80%,
369
+ var(--color-neutral-700) 20%
370
+ );
371
+ }
372
+
373
+ [data-rd-code-line-highlighted="true"] {
374
+ background-color: var(--rd-code-line-highlight-bg);
375
+ }
376
+
329
377
  .dark [data-rd-code-theme] [data-rd-token] {
330
378
  color: var(--rd-token-color-dark, var(--rd-token-color, currentColor));
331
379
  background-color: var(--rd-token-bg-dark, var(--rd-token-bg, transparent));