radiant-docs 0.1.60 → 0.1.62

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 (41) hide show
  1. package/package.json +1 -1
  2. package/template/package-lock.json +10 -4
  3. package/template/package.json +11 -2
  4. package/template/scripts/generate-proxy-allowed-origins.mjs +14 -6
  5. package/template/scripts/publish-shiki-platform-assets.mjs +1151 -0
  6. package/template/src/components/Header.astro +6 -1
  7. package/template/src/components/NavigationTabList.astro +65 -0
  8. package/template/src/components/NavigationTabs.astro +109 -0
  9. package/template/src/components/OpenApiPage.astro +178 -14
  10. package/template/src/components/Sidebar.astro +2 -2
  11. package/template/src/components/SidebarDropdown.astro +105 -44
  12. package/template/src/components/SidebarMenu.astro +3 -0
  13. package/template/src/components/SidebarSegmented.astro +87 -52
  14. package/template/src/components/SidebarTabs.astro +86 -0
  15. package/template/src/components/chat/AssistantDocsWidget.tsx +127 -2
  16. package/template/src/components/chat/AssistantEmbedPanel.tsx +287 -290
  17. package/template/src/components/endpoint/PlaygroundBar.astro +54 -9
  18. package/template/src/components/endpoint/PlaygroundForm.astro +1 -1
  19. package/template/src/components/endpoint/RequestSnippets.astro +6 -1
  20. package/template/src/components/endpoint/ResponseFieldTree.astro +17 -13
  21. package/template/src/components/endpoint/ResponseFields.astro +4 -6
  22. package/template/src/components/endpoint/ResponseSnippets.astro +6 -1
  23. package/template/src/components/sidebar/SidebarEndpointLink.astro +9 -12
  24. package/template/src/components/sidebar/SidebarOpenApi.astro +3 -9
  25. package/template/src/components/ui/Field.astro +18 -15
  26. package/template/src/components/ui/Tag.astro +16 -2
  27. package/template/src/components/user/Accordion.astro +1 -1
  28. package/template/src/components/user/Callout.astro +2 -2
  29. package/template/src/components/user/CodeBlock.astro +58 -7
  30. package/template/src/components/user/CodeGroup.astro +52 -1
  31. package/template/src/components/user/Column.astro +1 -1
  32. package/template/src/components/user/Step.astro +1 -1
  33. package/template/src/components/user/Tabs.astro +1 -1
  34. package/template/src/generated/shiki-platform-assets.json +24 -0
  35. package/template/src/layouts/Layout.astro +111 -8
  36. package/template/src/lib/assistant-panel-config.ts +59 -0
  37. package/template/src/lib/assistant-shiki-client.ts +506 -0
  38. package/template/src/lib/mdx/remark-resolve-internal-links.ts +334 -17
  39. package/template/src/lib/routes.ts +66 -24
  40. package/template/src/lib/utils.ts +11 -0
  41. package/template/src/styles/global.css +12 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import { Icon } from "astro-icon/components";
3
- import { methodColors } from "../../lib/utils";
3
+ import { methodTagColors } from "../../lib/utils";
4
4
  import type { OpenApiRoute } from "../../lib/routes";
5
5
 
6
6
  interface Props {
@@ -9,18 +9,26 @@ interface Props {
9
9
  }
10
10
 
11
11
  const { route, serverUrl } = Astro.props;
12
+ const methodColor =
13
+ methodTagColors[route.openApiMethod.toLowerCase()] ?? methodTagColors.get;
14
+
15
+ const pathParts = route.openApiPath
16
+ .split(/(\{[^}]+\})/g)
17
+ .filter(Boolean)
18
+ .map((text) => ({
19
+ text,
20
+ isParam: text.startsWith("{") && text.endsWith("}"),
21
+ }));
12
22
  ---
13
23
 
14
24
  <div
15
- class="min-w-0 flex-1 flex items-center p-1 border border-neutral-200 bg-white rounded-xl shadow-xs overflow-hidden dark:border-neutral-800 dark:bg-(--rd-code-surface)"
25
+ class="min-w-0 flex-1 flex items-center p-1 border-[0.5px] border-neutral-900/8 bg-white rounded-xl shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-6px_rgba(0,0,0,0.08)] overflow-hidden dark:border-white/6 dark:bg-(--rd-code-surface) dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-6px_rgba(0,0,0,0.2)]"
16
26
  >
17
27
  <span
18
- class:list={[
19
- "shrink-0 inline-block px-1.5 ml-1 text-sm font-semibold rounded-md uppercase border",
20
- methodColors[route.openApiMethod.toLowerCase()] || methodColors.get,
21
- ]}
28
+ class="rd-endpoint-method-tag shrink-0 inline-block px-1.5 ml-1 text-sm font-semibold rounded-md uppercase border"
29
+ style={`--rd-tag-color-light:${methodColor};--rd-tag-color-dark:${methodColor};`}
22
30
  >
