radiant-docs 0.1.68 → 0.1.70

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.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import type { MarkdownHeading } from "astro";
3
3
  import { Icon } from "astro-icon/components";
4
+ import { getConfig } from "../lib/validation";
4
5
 
5
6
  interface Props {
6
7
  headings: MarkdownHeading[];
@@ -11,6 +12,18 @@ interface GroupedHeading extends MarkdownHeading {
11
12
  }
12
13
 
13
14
  const { headings } = Astro.props;
15
+ type TabsPresentation = "topbar" | "sidebar";
16
+ const config = await getConfig();
17
+ const navigationTabs = config.navigation.tabs as
18
+ | (typeof config.navigation.tabs & { presentation?: TabsPresentation })
19
+ | undefined;
20
+ const hasTopbarNavigationTabs =
21
+ Array.isArray(navigationTabs?.items) &&
22
+ navigationTabs.items.length > 0 &&
23
+ (navigationTabs.presentation ?? "topbar") === "topbar";
24
+ const stickyTopClass = hasTopbarNavigationTabs
25
+ ? "top-[calc(68px+44px+64px)]"
26
+ : "top-[calc(68px+64px)]";
14
27
 
15
28
  // Group depth 3 headings under their preceding depth 2 heading
16
29
  const groupedHeadings: GroupedHeading[] = [];
@@ -30,7 +43,7 @@ for (const heading of headings) {
30
43
  ---
31
44
 
32
45
  <nav
33
- class="sticky top-[calc(68px+64px)] text-[13px]/5"
46
+ class:list={["sticky text-[13px]/5", stickyTopClass]}
34
47
  aria-label="Table of Contents"
35
48
  data-slugs={JSON.stringify(headings.map((h) => h.slug))}
36
49
  >
@@ -6,7 +6,7 @@ import AssistantEmbedPanel, {
6
6
  AssistantPanelIcon,
7
7
  type AssistantPanelSize,
8
8
  } from "./AssistantEmbedPanel";
9
- import type { AssistantShikiRuntimeConfig } from "../../lib/assistant-shiki-client";
9
+ import type { AssistantShikiRuntimeConfig } from "@radiant-docs/assistant-shiki";
10
10
  import {
11
11
  DEFAULT_ASSISTANT_CHROME_CONFIG,
12
12
  type AssistantChromeConfig,
@@ -6,13 +6,14 @@ import remarkParse from "remark-parse";
6
6
  import remarkGfm from "remark-gfm";
7
7
  import remarkRehype from "remark-rehype";
8
8
  import rehypeStringify from "rehype-stringify";
9
- import { getDocsBasePath, withBasePath } from "../../lib/base-path";
10
9
  import {
11
10
  highlightAssistantCodeToHtml,
12
11
  normalizeAssistantCodeLanguage,
12
+ resolveAssistantLanguageLabel,
13
13
  warmAssistantShikiRuntime,
14
14
  type AssistantShikiRuntimeConfig,
15
- } from "../../lib/assistant-shiki-client";
15
+ } from "@radiant-docs/assistant-shiki";
16
+ import { getDocsBasePath, withBasePath } from "../../lib/base-path";
16
17
 
17
18
  type AssistantLinkTarget = "current" | "blank";
18
19
  export type AssistantPanelSize = "default" | "expanded";
@@ -80,6 +81,7 @@ type ChatViewportWheelEvent = JSX.TargetedWheelEvent<HTMLDivElement>;
80
81
  type ChatViewportScrollEvent = JSX.TargetedEvent<HTMLDivElement, Event>;
81
82
 
82
83
  type PersistedPanelState = {
84
+ conversationId: string;
83
85
  messages: ChatMessage[];
84
86
  scrollTop: number;
85
87
  inFlight: boolean;
@@ -104,6 +106,16 @@ type PendingAssistantHandoff = {
104
106
  timeoutId: number;
105
107
  };
106
108
 
109
+ type AssistantPageContext = {
110
+ hostname: string | null;
111
+ pagePath: string | null;
112
+ };
113
+
114
+ type PendingPageContextRequest = {
115
+ resolve: (context: AssistantPageContext) => void;
116
+ timeoutId: number;
117
+ };
118
+
107
119
  export const ASSISTANT_PANEL_STORAGE_KEY = "docs:assistant-embed-panel:v1";
108
120
  const HANDOFF_QUERY_PARAM = "assistantHandoff";
109
121
  const OPEN_QUERY_PARAM = "assistant";
@@ -118,30 +130,6 @@ const DEFAULT_ASSISTANT_MOBILE_BREAKPOINT = "640px";
118
130
  const DEFAULT_ASSISTANT_PANEL_FULLSCREEN_HEIGHT_BREAKPOINT = "560px";
119
131
  const markdownHtmlCache = new Map<string, string>();
120
132
  const markdownHtmlPromiseCache = new Map<string, Promise<string>>();
121
- const ASSISTANT_LANGUAGE_LABEL: Record<string, string> = {
122
- bash: "Bash",
123
- css: "CSS",
124
- diff: "Diff",
125
- go: "Go",
126
- html: "HTML",
127
- java: "Java",
128
- javascript: "JavaScript",
129
- jsx: "JSX",
130
- json: "JSON",
131
- markdown: "Markdown",
132
- mdx: "MDX",
133
- php: "PHP",
134
- plaintext: "Text",
135
- python: "Python",
136
- ruby: "Ruby",
137
- rust: "Rust",
138
- shell: "Shell",
139
- sql: "SQL",
140
- tsx: "TSX",
141
- typescript: "TypeScript",
142
- yaml: "YAML",
143
- };
144
-
145
133
  const EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
146
134
 
147
135
  function buildMarkdownCacheKey(
@@ -413,6 +401,7 @@ function normalizePersistedMessages(rawMessages: unknown): ChatMessage[] {
413
401
 
414
402
  function createEmptyPersistedPanelState(): PersistedPanelState {
415
403
  return {
404
+ conversationId: createMessageId(),
416
405
  messages: [],
417
406
  scrollTop: 0,
418
407
  inFlight: false,
@@ -426,6 +415,12 @@ function normalizePanelSize(value: unknown): AssistantPanelSize {
426
415
  return value === "expanded" ? "expanded" : "default";
427
416
  }
428
417
 
418
+ function normalizeConversationId(value: unknown): string {
419
+ return typeof value === "string" && value.trim()
420
+ ? value.trim().slice(0, 128)
421
+ : createMessageId();
422
+ }
423
+
429
424
  function getPanelFullscreenMediaQuery(
430
425
  mobileBreakpoint: string,
431
426
  panelFullscreenHeightBreakpoint: string,
@@ -439,6 +434,7 @@ function normalizePersistedPanelState(rawState: unknown): PersistedPanelState {
439
434
  }
440
435
 
441
436
  const state = rawState as {
437
+ conversationId?: unknown;
442
438
  messages?: unknown;
443
439
  scrollTop?: unknown;
444
440
  inFlight?: unknown;
@@ -461,6 +457,7 @@ function normalizePersistedPanelState(rawState: unknown): PersistedPanelState {
461
457
  : 0;
462
458
 
463
459
  return {
460
+ conversationId: normalizeConversationId(state.conversationId),
464
461
  messages: normalizePersistedMessages(state.messages),
465
462
  scrollTop,
466
463
  inFlight: isInFlightFresh,
@@ -515,6 +512,7 @@ function writePersistedPanelState(state: PersistedPanelState) {
515
512
  ASSISTANT_PANEL_STORAGE_KEY,
516
513
  JSON.stringify({
517
514
  messages: state.messages,
515
+ conversationId: state.conversationId,
518
516
  scrollTop:
519
517
  Number.isFinite(state.scrollTop) && state.scrollTop > 0
520
518
  ? state.scrollTop
@@ -612,20 +610,6 @@ function escapeHtml(value: string): string {
612
610
  .replaceAll("'", "&#39;");
613
611
  }
614
612
 
615
- function resolveAssistantLanguageLabel(language: string): string {
616
- const normalized = language.trim().toLowerCase();
617
- if (!normalized) {
618
- return "";
619
- }
620
-
621
- return (
622
- ASSISTANT_LANGUAGE_LABEL[normalized] ??
623
- normalized
624
- .replace(/[-_]+/g, " ")
625
- .replace(/\b\w/g, (char) => char.toUpperCase())
626
- );
627
- }
628
-
629
613
  function ensureCodeBlockCopyButton(preElement: HTMLPreElement): void {
630
614
  const existingButton = Array.from(preElement.children).find(
631
615
  (child) =>
@@ -1004,20 +988,33 @@ function AssistantSourcesBlock({
1004
988
  }
1005
989
 
1006
990
  return (
1007
- <div
1008
- className="ask-ai-sources ask-ai-markdown prose-rules mt-3 max-w-full min-w-0 wrap-break-word text-[15px]!"
1009
- onClick={onClick}
1010
- >
1011
- <p>Sources:</p>
1012
- <ul>
991
+ <div className="ask-ai-sources mt-4 max-w-full min-w-0" onClick={onClick}>
992
+ <div className="text-xs font-semibold text-neutral-700 dark:text-neutral-200">
993
+ Sources
994
+ </div>
995
+ <ul className="mt-2 flex flex-wrap gap-2">
1013
996
  {visibleSources.map((source) => (
1014
- <li key={`${source.href}\0${source.label}`}>
997
+ <li
998
+ key={`${source.href}\0${source.label}`}
999
+ className="min-w-0 max-w-full"
1000
+ >
1015
1001
  <a
1016
- href={source.href}
1002
+ href={withBasePath(source.href)}
1017
1003
  target={linkTarget === "blank" ? "_blank" : undefined}
1018
1004
  rel={linkTarget === "blank" ? "noopener noreferrer" : undefined}
1005
+ className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-neutral-200 bg-white px-2 py-1 text-xs font-medium text-neutral-700 transition-colors hover:bg-neutral-50 hover:text-neutral-950 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800 dark:hover:text-white"
1019
1006
  >
1020
- {source.label}
1007
+ <Icon
1008
+ icon="lucide:file-text"
1009
+ className="size-3.5 shrink-0"
1010
+ aria-hidden="true"
1011
+ />
1012
+ <span className="truncate">{source.label}</span>
1013
+ <Icon
1014
+ icon="lucide:external-link"
1015
+ className="size-3 shrink-0 text-neutral-500 dark:text-neutral-400"
1016
+ aria-hidden="true"
1017
+ />
1021
1018
  </a>
1022
1019
  </li>
1023
1020
  ))}
@@ -1057,6 +1054,36 @@ function postParentMessage(
1057
1054
  window.parent.postMessage({ type, ...payload }, "*");
1058
1055
  }
1059
1056
 
1057
+ function getCurrentPageContext(): AssistantPageContext {
1058
+ if (typeof window === "undefined") {
1059
+ return { hostname: null, pagePath: null };
1060
+ }
1061
+
1062
+ return {
1063
+ hostname: window.location.hostname || null,
1064
+ pagePath: window.location.pathname || "/",
1065
+ };
1066
+ }
1067
+
1068
+ function normalizeParentPageContext(
1069
+ hostname: unknown,
1070
+ pagePath: unknown,
1071
+ ): AssistantPageContext {
1072
+ const normalizedHostname =
1073
+ typeof hostname === "string" && hostname.trim().length > 0
1074
+ ? hostname.trim().slice(0, 512)
1075
+ : null;
1076
+ const normalizedPagePath =
1077
+ typeof pagePath === "string" && pagePath.trim().startsWith("/")
1078
+ ? pagePath.trim().slice(0, 2048)
1079
+ : null;
1080
+
1081
+ return {
1082
+ hostname: normalizedHostname,
1083
+ pagePath: normalizedPagePath,
1084
+ };
1085
+ }
1086
+
1060
1087
  function getApiPath(
1061
1088
  apiPath: string,
1062
1089
  allowApiPathQueryOverride: boolean,
@@ -1174,6 +1201,11 @@ export default function AssistantEmbedPanel({
1174
1201
  const scrollPersistenceUnlockTimeoutRef = useRef<number | null>(null);
1175
1202
  const isScrollPersistenceLockedRef = useRef(false);
1176
1203
  const pendingHandoffsRef = useRef(new Map<string, PendingAssistantHandoff>());
1204
+ const pendingPageContextRequestsRef = useRef(
1205
+ new Map<string, PendingPageContextRequest>(),
1206
+ );
1207
+ const pageContextRef = useRef<AssistantPageContext>(getCurrentPageContext());
1208
+ const conversationIdRef = useRef(initialPanelState.conversationId);
1177
1209
  const messagesRef = useRef(initialPanelState.messages);
1178
1210
  const isBusyRef = useRef(initialPanelState.inFlight);
1179
1211
  const isAwaitingFirstTokenRef = useRef(
@@ -1203,6 +1235,32 @@ export default function AssistantEmbedPanel({
1203
1235
  launcherIconColors?.dark ?? launcherIconColor ?? "#ffffff";
1204
1236
  const resolvedPanelSize = panelSize ?? localPanelSize;
1205
1237
 
1238
+ const requestPageContext = (): Promise<AssistantPageContext> => {
1239
+ const currentContext = getCurrentPageContext();
1240
+ if (
1241
+ panelSurface !== "iframe" ||
1242
+ typeof window === "undefined" ||
1243
+ window.parent === window
1244
+ ) {
1245
+ pageContextRef.current = currentContext;
1246
+ return Promise.resolve(currentContext);
1247
+ }
1248
+
1249
+ return new Promise((resolve) => {
1250
+ const requestId = createMessageId();
1251
+ const timeoutId = window.setTimeout(() => {
1252
+ pendingPageContextRequestsRef.current.delete(requestId);
1253
+ resolve(pageContextRef.current);
1254
+ }, 250);
1255
+
1256
+ pendingPageContextRequestsRef.current.set(requestId, {
1257
+ resolve,
1258
+ timeoutId,
1259
+ });
1260
+ postParentMessage("assistant-embed:get-page-context", { requestId });
1261
+ });
1262
+ };
1263
+
1206
1264
  const persistPanelState = (
1207
1265
  nextMessages = messagesRef.current,
1208
1266
  scrollTop = savedScrollTopRef.current,
@@ -1227,6 +1285,7 @@ export default function AssistantEmbedPanel({
1227
1285
  }
1228
1286
 
1229
1287
  writePersistedPanelState({
1288
+ conversationId: conversationIdRef.current,
1230
1289
  messages: nextMessages,
1231
1290
  scrollTop,
1232
1291
  inFlight: isBusyRef.current,
@@ -1388,6 +1447,7 @@ export default function AssistantEmbedPanel({
1388
1447
  };
1389
1448
 
1390
1449
  const applyPersistedPanelState = (nextState: PersistedPanelState) => {
1450
+ conversationIdRef.current = nextState.conversationId;
1391
1451
  messagesRef.current = nextState.messages;
1392
1452
  savedScrollTopRef.current = nextState.scrollTop;
1393
1453
  isBusyRef.current = nextState.inFlight;
@@ -1445,6 +1505,7 @@ export default function AssistantEmbedPanel({
1445
1505
 
1446
1506
  pendingHandoffsRef.current.set(handoffId, {
1447
1507
  state: {
1508
+ conversationId: conversationIdRef.current,
1448
1509
  messages: messagesRef.current,
1449
1510
  scrollTop: savedScrollTopRef.current,
1450
1511
  inFlight: isBusyRef.current,
@@ -1774,8 +1835,35 @@ export default function AssistantEmbedPanel({
1774
1835
  type?: unknown;
1775
1836
  size?: unknown;
1776
1837
  isFullscreen?: unknown;
1838
+ hostname?: unknown;
1839
+ pagePath?: unknown;
1840
+ requestId?: unknown;
1777
1841
  };
1778
1842
 
1843
+ if (data.type === "assistant-embed:set-page-context") {
1844
+ if (event.source !== window.parent) {
1845
+ return;
1846
+ }
1847
+
1848
+ const pageContext = normalizeParentPageContext(
1849
+ data.hostname,
1850
+ data.pagePath,
1851
+ );
1852
+ pageContextRef.current = pageContext;
1853
+
1854
+ if (typeof data.requestId === "string") {
1855
+ const pendingRequest = pendingPageContextRequestsRef.current.get(
1856
+ data.requestId,
1857
+ );
1858
+ if (pendingRequest) {
1859
+ window.clearTimeout(pendingRequest.timeoutId);
1860
+ pendingPageContextRequestsRef.current.delete(data.requestId);
1861
+ pendingRequest.resolve(pageContext);
1862
+ }
1863
+ }
1864
+ return;
1865
+ }
1866
+
1779
1867
  if (data.type === "assistant-embed:panel-opened") {
1780
1868
  setEmptyStateAnimationKey((previous) => previous + 1);
1781
1869
  queueSavedScrollRestore();
@@ -1799,6 +1887,10 @@ export default function AssistantEmbedPanel({
1799
1887
  window.addEventListener("message", handlePanelMessage);
1800
1888
  return () => {
1801
1889
  window.removeEventListener("message", handlePanelMessage);
1890
+ for (const pendingRequest of pendingPageContextRequestsRef.current.values()) {
1891
+ window.clearTimeout(pendingRequest.timeoutId);
1892
+ }
1893
+ pendingPageContextRequestsRef.current.clear();
1802
1894
  };
1803
1895
  }, []);
1804
1896
 
@@ -1810,6 +1902,7 @@ export default function AssistantEmbedPanel({
1810
1902
  const handleStartNewChat = () => {
1811
1903
  activeRequestAbortRef.current?.abort();
1812
1904
  activeRequestAbortRef.current = null;
1905
+ conversationIdRef.current = createMessageId();
1813
1906
  setSharedBusy(false);
1814
1907
  setSharedAwaitingFirstToken(false);
1815
1908
  resetThreadScrollPosition([]);
@@ -1837,6 +1930,7 @@ export default function AssistantEmbedPanel({
1837
1930
  const handleUnavailableBack = () => {
1838
1931
  activeRequestAbortRef.current?.abort();
1839
1932
  activeRequestAbortRef.current = null;
1933
+ conversationIdRef.current = createMessageId();
1840
1934
  setSharedBusy(false);
1841
1935
  setSharedAwaitingFirstToken(false);
1842
1936
  resetThreadScrollPosition([]);
@@ -1893,10 +1987,14 @@ export default function AssistantEmbedPanel({
1893
1987
  requestHeaders["x-ask-ai-dev-token"] = devProxyToken;
1894
1988
  }
1895
1989
 
1990
+ const pageContext = await requestPageContext();
1896
1991
  const response = await fetch(resolvedApiPathRef.current, {
1897
1992
  method: "POST",
1898
1993
  headers: requestHeaders,
1899
1994
  body: JSON.stringify({
1995
+ conversationId: conversationIdRef.current,
1996
+ pagePath: pageContext.pagePath,
1997
+ hostname: pageContext.hostname,
1900
1998
  messages: nextConversation.map((message) => ({
1901
1999
  role: message.role,
1902
2000
  content: message.content,
@@ -2141,7 +2239,7 @@ export default function AssistantEmbedPanel({
2141
2239
  linkTarget === "current" && onCurrentLinkNavigate
2142
2240
  ? getSameOriginNavigationRequest(event)
2143
2241
  : null;
2144
- if (!navigationRequest) {
2242
+ if (!navigationRequest || !onCurrentLinkNavigate) {
2145
2243
  return;
2146
2244
  }
2147
2245
 
@@ -2724,6 +2822,18 @@ export default function AssistantEmbedPanel({
2724
2822
  -ms-overflow-style: none;
2725
2823
  }
2726
2824
 
2825
+ .ask-ai-markdown [data-rd-code-line] {
2826
+ display: flex;
2827
+ width: max-content;
2828
+ min-width: 100%;
2829
+ }
2830
+
2831
+ .ask-ai-markdown [data-rd-code-line-content] {
2832
+ flex: 1;
2833
+ padding-inline: 0.75rem;
2834
+ white-space: pre;
2835
+ }
2836
+
2727
2837
  .dark .ask-ai-markdown pre > code {
2728
2838
  scrollbar-width: none;
2729
2839
  }
@@ -1074,7 +1074,7 @@ const headerValuePrefixes = Object.fromEntries(
1074
1074
  import {
1075
1075
  highlightAssistantCodeToHtml,
1076
1076
  type AssistantShikiRuntimeConfig,
1077
- } from "../../lib/assistant-shiki-client";
1077
+ } from "@radiant-docs/assistant-shiki";
1078
1078
 
1079
1079
  type PlaygroundShikiConfig = AssistantShikiRuntimeConfig | null | undefined;
1080
1080
 
@@ -397,90 +397,101 @@ const renderedCodeLinesHtml = normalizedTokenLines
397
397
  </div>
398
398
  </div>
399
399
  </div>
400
- <script is:inline data-astro-rerun>
401
- (() => {
402
- const script = document.currentScript;
403
- if (!(script instanceof HTMLScriptElement)) return;
400
+ <script>
401
+ const codeBlockRootSelector = "[data-rd-code-block-root='true']";
402
+ const codeBlockControlsKey = "__rdCodeBlockControls";
403
+
404
+ const getCodeBlockControls = () => {
405
+ window[codeBlockControlsKey] = window[codeBlockControlsKey] || {
406
+ copiedStateTimers: new WeakMap(),
407
+ installed: false,
408
+ };
404
409
 
405
- const root = script.closest("[data-rd-code-block-root='true']");
406
- if (!(root instanceof HTMLElement)) return;
410
+ return window[codeBlockControlsKey];
411
+ };
407
412
 
408
- const copyButtons = root.querySelectorAll(
409
- "[data-rd-copy-trigger='true']",
410
- );
411
- if (copyButtons.length === 0) return;
413
+ const setCopiedState = (button, copied) => {
414
+ const copyIcon = button.querySelector("[data-rd-copy-icon]");
415
+ const checkIcon = button.querySelector("[data-rd-copy-check]");
412
416
 
413
- const setCopiedState = (button, copied) => {
414
- const copyIcon = button.querySelector("[data-rd-copy-icon]");
415
- const checkIcon = button.querySelector("[data-rd-copy-check]");
417
+ if (!copyIcon || !checkIcon) return;
416
418
 
417
- if (!copyIcon || !checkIcon) return;
419
+ if (copied) {
420
+ copyIcon.classList.add("scale-50", "opacity-0", "-rotate-6");
421
+ copyIcon.classList.remove("scale-100", "opacity-100", "rotate-0");
418
422
 
419
- if (copied) {
420
- copyIcon.classList.add("scale-50", "opacity-0", "-rotate-6");
421
- copyIcon.classList.remove("scale-100", "opacity-100", "rotate-0");
422
-
423
- checkIcon.classList.remove("scale-50", "opacity-0", "rotate-6");
424
- checkIcon.classList.add("scale-110", "opacity-100", "rotate-0");
425
- return;
426
- }
427
-
428
- copyIcon.classList.remove("scale-50", "opacity-0", "-rotate-6");
429
- copyIcon.classList.add("scale-100", "opacity-100", "rotate-0");
423
+ checkIcon.classList.remove("scale-50", "opacity-0", "rotate-6");
424
+ checkIcon.classList.add("scale-110", "opacity-100", "rotate-0");
425
+ return;
426
+ }
430
427
 
431
- checkIcon.classList.remove("scale-110", "opacity-100", "rotate-0");
432
- checkIcon.classList.add("scale-50", "opacity-0", "rotate-6");
433
- };
428
+ copyIcon.classList.remove("scale-50", "opacity-0", "-rotate-6");
429
+ copyIcon.classList.add("scale-100", "opacity-100", "rotate-0");
430
+
431
+ checkIcon.classList.remove("scale-110", "opacity-100", "rotate-0");
432
+ checkIcon.classList.add("scale-50", "opacity-0", "rotate-6");
433
+ };
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);
434
451
 
435
- const fallbackCopy = (text) => {
436
452
  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;
453
+ textarea.focus({ preventScroll: true });
463
454
  } catch {
464
- return false;
455
+ textarea.focus();
465
456
  }
466
- };
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
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.
468
+ const copyToClipboard = async (text) => {
469
+ try {
470
+ if (navigator.clipboard?.writeText) {
471
+ await navigator.clipboard.writeText(text);
472
+ return true;
476
473
  }
474
+ } catch {
475
+ // Fallback below when the async Clipboard API is unavailable or blocked.
476
+ }
477
477
 
478
- return fallbackCopy(text);
479
- };
478
+ return fallbackCopy(text);
479
+ };
480
+
481
+ const initCodeBlock = (root) => {
482
+ if (!(root instanceof HTMLElement)) return;
483
+ if (root.dataset.rdCodeBlockInitialized === "true") return;
484
+
485
+ const copyButtons = root.querySelectorAll("[data-rd-copy-trigger='true']");
486
+ if (copyButtons.length === 0) return;
487
+
488
+ root.dataset.rdCodeBlockInitialized = "true";
480
489
 
481
490
  copyButtons.forEach((button) => {
482
- let timeoutId = null;
491
+ if (!(button instanceof HTMLElement)) return;
492
+
483
493
  button.addEventListener("click", async () => {
494
+ const controls = getCodeBlockControls();
484
495
  const encodedCopyValue =
485
496
  button.getAttribute("data-rd-copy-content") ?? "";
486
497
  const copyValue = decodeURIComponent(encodedCopyValue);
@@ -494,16 +505,35 @@ const renderedCodeLinesHtml = normalizedTokenLines
494
505
 
495
506
  setCopiedState(button, true);
496
507
 
497
- if (timeoutId) window.clearTimeout(timeoutId);
498
- timeoutId = window.setTimeout(() => {
508
+ const existingTimeoutId = controls.copiedStateTimers.get(button);
509
+ if (existingTimeoutId) window.clearTimeout(existingTimeoutId);
510
+
511
+ const timeoutId = window.setTimeout(() => {
499
512
  setCopiedState(button, false);
500
- timeoutId = null;
513
+ controls.copiedStateTimers.delete(button);
501
514
  }, 1200);
515
+ controls.copiedStateTimers.set(button, timeoutId);
502
516
  } catch {
503
517
  setCopiedState(button, false);
504
518
  }
505
519
  });
506
520
  });
507
- })();
521
+ };
522
+
523
+ const initCodeBlocks = () => {
524
+ document.querySelectorAll(codeBlockRootSelector).forEach((root) => {
525
+ initCodeBlock(root);
526
+ });
527
+ };
528
+
529
+ const controls = getCodeBlockControls();
530
+ controls.init = initCodeBlocks;
531
+
532
+ if (!controls.installed) {
533
+ controls.installed = true;
534
+ document.addEventListener("astro:page-load", initCodeBlocks);
535
+ }
536
+
537
+ initCodeBlocks();
508
538
  </script>
509
539
  </div>