23
- {route.openApiMethod}
31
+ {route.openApiMethod.toUpperCase()}
24
32
  </span>
25
33
  <code
26
34
  class="group flex-1 mx-2 h-[30px] flex items-center text-[13px] text-neutral-600 dark:text-neutral-300 min-w-0 relative cursor-pointer"
@@ -41,8 +49,23 @@ const { route, serverUrl } = Astro.props;
41
49
  }
42
50
  }`
43
51
  >
44
- <span class="truncate min-w-0 flex-1">
45
- {route.openApiPath}
52
+ <span
53
+ class="flex min-w-0 flex-1 items-center overflow-hidden whitespace-nowrap"
54
+ >
55
+ {
56
+ pathParts.map((part) =>
57
+ part.isParam ? (
58
+ <span
59
+ class="rd-endpoint-path-param inline-block rounded-sm border p-px leading-none"
60
+ style={`--rd-tag-color-light:${methodColor};--rd-tag-color-dark:${methodColor};`}
61
+ >
62
+ {part.text}
63
+ </span>
64
+ ) : (
65
+ <span>{part.text}</span>
66
+ ),
67
+ )
68
+ }
46
69
  </span>
47
70
  <div
48
71
  class="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1 text-[12px] px-1.5 py-px bg-white border border-neutral-200 rounded-md duration-200 opacity-0 scale-75 group-hover:scale-100 group-hover:opacity-100 group-hover:duration-200 group-hover:ease-out group-hover:delay-75 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300"
@@ -62,3 +85,25 @@ const { route, serverUrl } = Astro.props;
62
85
  </code>
63
86
  <slot />
64
87
  </div>
88
+
89
+ <style>
90
+ .rd-endpoint-method-tag,
91
+ .rd-endpoint-path-param {
92
+ --rd-tag-color: var(--rd-tag-color-light, var(--color-theme));
93
+ background-color: color-mix(in oklab, var(--rd-tag-color) 8%, transparent);
94
+ border-color: color-mix(in oklab, var(--rd-tag-color) 0%, transparent);
95
+ color: color-mix(in oklab, var(--rd-tag-color) 95%, transparent);
96
+ }
97
+
98
+ :global(.dark) .rd-endpoint-method-tag,
99
+ :global(.dark) .rd-endpoint-path-param,
100
+ :global([data-theme="dark"]) .rd-endpoint-method-tag,
101
+ :global([data-theme="dark"]) .rd-endpoint-path-param {
102
+ --rd-tag-color: var(
103
+ --rd-tag-color-dark,
104
+ var(--rd-tag-color-light, var(--color-theme))
105
+ );
106
+ background-color: color-mix(in oklab, var(--rd-tag-color) 12%, transparent);
107
+ color: color-mix(in oklab, var(--rd-tag-color) 85%, transparent);
108
+ }
109
+ </style>
@@ -587,7 +587,7 @@ const sectionVariantFieldNames = Object.fromEntries(
587
587
  }
588
588
 
589
589
  return (
590
- <div class="border border-neutral-200 bg-white shadow-xs rounded-xl p-4 pb-0 dark:border-neutral-800 dark:bg-(--rd-code-surface) [&_[role='region']]:border-b-0">
590
+ <div class="border-[0.5px] border-neutral-900/8 bg-white shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-6px_rgba(0,0,0,0.08)] rounded-xl p-4 pb-0 dark:border-white/6 dark:bg-(--rd-code-surface) dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-6px_rgba(0,0,0,0.2)] [&_[role='region']]:border-b-0">
591
591
  <Accordion title={headers[key]} defaultOpen titleSize="xl">
592
592
  {key === "body" && formattedBodyDescription && (
593
593
  <div
@@ -593,7 +593,12 @@ const hasMultipleRequests = requestSnippetItems.length > 1;
593
593
  </div>
594
594
  </div>
595
595
 
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)">
596
+ <div
597
+ class:list={[
598
+ "relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-b-xl border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface)",
599
+ hasMultipleRequests && "rounded-tl-xl",
600
+ ]}
601
+ >
597
602
  <div
598
603
  x-ref="snippetPanels"
599
604
  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"
@@ -46,7 +46,9 @@ function countImmediateChildren(field: ResponseField): number {
46
46
  }
47
47
  ---
48
48
 
49
- <div class="flex flex-col gap-4 divide-y divide-neutral-100 dark:divide-neutral-800 *:pb-3 *:last:pb-0">
49
+ <div
50
+ class="flex flex-col gap-4 divide-y divide-neutral-100 dark:divide-neutral-800 *:pb-3 *:last:pb-0"
51
+ >
50
52
  {
51
53
  fields.map((field) => {
52
54
  const revealedFieldCount = countImmediateChildren(field);
@@ -69,10 +71,7 @@ function countImmediateChildren(field: ResponseField): number {
69
71
  exclusiveMaximum={field.exclusiveMaximum}
70
72
  />
71
73
  {hasExpandableContent && (
72
- <div
73
- class="mt-2 w-full overflow-hidden rounded-lg border border-neutral-200 bg-white transition-colors duration-200 dark:border-neutral-800 dark:bg-(--rd-code-surface)"
74
- x-bind:class="expanded ? 'border-neutral-300 dark:border-neutral-700' : 'border-neutral-200 dark:border-neutral-800'"
75
- >
74
+ <div class="mt-2.5 w-full overflow-hidden rounded-lg border-[0.5px] border-neutral-900/8 bg-white shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-6px_rgba(0,0,0,0.08)] transition-colors duration-200 dark:border-white/6 dark:bg-(--rd-code-surface) dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-6px_rgba(0,0,0,0.2)]">
76
75
  <button
77
76
  type="button"
78
77
  x-on:click="expanded = !expanded"
@@ -83,19 +82,21 @@ function countImmediateChildren(field: ResponseField): number {
83
82
  <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 group-hover:text-neutral-600 transition duration-200 dark:text-neutral-500 dark:group-hover:text-neutral-300" />
84
83
  <span>
85
84
  {revealedFieldCount}{" "}
86
- {revealedFieldCount === 1
87
- ? "field"
88
- : "fields"}
85
+ {revealedFieldCount === 1 ? "field" : "fields"}
89
86
  </span>
90
87
  </span>
91
88
  </button>
92
89
  <div x-show="expanded" x-collapse x-cloak>
93
90
  <div class="border-t border-neutral-100 px-3 py-3 dark:border-neutral-800">
94
- {field.nested && field.nested.length > 0 && nestedFieldCount > 0 && (
95
- <Astro.self fields={field.nested} depth={depth + 1} />
96
- )}
91
+ {field.nested &&
92
+ field.nested.length > 0 &&
93
+ nestedFieldCount > 0 && (
94
+ <Astro.self fields={field.nested} depth={depth + 1} />
95
+ )}
97
96
  {field.variants && field.variants.length > 0 && (
98
- <div class:list={["space-y-2", nestedFieldCount > 0 && "mt-3"]}>
97
+ <div
98
+ class:list={["space-y-2", nestedFieldCount > 0 && "mt-3"]}
99
+ >
99
100
  <p class="text-xs text-neutral-500 dark:text-neutral-400">
100
101
  {field.variantType === "anyOf"
101
102
  ? "One or more variants may apply."
@@ -107,7 +108,10 @@ function countImmediateChildren(field: ResponseField): number {
107
108
  <div class="mb-2 text-xs font-medium text-neutral-600 dark:text-neutral-400">
108
109
  {variant.label}
109
110
  </div>
110
- <Astro.self fields={variant.fields} depth={depth + 1} />
111
+ <Astro.self
112
+ fields={variant.fields}
113
+ depth={depth + 1}
114
+ />
111
115
  </div>
112
116
  {field.variantType === "oneOf" &&
113
117
  index < field.variants.length - 1 && (
@@ -771,10 +771,9 @@ Object.entries(responses)
771
771
  {response.variants && response.variants.length > 0 && (
772
772
  <p class="mb-2 text-xs text-neutral-500 dark:text-neutral-400">Common fields</p>
773
773
  )}
774
- <div x-data="{ expanded: false }">
774
+ <div x-data="{ expanded: true }">
775
775
  <div
776
- class="w-full overflow-hidden rounded-xl border border-neutral-200 bg-white transition-colors duration-200 dark:border-neutral-800 dark:bg-(--rd-code-surface)"
777
- x-bind:class="expanded ? 'border-neutral-300 dark:border-neutral-700' : 'border-neutral-200 dark:border-neutral-800'"
776
+ class="w-full overflow-hidden rounded-xl border-[0.5px] border-neutral-900/8 bg-white shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-6px_rgba(0,0,0,0.08)] transition-colors duration-200 dark:border-white/6 dark:bg-(--rd-code-surface) dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-6px_rgba(0,0,0,0.2)]"
778
777
  >
779
778
  <button
780
779
  type="button"
@@ -814,10 +813,9 @@ Object.entries(responses)
814
813
  <div class="mb-2 text-xs font-medium text-neutral-600 dark:text-neutral-400">
815
814
  {variant.label}
816
815
  </div>
817
- <div x-data="{ expanded: false }">
816
+ <div x-data="{ expanded: true }">
818
817
  <div
819
- class="w-full overflow-hidden rounded-lg border border-neutral-200 bg-white transition-colors duration-200 dark:border-neutral-800 dark:bg-(--rd-code-surface)"
820
- x-bind:class="expanded ? 'border-neutral-300 dark:border-neutral-700' : 'border-neutral-200 dark:border-neutral-800'"
818
+ class="w-full overflow-hidden rounded-lg border-[0.5px] border-neutral-900/8 bg-white shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-6px_rgba(0,0,0,0.08)] transition-colors duration-200 dark:border-white/6 dark:bg-(--rd-code-surface) dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-6px_rgba(0,0,0,0.2)]"
821
819
  >
822
820
  <button
823
821
  type="button"
@@ -513,7 +513,12 @@ const hasMultipleResponses = responseSnippetItems.length > 1;
513
513
  </div>
514
514
  </div>
515
515
 
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)">
516
+ <div
517
+ class:list={[
518
+ "relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-b-xl border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface)",
519
+ hasMultipleResponses && "rounded-tl-xl",
520
+ ]}
521
+ >
517
522
  <div
518
523
  x-ref="snippetPanels"
519
524
  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"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import Tag from "../ui/Tag.astro";
3
- import { methodColors } from "../../lib/utils";
3
+ import { methodTagColors } from "../../lib/utils";
4
4
  import { getOpenApiEndpointRouteHref } from "../../lib/routes";
5
5
  import type { NavTag } from "../../lib/validation";
6
6
 
@@ -34,6 +34,10 @@ const href = await getOpenApiEndpointRouteHref({
34
34
 
35
35
  // Use summary for title, fallback to method + path
36
36
  const text = title || summary || `${normalizedMethod.toUpperCase()} ${pathStr}`;
37
+ const methodLabel = (
38
+ normalizedMethod !== "delete" ? normalizedMethod : "del"
39
+ ).toUpperCase();
40
+ const methodColor = methodTagColors[normalizedMethod] ?? methodTagColors.get;
37
41
 
38
42
  // Normalize paths for comparison (remove trailing slashes)
39
43
  const currentPath = Astro.url.pathname.replace(/\/$/, "");
@@ -44,22 +48,15 @@ const isActive = currentPath === targetPath;
44
48
  <a
45
49
  href={href}
46
50
  class:list={[
47
- "flex items-center px-2 py-[7px] text-sm relative z-0 before:-z-10 before:absolute before:inset-x-0 before:inset-y-px before:rounded-md before:duration-150",
51
+ "flex items-center gap-2 px-2 py-[7px] text-sm relative z-0 before:-z-10 before:absolute before:inset-x-0 before:inset-y-px before:rounded-md before:duration-150",
48
52
  isActive
49
53
  ? "before:bg-neutral-200/50 dark:before:bg-neutral-800 text-neutral-900 dark:text-neutral-200"
50
54
  : "text-neutral-600 dark:text-neutral-400 hover:before:bg-neutral-100/70 dark:hover:before:bg-neutral-800/50 hover:text-neutral-900 dark:hover:text-neutral-300",
51
55
  ]}
52
56
  >
53
- <span
54
- class:list={[
55
- "px-1 py-px mr-1.5 border rounded-md text-[10px] font-semibold uppercase",
56
- methodColors[normalizedMethod] ?? methodColors.get,
57
- ]}
58
- >
59
- {normalizedMethod !== "delete" ? normalizedMethod : "del"}
60
- </span>
61
- <div class="flex items-center gap-2 min-w-0">
62
- <span>{text}</span>
57
+ <div class="flex min-w-0 flex-1 items-center gap-2">
58
+ <span class="min-w-0 truncate">{text}</span>
63
59
  {tag && <Tag tag={tag} />}
64
60
  </div>
61
+ <Tag color={methodColor}>{methodLabel}</Tag>
65
62
  </a>
@@ -31,7 +31,7 @@ const openApiDoc = await loadOpenApiSpec(openApiPath);
31
31
 
32
32
  // Helper function to parse endpoint string
33
33
  function parseEndpointString(
34
- endpointStr: string
34
+ endpointStr: string,
35
35
  ): { method: string; path: string } | null {
36
36
  const trimmed = endpointStr.trim();
37
37
  const parts = trimmed.split(/\s+/);
@@ -57,7 +57,7 @@ function shouldIncludeEndpoint(
57
57
  method: string,
58
58
  pathStr: string,
59
59
  include?: string[],
60
- exclude?: string[]
60
+ exclude?: string[],
61
61
  ): boolean {
62
62
  const normalizedMethod = method.toUpperCase();
63
63
  const normalizedPath = pathStr.toLowerCase();
@@ -168,13 +168,7 @@ const sortedTagGroups: TagGroup[] = Array.from(tagGroups.entries())
168
168
  // If there are tagged groups, render them (including "Other" if it exists)
169
169
  hasTaggedGroups
170
170
  ? sortedTagGroups.map((group, index) => (
171
- <li
172
- class={
173
- index > 0
174
- ? "mt-5 pt-[25px] relative before:absolute before:top-0 before:inset-x-0 before:h-px before:bg-linear-[90deg,transparent,var(--color-neutral-200)_20%,var(--color-neutral-200)_80%,transparent] dark:before:bg-linear-[90deg,transparent,var(--color-neutral-700)_20%,var(--color-neutral-700)_80%,transparent]"
175
- : ""
176
- }
177
- >
171
+ <li class="my-8 first:mt-0 last:mb-0">
178
172
  <div class="text-sm font-semibold mb-2 flex items-center gap-2 px-2">
179
173
  {group.tag}
180
174
  </div>
@@ -1,5 +1,6 @@
1
1
  ---
2
- import { renderMarkdown } from "../../lib/utils";
2
+ import { methodTagColors, renderMarkdown } from "../../lib/utils";
3
+ import Tag from "./Tag.astro";
3
4
 
4
5
  interface Props {
5
6
  name: string;
@@ -73,6 +74,9 @@ if (hasMinLength && hasMaxLength) {
73
74
  } else if (hasMaxLength) {
74
75
  stringLengthLabel = `length <= ${maxLength}`;
75
76
  }
77
+
78
+ const codeChipClass =
79
+ "mx-px px-[5px] pr-1 bg-neutral-100/80 text-neutral-700/90 rounded-sm leading-none font-mono font-normal border border-neutral-900/4 after:hidden before:hidden dark:bg-neutral-800/90 dark:text-neutral-200/90 dark:border-white/3";
76
80
  ---
77
81
 
78
82
  <div class="space-y-3">
@@ -80,15 +84,14 @@ if (hasMinLength && hasMaxLength) {
80
84
  <span class="font-medium leading-4">
81
85
  {name}
82
86
  </span>
83
- <code
84
- class="text-[10px] font-medium text-neutral-500 border border-neutral-200/80 bg-neutral-50 px-1 rounded-sm dark:text-neutral-300 dark:border-neutral-700/70 dark:bg-neutral-900/70"
85
- >{type}</code
86
- >
87
+ <code class:list={["text-[10px] py-[1.5px]", codeChipClass]}>
88
+ {type}
89
+ </code>
87
90
  {
88
91
  required && (
89
- <div class="text-red-700/70 bg-red-50 border border-red-700/10 rounded-full px-1.5 text-[10px] font-mono font-medium leading-none py-0.5 pb-0.5 h-fit dark:text-red-300 dark:bg-red-950/40 dark:border-red-900/40">
92
+ <Tag color={methodTagColors.delete} class="text-[10px]!">
90
93
  required
91
- </div>
94
+ </Tag>
92
95
  )
93
96
  }
94
97
  {
@@ -114,14 +117,14 @@ if (hasMinLength && hasMaxLength) {
114
117
  <span class="font-medium text-xs">Options:</span>
115
118
  {enumValues.map((v, i, a) => {
116
119
  return (
117
- <div class="flex">
118
- <span class="text-[11px] font-medium text-neutral-600 border border-neutral-200 bg-neutral-50 px-1 py-px rounded-md dark:text-neutral-300 dark:border-neutral-700 dark:bg-neutral-900/70">
119
- <code>{v}</code>
120
- </span>
120
+ <div class="inline-flex items-center leading-none">
121
+ <code class:list={["text-[11px] py-[2px]", codeChipClass]}>
122
+ {v}
123
+ </code>
121
124
  {a.length - 2 === i ? (
122
- <span class="ml-1"> or </span>
125
+ <span class="ml-1 leading-none"> or </span>
123
126
  ) : (
124
- a.length - 1 !== i && <span>, </span>
127
+ a.length - 1 !== i && <span class="leading-none">, </span>
125
128
  )}
126
129
  </div>
127
130
  );
@@ -133,7 +136,7 @@ if (hasMinLength && hasMaxLength) {
133
136
  numericRangeLabel && (
134
137
  <div class="text-sm">
135
138
  <span class="font-medium text-xs">Range:</span>
136
- <code class="text-[11px] font-medium text-neutral-600 border border-neutral-200 bg-neutral-50 px-1 py-px rounded-md dark:text-neutral-300 dark:border-neutral-700 dark:bg-neutral-900/70">
139
+ <code class:list={["text-[11px]", codeChipClass]}>
137
140
  {numericRangeLabel}
138
141
  </code>
139
142
  </div>
@@ -143,7 +146,7 @@ if (hasMinLength && hasMaxLength) {
143
146
  stringLengthLabel && (
144
147
  <div class="text-sm">
145
148
  <span class="font-medium text-xs">Length:</span>
146
- <code class="text-[11px] font-medium text-neutral-600 border border-neutral-200 bg-neutral-50 px-1 py-px rounded-md dark:text-neutral-300 dark:border-neutral-700 dark:bg-neutral-900/70">
149
+ <code class:list={["text-[11px]", codeChipClass]}>
147
150
  {stringLengthLabel}
148
151
  </code>
149
152
  </div>
@@ -8,9 +8,10 @@ import {
8
8
  interface Props {
9
9
  tag?: NavTag;
10
10
  color?: string | ThemeColorByMode;
11
+ class?: string;
11
12
  }
12
13
 
13
- const { tag, color } = Astro.props;
14
+ const { tag, color, class: className } = Astro.props;
14
15
  const config = await getConfig();
15
16
 
16
17
  function getTagText(value: NavTag | undefined): string | undefined {
@@ -51,7 +52,10 @@ const colorStyle = resolvedColor
51
52
  ---
52
53
 
53
54
  <span
54
- class="rd-tag text-[9px] border px-[5px] py-[2px] rounded-full tracking-wide leading-none font-medium shrink-0"
55
+ class:list={[
56
+ "rd-tag text-[9px] border px-[5px] py-[2px] rounded-full tracking-wide leading-none font-medium shrink-0",
57
+ className,
58
+ ]}
55
59
  style={colorStyle}
56
60
  >
57
61
  {text ? text : <slot />}
@@ -64,4 +68,14 @@ const colorStyle = resolvedColor
64
68
  border-color: color-mix(in oklab, var(--rd-tag-color) 0%, transparent);
65
69
  color: color-mix(in oklab, var(--rd-tag-color) 95%, transparent);
66
70
  }
71
+
72
+ :global(.dark) .rd-tag,
73
+ :global([data-theme="dark"]) .rd-tag {
74
+ --rd-tag-color: var(
75
+ --rd-tag-color-dark,
76
+ var(--rd-tag-color-light, var(--color-theme))
77
+ );
78
+ background-color: color-mix(in oklab, var(--rd-tag-color) 12%, transparent);
79
+ color: color-mix(in oklab, var(--rd-tag-color) 85%, transparent);
80
+ }
67
81
  </style>
@@ -70,7 +70,7 @@ const TITLE_SIZE_CLASS = {
70
70
  <div class="min-h-0 overflow-hidden">
71
71
  <div
72
72
  data-rd-accordion-content-inner
73
- class="prose-rules max-w-none! *:max-w-none! pb-4!"
73
+ class="prose-rules max-w-none! pb-4!"
74
74
  >
75
75
  <slot />
76
76
  </div>
@@ -108,7 +108,7 @@ const hasTitle = typeof resolvedTitle === "string" && resolvedTitle.length > 0;
108
108
  {resolvedTitle}
109
109
  </div>
110
110
  </div>
111
- <div class="prose-rules prose-sm! max-w-none! *:max-w-none! text-neutral-700 dark:text-neutral-300">
111
+ <div class="prose-rules max-w-none! text-sm! leading-6! text-neutral-700 dark:text-neutral-300">
112
112
  <slot />
113
113
  </div>
114
114
  </>
@@ -129,7 +129,7 @@ const hasTitle = typeof resolvedTitle === "string" && resolvedTitle.length > 0;
129
129
  class="mt-0.5 shrink-0 border border-white"
130
130
  />
131
131
  ) : null}
132
- <div class="prose-rules prose-sm! max-w-none! min-w-0 flex-1 *:max-w-none! text-neutral-700 dark:text-neutral-300">
132
+ <div class="prose-rules max-w-none! min-w-0 flex-1 text-sm! leading-6! text-neutral-700 dark:text-neutral-300">
133
133
  <slot />
134
134
  </div>
135
135
  </div>
@@ -258,7 +258,7 @@ const renderedCodeLinesHtml = normalizedTokenLines
258
258
  ? ' data-rd-code-line-highlighted="true"'
259
259
  : "";
260
260
 
261
- return `<span class="flex min-w-full"${lineHighlightAttribute}>${lineNumberHtml}<span class="${lineContentClass}">${tokenHtml}</span></span>`;
261
+ return `<span class="flex w-max min-w-full"${lineHighlightAttribute}>${lineNumberHtml}<span class="${lineContentClass}">${tokenHtml}</span></span>`;
262
262
  })
263
263
  .join("");
264
264
  ---
@@ -339,7 +339,7 @@ const renderedCodeLinesHtml = normalizedTokenLines
339
339
 
340
340
  <div
341
341
  class:list={[
342
- "relative min-w-0 border-[0.5px] overflow-hidden border-(--rd-code-tab-edge-border)",
342
+ "relative min-w-0 border-[0.5px] overflow-hidden border-(--rd-code-tab-edge-border) bg-(--rd-code-surface)",
343
343
  parsedFlushTop ? "rounded-b-xl" : "rounded-xl",
344
344
  parsedFlushTop && "border-t-0",
345
345
  parsedShowFilename && "border-t-0 rounded-t-none",
@@ -349,10 +349,10 @@ const renderedCodeLinesHtml = normalizedTokenLines
349
349
  >
350
350
  {
351
351
  !parsedInCodeGroup && !parsedShowFilename ? (
352
- <div class="pointer-events-none absolute right-2 top-2 z-20">
352
+ <div class="pointer-events-none absolute right-2 top-2 z-20 [@media(pointer:coarse)]:pointer-events-auto">
353
353
  <button
354
354
  type="button"
355
- class="pointer-events-none inline-flex size-7 appearance-none items-center justify-center rounded-md bg-(--rd-code-surface)/40 backdrop-blur-sm text-neutral-500/80 outline-none ring-0 transition-colors duration-150 hover:text-neutral-700 focus:outline-none focus-visible:outline-none focus:ring-0 focus-visible:ring-0 cursor-pointer group-hover/prose-code:pointer-events-auto group-focus-within/prose-code:pointer-events-auto dark:text-neutral-400 dark:hover:text-neutral-200 relative before:absolute before:inset-1 hover:before:inset-0 before:rounded-md hover:before:bg-neutral-900/4 dark:hover:before:bg-white/4 before:duration-150"
355
+ class="pointer-events-none inline-flex size-7 appearance-none items-center justify-center rounded-md bg-(--rd-code-surface)/40 backdrop-blur-sm text-neutral-500/80 outline-none ring-0 transition-colors duration-150 hover:text-neutral-700 focus:outline-none focus-visible:outline-none focus:ring-0 focus-visible:ring-0 cursor-pointer group-hover/prose-code:pointer-events-auto group-focus-within/prose-code:pointer-events-auto [@media(pointer:coarse)]:pointer-events-auto dark:text-neutral-400 dark:hover:text-neutral-200 relative before:absolute before:inset-1 hover:before:inset-0 before:rounded-md hover:before:bg-neutral-900/4 dark:hover:before:bg-white/4 before:duration-150"
356
356
  data-rd-copy-trigger="true"
357
357
  data-rd-copy-content={encodedRaw}
358
358
  aria-label="Copy code"
@@ -390,10 +390,10 @@ const renderedCodeLinesHtml = normalizedTokenLines
390
390
 
391
391
  <div
392
392
  data-rd-code-scroll-area
393
- 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"
393
+ class="relative overflow-x-auto overscroll-x-contain bg-(--rd-code-surface) [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"
394
394
  >
395
395
  <pre
396
- class="relative m-0 min-w-full bg-(--rd-code-surface) p-0 text-[13px] leading-6"><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>
396
+ class="relative m-0 w-max min-w-full bg-(--rd-code-surface) p-0 text-[13px] leading-6"><code data-rd-code-theme class="block w-max min-w-full py-2.5 font-mono text-neutral-800 dark:text-neutral-200"><Fragment set:html={renderedCodeLinesHtml} /></code></pre>
397
397
  </div>
398
398
  </div>
399
399
  </div>
@@ -432,6 +432,52 @@ const renderedCodeLinesHtml = normalizedTokenLines
432
432
  checkIcon.classList.add("scale-50", "opacity-0", "rotate-6");
433
433
  };
434
434
 
435
+ const fallbackCopy = (text) => {
436
+ try {
437
+ const textarea = document.createElement("textarea");
438
+ textarea.value = text;
439
+ textarea.setAttribute("readonly", "");
440
+ textarea.style.position = "fixed";
441
+ textarea.style.top = "0";
442
+ textarea.style.left = "0";
443
+ textarea.style.width = "1px";
444
+ textarea.style.height = "1px";
445
+ textarea.style.padding = "0";
446
+ textarea.style.border = "0";
447
+ textarea.style.opacity = "0";
448
+ textarea.style.fontSize = "16px";
449
+ textarea.style.pointerEvents = "none";
450
+ document.body.appendChild(textarea);
451
+
452
+ try {
453
+ textarea.focus({ preventScroll: true });
454
+ } catch {
455
+ textarea.focus();
456
+ }
457
+ textarea.select();
458
+ textarea.setSelectionRange(0, textarea.value.length);
459
+
460
+ const copied = document.execCommand("copy");
461
+ document.body.removeChild(textarea);
462
+ return copied;
463
+ } catch {
464
+ return false;
465
+ }
466
+ };
467
+
468
+ const copyToClipboard = async (text) => {
469
+ try {
470
+ if (navigator.clipboard?.writeText) {
471
+ await navigator.clipboard.writeText(text);
472
+ return true;
473
+ }
474
+ } catch {
475
+ // Fallback below when the async Clipboard API is unavailable or blocked.
476
+ }
477
+
478
+ return fallbackCopy(text);
479
+ };
480
+
435
481
  copyButtons.forEach((button) => {
436
482
  let timeoutId = null;
437
483
  button.addEventListener("click", async () => {
@@ -440,7 +486,12 @@ const renderedCodeLinesHtml = normalizedTokenLines
440
486
  const copyValue = decodeURIComponent(encodedCopyValue);
441
487
 
442
488
  try {
443
- await navigator.clipboard.writeText(copyValue);
489
+ const copied = await copyToClipboard(copyValue);
490
+ if (!copied) {
491
+ setCopiedState(button, false);
492
+ return;
493
+ }
494
+
444
495
  setCopiedState(button, true);
445
496
 
446
497
  if (timeoutId) window.clearTimeout(timeoutId);
@@ -130,6 +130,52 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
130
130
  checkIcon.classList.add("scale-50", "opacity-0", "rotate-6");
131
131
  };
132
132
 
133
+ const fallbackCopy = (text) => {
134
+ try {
135
+ const textarea = document.createElement("textarea");
136
+ textarea.value = text;
137
+ textarea.setAttribute("readonly", "");
138
+ textarea.style.position = "fixed";
139
+ textarea.style.top = "0";
140
+ textarea.style.left = "0";
141
+ textarea.style.width = "1px";
142
+ textarea.style.height = "1px";
143
+ textarea.style.padding = "0";
144
+ textarea.style.border = "0";
145
+ textarea.style.opacity = "0";
146
+ textarea.style.fontSize = "16px";
147
+ textarea.style.pointerEvents = "none";
148
+ document.body.appendChild(textarea);
149
+
150
+ try {
151
+ textarea.focus({ preventScroll: true });
152
+ } catch {
153
+ textarea.focus();
154
+ }
155
+ textarea.select();
156
+ textarea.setSelectionRange(0, textarea.value.length);
157
+
158
+ const copied = document.execCommand("copy");
159
+ document.body.removeChild(textarea);
160
+ return copied;
161
+ } catch {
162
+ return false;
163
+ }
164
+ };
165
+
166
+ const copyToClipboard = async (text) => {
167
+ try {
168
+ if (navigator.clipboard?.writeText) {
169
+ await navigator.clipboard.writeText(text);
170
+ return true;
171
+ }
172
+ } catch {
173
+ // Fallback below when the async Clipboard API is unavailable or blocked.
174
+ }
175
+
176
+ return fallbackCopy(text);
177
+ };
178
+
133
179
  const tabs = codeItems.map((itemElement, index) => {
134
180
  const filename =
135
181
  itemElement.getAttribute("data-rd-tab-filename") ??
@@ -411,7 +457,12 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
411
457
  const copyValue = decodeURIComponent(encodedCopyValue);
412
458
 
413
459
  try {
414
- await navigator.clipboard.writeText(copyValue);
460
+ const copied = await copyToClipboard(copyValue);
461
+ if (!copied) {
462
+ setCopiedState(copyButton, false);
463
+ return;
464
+ }
465
+
415
466
  setCopiedState(copyButton, true);
416
467
 
417
468
  if (timeoutId) window.clearTimeout(timeoutId);
@@ -2,7 +2,7 @@
2
2
  ---
3
3
 
4
4
  <div data-rd-column class="min-w-0">
5
- <div class="prose-rules max-w-none! *:max-w-none!">
5
+ <div class="prose-rules max-w-none!">
6
6
  <slot />
7
7
  </div>
8
8
  </div>
@@ -27,7 +27,7 @@ const { title } = Astro.props;
27
27
  {title}
28
28
  </h3>
29
29
  </div>
30
- <div class="prose-rules max-w-none! *:max-w-none!">
30
+ <div class="prose-rules max-w-none!">
31
31
  <slot />
32
32
  </div>
33
33
  </div>