radiant-docs 0.1.68 → 0.1.69

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.68",
3
+ "version": "0.1.69",
4
4
  "description": "CLI tool for previewing Radiant documentation locally",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,9 +65,54 @@ const tocHeadings = headings.filter(
65
65
  );
66
66
  ---
67
67
 
68
- <Layout pageTitle={title} pageDescription={description}>
69
- <div class="flex w-full min-w-0 justify-between gap-x-10">
70
- <div class="mx-auto max-w-3xl w-full">
68
+ <Layout pageTitle={title} pageDescription={description} hasMarkdownAlternate>
69
+ <style is:inline>
70
+ [data-rd-mdx-shell] {
71
+ display: grid;
72
+ grid-template-columns: minmax(0, 48rem);
73
+ width: 100%;
74
+ min-width: 0;
75
+ justify-content: center;
76
+ column-gap: 2.5rem;
77
+ }
78
+
79
+ [data-rd-mdx-content] {
80
+ width: 100%;
81
+ max-width: none;
82
+ min-width: 0;
83
+ margin-inline: 0;
84
+ }
85
+
86
+ [data-rd-toc-column] {
87
+ display: none;
88
+ width: 12rem;
89
+ flex: 0 0 12rem;
90
+ }
91
+
92
+ @media (min-width: 80rem) {
93
+ [data-rd-mdx-shell] {
94
+ grid-template-columns: minmax(0, 1fr) minmax(0, 48rem) minmax(
95
+ 0,
96
+ 1fr
97
+ ) 12rem;
98
+ justify-content: stretch;
99
+ }
100
+
101
+ [data-rd-mdx-content] {
102
+ grid-column: 2;
103
+ }
104
+
105
+ [data-rd-toc-column] {
106
+ display: block;
107
+ grid-column: 4;
108
+ }
109
+ }
110
+ </style>
111
+ <div
112
+ class="flex w-full min-w-0 justify-between gap-x-10"
113
+ data-rd-mdx-shell
114
+ >
115
+ <div class="mx-auto max-w-3xl w-full" data-rd-mdx-content>
71
116
  <header
72
117
  class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"
73
118
  >
@@ -91,7 +136,7 @@ const tocHeadings = headings.filter(
91
136
  homePath={homePath}
92
137
  />
93
138
  </div>
94
- <aside class="hidden xl:block w-48 shrink-0">
139
+ <aside class="hidden xl:block w-48 shrink-0" data-rd-toc-column>
95
140
  <TableOfContents headings={tocHeadings} />
96
141
  </aside>
97
142
  </div>
@@ -59,7 +59,7 @@ const snippetStickyClass = hasTopbarNavigationTabs
59
59
  : "top-[92px] max-h-[calc(100vh-92px)]";
60
60
  ---
61
61
 
62
- <Layout pageTitle={title}>
62
+ <Layout pageTitle={title} hasMarkdownAlternate>
63
63
  <article class="mx-auto w-full max-w-7xl">
64
64
  <header
65
65
  class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"
@@ -431,24 +431,40 @@ const menuItems: MenuItem[] = [
431
431
  }
432
432
  </style>
433
433
 
434
- <script is:inline data-astro-rerun>
435
- (() => {
436
- const script = document.currentScript;
437
- let root = script?.previousElementSibling;
438
- while (
439
- root &&
440
- (!(root instanceof HTMLElement) ||
441
- !root.hasAttribute("data-rd-page-actions"))
442
- ) {
443
- root = root.previousElementSibling;
444
- }
434
+ <script>
435
+ const pageActionsSelector = "[data-rd-page-actions]";
436
+ const pageActionsControlsKey = "__rdPageActionsControls";
437
+
438
+ function fallbackCopy(text) {
439
+ const textarea = document.createElement("textarea");
440
+ textarea.value = text;
441
+ textarea.setAttribute("readonly", "");
442
+ textarea.style.position = "fixed";
443
+ textarea.style.opacity = "0";
444
+ document.body.appendChild(textarea);
445
+ textarea.select();
446
+ const copied = document.execCommand("copy");
447
+ document.body.removeChild(textarea);
448
+ return copied;
449
+ }
445
450
 
446
- if (
447
- !(root instanceof HTMLElement) ||
448
- root.dataset.pageActionsBound === "true"
449
- ) {
450
- return;
451
+ async function copyToClipboard(text) {
452
+ try {
453
+ if (navigator.clipboard?.writeText) {
454
+ await navigator.clipboard.writeText(text);
455
+ return true;
456
+ }
457
+ } catch {
458
+ // Fallback below when clipboard API is unavailable or blocked.
451
459
  }
460
+
461
+ return fallbackCopy(text);
462
+ }
463
+
464
+ function initPageActions(root) {
465
+ if (!(root instanceof HTMLElement)) return;
466
+ if (root.dataset.pageActionsBound === "true") return;
467
+
452
468
  root.dataset.pageActionsBound = "true";
453
469
 
454
470
  const trigger = root.querySelector("[data-rd-page-actions-trigger]");
@@ -463,32 +479,6 @@ const menuItems: MenuItem[] = [
463
479
  let mcpMetadataPromise;
464
480
  const copyFeedbackTimeouts = new WeakMap();
465
481
 
466
- function fallbackCopy(text) {
467
- const textarea = document.createElement("textarea");
468
- textarea.value = text;
469
- textarea.setAttribute("readonly", "");
470
- textarea.style.position = "fixed";
471
- textarea.style.opacity = "0";
472
- document.body.appendChild(textarea);
473
- textarea.select();
474
- const copied = document.execCommand("copy");
475
- document.body.removeChild(textarea);
476
- return copied;
477
- }
478
-
479
- async function copyToClipboard(text) {
480
- try {
481
- if (navigator.clipboard?.writeText) {
482
- await navigator.clipboard.writeText(text);
483
- return true;
484
- }
485
- } catch {
486
- // Fallback below when clipboard API is unavailable or blocked.
487
- }
488
-
489
- return fallbackCopy(text);
490
- }
491
-
492
482
  function setExpanded(nextExpanded) {
493
483
  if (!(trigger instanceof HTMLElement)) return;
494
484
  trigger.setAttribute("aria-expanded", String(nextExpanded));
@@ -711,5 +701,21 @@ const menuItems: MenuItem[] = [
711
701
  closeMenu();
712
702
  });
713
703
  });
714
- })();
704
+ }
705
+
706
+ function initAllPageActions() {
707
+ document.querySelectorAll(pageActionsSelector).forEach((root) => {
708
+ initPageActions(root);
709
+ });
710
+ }
711
+
712
+ window[pageActionsControlsKey] = window[pageActionsControlsKey] || {};
713
+ window[pageActionsControlsKey].init = initAllPageActions;
714
+
715
+ if (!window[pageActionsControlsKey].installed) {
716
+ window[pageActionsControlsKey].installed = true;
717
+ document.addEventListener("astro:page-load", initAllPageActions);
718
+ }
719
+
720
+ initAllPageActions();
715
721
  </script>
@@ -19,6 +19,33 @@ const config: DocsConfig = await getConfig();
19
19
  >
20
20
  <SidebarMenu navigation={config.navigation} />
21
21
  </nav>
22
+ <script is:inline>
23
+ (() => {
24
+ const script = document.currentScript;
25
+ const sidebar = script?.previousElementSibling;
26
+
27
+ if (
28
+ !(sidebar instanceof HTMLElement) ||
29
+ !sidebar.hasAttribute("data-rd-sidebar-scroll-container")
30
+ ) {
31
+ return;
32
+ }
33
+
34
+ if (document.readyState !== "loading") return;
35
+
36
+ try {
37
+ const value = Number(
38
+ sessionStorage.getItem("radiant-docs:sidebar-scroll-top"),
39
+ );
40
+ if (!Number.isFinite(value) || value <= 0) return;
41
+
42
+ sidebar.scrollTop = Math.max(0, Math.round(value));
43
+ sidebar.dataset.rdSidebarScrollRestored = "true";
44
+ } catch {
45
+ // Storage can be unavailable in private browsing or locked-down embeds.
46
+ }
47
+ })();
48
+ </script>
22
49
  <div
23
50
  class:list={[
24
51
  "bg-background z-10 px-3 pt-3 pb-[calc(env(safe-area-inset-bottom)+0.75rem)] border-t border-t-border-light flex gap-1.5 items-center",
@@ -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
  >
@@ -80,6 +80,7 @@ type ChatViewportWheelEvent = JSX.TargetedWheelEvent<HTMLDivElement>;
80
80
  type ChatViewportScrollEvent = JSX.TargetedEvent<HTMLDivElement, Event>;
81
81
 
82
82
  type PersistedPanelState = {
83
+ conversationId: string;
83
84
  messages: ChatMessage[];
84
85
  scrollTop: number;
85
86
  inFlight: boolean;
@@ -104,6 +105,16 @@ type PendingAssistantHandoff = {
104
105
  timeoutId: number;
105
106
  };
106
107
 
108
+ type AssistantPageContext = {
109
+ hostname: string | null;
110
+ pagePath: string | null;
111
+ };
112
+
113
+ type PendingPageContextRequest = {
114
+ resolve: (context: AssistantPageContext) => void;
115
+ timeoutId: number;
116
+ };
117
+
107
118
  export const ASSISTANT_PANEL_STORAGE_KEY = "docs:assistant-embed-panel:v1";
108
119
  const HANDOFF_QUERY_PARAM = "assistantHandoff";
109
120
  const OPEN_QUERY_PARAM = "assistant";
@@ -413,6 +424,7 @@ function normalizePersistedMessages(rawMessages: unknown): ChatMessage[] {
413
424
 
414
425
  function createEmptyPersistedPanelState(): PersistedPanelState {
415
426
  return {
427
+ conversationId: createMessageId(),
416
428
  messages: [],
417
429
  scrollTop: 0,
418
430
  inFlight: false,
@@ -426,6 +438,12 @@ function normalizePanelSize(value: unknown): AssistantPanelSize {
426
438
  return value === "expanded" ? "expanded" : "default";
427
439
  }
428
440
 
441
+ function normalizeConversationId(value: unknown): string {
442
+ return typeof value === "string" && value.trim()
443
+ ? value.trim().slice(0, 128)
444
+ : createMessageId();
445
+ }
446
+
429
447
  function getPanelFullscreenMediaQuery(
430
448
  mobileBreakpoint: string,
431
449
  panelFullscreenHeightBreakpoint: string,
@@ -439,6 +457,7 @@ function normalizePersistedPanelState(rawState: unknown): PersistedPanelState {
439
457
  }
440
458
 
441
459
  const state = rawState as {
460
+ conversationId?: unknown;
442
461
  messages?: unknown;
443
462
  scrollTop?: unknown;
444
463
  inFlight?: unknown;
@@ -461,6 +480,7 @@ function normalizePersistedPanelState(rawState: unknown): PersistedPanelState {
461
480
  : 0;
462
481
 
463
482
  return {
483
+ conversationId: normalizeConversationId(state.conversationId),
464
484
  messages: normalizePersistedMessages(state.messages),
465
485
  scrollTop,
466
486
  inFlight: isInFlightFresh,
@@ -515,6 +535,7 @@ function writePersistedPanelState(state: PersistedPanelState) {
515
535
  ASSISTANT_PANEL_STORAGE_KEY,
516
536
  JSON.stringify({
517
537
  messages: state.messages,
538
+ conversationId: state.conversationId,
518
539
  scrollTop:
519
540
  Number.isFinite(state.scrollTop) && state.scrollTop > 0
520
541
  ? state.scrollTop
@@ -1013,7 +1034,7 @@ function AssistantSourcesBlock({
1013
1034
  {visibleSources.map((source) => (
1014
1035
  <li key={`${source.href}\0${source.label}`}>
1015
1036
  <a
1016
- href={source.href}
1037
+ href={withBasePath(source.href)}
1017
1038
  target={linkTarget === "blank" ? "_blank" : undefined}
1018
1039
  rel={linkTarget === "blank" ? "noopener noreferrer" : undefined}
1019
1040
  >
@@ -1057,6 +1078,36 @@ function postParentMessage(
1057
1078
  window.parent.postMessage({ type, ...payload }, "*");
1058
1079
  }
1059
1080
 
1081
+ function getCurrentPageContext(): AssistantPageContext {
1082
+ if (typeof window === "undefined") {
1083
+ return { hostname: null, pagePath: null };
1084
+ }
1085
+
1086
+ return {
1087
+ hostname: window.location.hostname || null,
1088
+ pagePath: window.location.pathname || "/",
1089
+ };
1090
+ }
1091
+
1092
+ function normalizeParentPageContext(
1093
+ hostname: unknown,
1094
+ pagePath: unknown,
1095
+ ): AssistantPageContext {
1096
+ const normalizedHostname =
1097
+ typeof hostname === "string" && hostname.trim().length > 0
1098
+ ? hostname.trim().slice(0, 512)
1099
+ : null;
1100
+ const normalizedPagePath =
1101
+ typeof pagePath === "string" && pagePath.trim().startsWith("/")
1102
+ ? pagePath.trim().slice(0, 2048)
1103
+ : null;
1104
+
1105
+ return {
1106
+ hostname: normalizedHostname,
1107
+ pagePath: normalizedPagePath,
1108
+ };
1109
+ }
1110
+
1060
1111
  function getApiPath(
1061
1112
  apiPath: string,
1062
1113
  allowApiPathQueryOverride: boolean,
@@ -1174,6 +1225,11 @@ export default function AssistantEmbedPanel({
1174
1225
  const scrollPersistenceUnlockTimeoutRef = useRef<number | null>(null);
1175
1226
  const isScrollPersistenceLockedRef = useRef(false);
1176
1227
  const pendingHandoffsRef = useRef(new Map<string, PendingAssistantHandoff>());
1228
+ const pendingPageContextRequestsRef = useRef(
1229
+ new Map<string, PendingPageContextRequest>(),
1230
+ );
1231
+ const pageContextRef = useRef<AssistantPageContext>(getCurrentPageContext());
1232
+ const conversationIdRef = useRef(initialPanelState.conversationId);
1177
1233
  const messagesRef = useRef(initialPanelState.messages);
1178
1234
  const isBusyRef = useRef(initialPanelState.inFlight);
1179
1235
  const isAwaitingFirstTokenRef = useRef(
@@ -1203,6 +1259,32 @@ export default function AssistantEmbedPanel({
1203
1259
  launcherIconColors?.dark ?? launcherIconColor ?? "#ffffff";
1204
1260
  const resolvedPanelSize = panelSize ?? localPanelSize;
1205
1261
 
1262
+ const requestPageContext = (): Promise<AssistantPageContext> => {
1263
+ const currentContext = getCurrentPageContext();
1264
+ if (
1265
+ panelSurface !== "iframe" ||
1266
+ typeof window === "undefined" ||
1267
+ window.parent === window
1268
+ ) {
1269
+ pageContextRef.current = currentContext;
1270
+ return Promise.resolve(currentContext);
1271
+ }
1272
+
1273
+ return new Promise((resolve) => {
1274
+ const requestId = createMessageId();
1275
+ const timeoutId = window.setTimeout(() => {
1276
+ pendingPageContextRequestsRef.current.delete(requestId);
1277
+ resolve(pageContextRef.current);
1278
+ }, 250);
1279
+
1280
+ pendingPageContextRequestsRef.current.set(requestId, {
1281
+ resolve,
1282
+ timeoutId,
1283
+ });
1284
+ postParentMessage("assistant-embed:get-page-context", { requestId });
1285
+ });
1286
+ };
1287
+
1206
1288
  const persistPanelState = (
1207
1289
  nextMessages = messagesRef.current,
1208
1290
  scrollTop = savedScrollTopRef.current,
@@ -1227,6 +1309,7 @@ export default function AssistantEmbedPanel({
1227
1309
  }
1228
1310
 
1229
1311
  writePersistedPanelState({
1312
+ conversationId: conversationIdRef.current,
1230
1313
  messages: nextMessages,
1231
1314
  scrollTop,
1232
1315
  inFlight: isBusyRef.current,
@@ -1388,6 +1471,7 @@ export default function AssistantEmbedPanel({
1388
1471
  };
1389
1472
 
1390
1473
  const applyPersistedPanelState = (nextState: PersistedPanelState) => {
1474
+ conversationIdRef.current = nextState.conversationId;
1391
1475
  messagesRef.current = nextState.messages;
1392
1476
  savedScrollTopRef.current = nextState.scrollTop;
1393
1477
  isBusyRef.current = nextState.inFlight;
@@ -1445,6 +1529,7 @@ export default function AssistantEmbedPanel({
1445
1529
 
1446
1530
  pendingHandoffsRef.current.set(handoffId, {
1447
1531
  state: {
1532
+ conversationId: conversationIdRef.current,
1448
1533
  messages: messagesRef.current,
1449
1534
  scrollTop: savedScrollTopRef.current,
1450
1535
  inFlight: isBusyRef.current,
@@ -1774,8 +1859,35 @@ export default function AssistantEmbedPanel({
1774
1859
  type?: unknown;
1775
1860
  size?: unknown;
1776
1861
  isFullscreen?: unknown;
1862
+ hostname?: unknown;
1863
+ pagePath?: unknown;
1864
+ requestId?: unknown;
1777
1865
  };
1778
1866
 
1867
+ if (data.type === "assistant-embed:set-page-context") {
1868
+ if (event.source !== window.parent) {
1869
+ return;
1870
+ }
1871
+
1872
+ const pageContext = normalizeParentPageContext(
1873
+ data.hostname,
1874
+ data.pagePath,
1875
+ );
1876
+ pageContextRef.current = pageContext;
1877
+
1878
+ if (typeof data.requestId === "string") {
1879
+ const pendingRequest = pendingPageContextRequestsRef.current.get(
1880
+ data.requestId,
1881
+ );
1882
+ if (pendingRequest) {
1883
+ window.clearTimeout(pendingRequest.timeoutId);
1884
+ pendingPageContextRequestsRef.current.delete(data.requestId);
1885
+ pendingRequest.resolve(pageContext);
1886
+ }
1887
+ }
1888
+ return;
1889
+ }
1890
+
1779
1891
  if (data.type === "assistant-embed:panel-opened") {
1780
1892
  setEmptyStateAnimationKey((previous) => previous + 1);
1781
1893
  queueSavedScrollRestore();
@@ -1799,6 +1911,10 @@ export default function AssistantEmbedPanel({
1799
1911
  window.addEventListener("message", handlePanelMessage);
1800
1912
  return () => {
1801
1913
  window.removeEventListener("message", handlePanelMessage);
1914
+ for (const pendingRequest of pendingPageContextRequestsRef.current.values()) {
1915
+ window.clearTimeout(pendingRequest.timeoutId);
1916
+ }
1917
+ pendingPageContextRequestsRef.current.clear();
1802
1918
  };
1803
1919
  }, []);
1804
1920
 
@@ -1810,6 +1926,7 @@ export default function AssistantEmbedPanel({
1810
1926
  const handleStartNewChat = () => {
1811
1927
  activeRequestAbortRef.current?.abort();
1812
1928
  activeRequestAbortRef.current = null;
1929
+ conversationIdRef.current = createMessageId();
1813
1930
  setSharedBusy(false);
1814
1931
  setSharedAwaitingFirstToken(false);
1815
1932
  resetThreadScrollPosition([]);
@@ -1837,6 +1954,7 @@ export default function AssistantEmbedPanel({
1837
1954
  const handleUnavailableBack = () => {
1838
1955
  activeRequestAbortRef.current?.abort();
1839
1956
  activeRequestAbortRef.current = null;
1957
+ conversationIdRef.current = createMessageId();
1840
1958
  setSharedBusy(false);
1841
1959
  setSharedAwaitingFirstToken(false);
1842
1960
  resetThreadScrollPosition([]);
@@ -1893,10 +2011,14 @@ export default function AssistantEmbedPanel({
1893
2011
  requestHeaders["x-ask-ai-dev-token"] = devProxyToken;
1894
2012
  }
1895
2013
 
2014
+ const pageContext = await requestPageContext();
1896
2015
  const response = await fetch(resolvedApiPathRef.current, {
1897
2016
  method: "POST",
1898
2017
  headers: requestHeaders,
1899
2018
  body: JSON.stringify({
2019
+ conversationId: conversationIdRef.current,
2020
+ pagePath: pageContext.pagePath,
2021
+ hostname: pageContext.hostname,
1900
2022
  messages: nextConversation.map((message) => ({
1901
2023
  role: message.role,
1902
2024
  content: message.content,
@@ -2141,7 +2263,7 @@ export default function AssistantEmbedPanel({
2141
2263
  linkTarget === "current" && onCurrentLinkNavigate
2142
2264
  ? getSameOriginNavigationRequest(event)
2143
2265
  : null;
2144
- if (!navigationRequest) {
2266
+ if (!navigationRequest || !onCurrentLinkNavigate) {
2145
2267
  return;
2146
2268
  }
2147
2269
 
@@ -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>
@@ -62,6 +62,9 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
62
62
  <div
63
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
+ data-rd-preview-visible-lines={String(visibleLines)}
66
+ data-rd-preview-total-line-count={String(totalLineCount)}
67
+ data-rd-preview-show-all-code={shouldShowAllCode ? "true" : "false"}
65
68
  >
66
69
  <div
67
70
  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"
@@ -215,27 +218,29 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
215
218
  }
216
219
  </style>
217
220
 
218
- <script
219
- is:inline
220
- data-astro-rerun
221
- define:vars={{
222
- visibleLines,
223
- totalLineCount,
224
- shouldShowAllCode,
225
- }}
226
- >
227
- (() => {
228
- const script = document.currentScript;
229
- if (!(script instanceof HTMLScriptElement)) return;
230
-
231
- let root = script.previousElementSibling;
232
- while (
233
- root &&
234
- (!(root instanceof HTMLElement) ||
235
- !root.hasAttribute("data-rd-component-preview-root"))
236
- ) {
237
- root = root.previousElementSibling;
238
- }
221
+ <script>
222
+ const previewRootSelector = '[data-rd-component-preview-root="true"]';
223
+ const previewControlsKey = "__rdComponentPreviewControls";
224
+
225
+ const parsePositiveInteger = (value, fallback) => {
226
+ const parsed = Number.parseInt(value ?? "", 10);
227
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
228
+ };
229
+
230
+ const syncExpandedHeight = (root) => {
231
+ const codeWrapper = root.querySelector(".rd-component-preview__code");
232
+ if (!(codeWrapper instanceof HTMLElement)) return;
233
+
234
+ const scrollArea = codeWrapper.querySelector("[data-rd-code-scroll-area]");
235
+ if (!(scrollArea instanceof HTMLElement)) return;
236
+
237
+ codeWrapper.style.setProperty(
238
+ "--rd-preview-expanded-height",
239
+ `${scrollArea.scrollHeight}px`,
240
+ );
241
+ };
242
+
243
+ const initComponentPreview = (root) => {
239
244
  if (!(root instanceof HTMLElement)) return;
240
245
 
241
246
  const codeWrapper = root.querySelector(".rd-component-preview__code");
@@ -252,30 +257,66 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
252
257
  return;
253
258
  }
254
259
 
255
- const syncExpandedHeight = () => {
256
- codeWrapper.style.setProperty(
257
- "--rd-preview-expanded-height",
258
- `${scrollArea.scrollHeight}px`,
259
- );
260
- };
260
+ syncExpandedHeight(root);
261
+
262
+ if (root.dataset.rdComponentPreviewInitialized === "true") return;
263
+ root.dataset.rdComponentPreviewInitialized = "true";
261
264
 
262
- syncExpandedHeight();
265
+ const visibleLines = parsePositiveInteger(
266
+ root.dataset.rdPreviewVisibleLines,
267
+ 5,
268
+ );
269
+ const totalLineCount = parsePositiveInteger(
270
+ root.dataset.rdPreviewTotalLineCount,
271
+ visibleLines,
272
+ );
273
+ const shouldShowAllCode = root.dataset.rdPreviewShowAllCode === "true";
263
274
 
264
275
  if (shouldShowAllCode || totalLineCount <= visibleLines) {
265
276
  codeWrapper.dataset.rdPreviewExpanded = "true";
266
- window.addEventListener("resize", syncExpandedHeight, { passive: true });
267
277
  return;
268
278
  }
269
279
 
270
280
  const setExpanded = () => {
271
- syncExpandedHeight();
281
+ syncExpandedHeight(root);
272
282
  codeWrapper.dataset.rdPreviewExpanded = "true";
273
283
  };
274
284
 
275
285
  expandButton.addEventListener("click", () => {
276
286
  setExpanded();
277
287
  });
288
+ };
289
+
290
+ const initComponentPreviews = () => {
291
+ document.querySelectorAll(previewRootSelector).forEach((root) => {
292
+ initComponentPreview(root);
293
+ });
294
+ };
295
+
296
+ const schedulePreviewHeightSync = () => {
297
+ if (window[previewControlsKey]?.resizeFrameId) return;
298
+
299
+ window[previewControlsKey].resizeFrameId = window.requestAnimationFrame(
300
+ () => {
301
+ window[previewControlsKey].resizeFrameId = 0;
302
+ document.querySelectorAll(previewRootSelector).forEach((root) => {
303
+ if (root instanceof HTMLElement) syncExpandedHeight(root);
304
+ });
305
+ },
306
+ );
307
+ };
308
+
309
+ window[previewControlsKey] = window[previewControlsKey] || {};
310
+ window[previewControlsKey].init = initComponentPreviews;
311
+
312
+ if (!window[previewControlsKey].installed) {
313
+ window[previewControlsKey].installed = true;
314
+ window[previewControlsKey].resizeFrameId = 0;
315
+ window.addEventListener("resize", schedulePreviewHeightSync, {
316
+ passive: true,
317
+ });
318
+ document.addEventListener("astro:page-load", initComponentPreviews);
319
+ }
278
320
 
279
- window.addEventListener("resize", syncExpandedHeight, { passive: true });
280
- })();
321
+ initComponentPreviews();
281
322
  </script>
@@ -17,6 +17,7 @@ import { resolvePageDescription } from "../lib/page-description";
17
17
  interface Props {
18
18
  pageTitle?: string;
19
19
  pageDescription?: string;
20
+ hasMarkdownAlternate?: boolean;
20
21
  }
21
22
 
22
23
  type TabsPresentation = "topbar" | "sidebar";
@@ -36,7 +37,13 @@ function routePathToOgImagePath(routePath: string): string {
36
37
  return `/_og/images/${normalizedRoutePath.slice(1, -1)}.png`;
37
38
  }
38
39
 
39
- const { pageTitle, pageDescription } = Astro.props as Props;
40
+ function routePathToMarkdownPath(routePath: string): string {
41
+ const normalizedRoutePath = routePath.replace(/^\/+/, "").replace(/\/+$/, "");
42
+ return normalizedRoutePath ? `/${normalizedRoutePath}.md` : "/index.md";
43
+ }
44
+
45
+ const { pageTitle, pageDescription, hasMarkdownAlternate = false } =
46
+ Astro.props as Props;
40
47
  const config = await getConfig();
41
48
  const favicon = getFaviconConfig();
42
49
  const neutralPaletteCss = getDocsThemeCss(config.theme);
@@ -56,6 +63,12 @@ const canonicalUrl = new URL(
56
63
  prependBasePath(routePathname),
57
64
  Astro.site ?? Astro.url,
58
65
  ).toString();
66
+ const markdownUrl = hasMarkdownAlternate
67
+ ? new URL(
68
+ prependBasePath(routePathToMarkdownPath(routePathname)),
69
+ Astro.site ?? Astro.url,
70
+ ).toString()
71
+ : undefined;
59
72
  const ogImageUrl = new URL(
60
73
  resolveStaticAssetUrl(routePathToOgImagePath(routePathname)),
61
74
  Astro.site ?? Astro.url,
@@ -288,6 +301,16 @@ const hasTopbarNavigationTabs =
288
301
  />
289
302
  <meta name="generator" content={Astro.generator} />
290
303
  <link rel="canonical" href={canonicalUrl} />
304
+ {
305
+ markdownUrl && (
306
+ <link
307
+ rel="alternate"
308
+ type="text/markdown"
309
+ href={markdownUrl}
310
+ title="Markdown version"
311
+ />
312
+ )
313
+ }
291
314
  <meta property="og:url" content={canonicalUrl} />
292
315
  <meta property="og:type" content="website" />
293
316
  <meta property="og:site_name" content={config.title} />
@@ -7,10 +7,7 @@ import {
7
7
  } from "./assistant-chrome";
8
8
  import { getDocsBasePath, withBasePath } from "./base-path";
9
9
  import { getFaviconConfig } from "./favicon";
10
- import {
11
- getDocsBaseColorShade,
12
- getThemeForegroundColor,
13
- } from "./theme-css";
10
+ import { getDocsBaseColorShade, getThemeForegroundColor } from "./theme-css";
14
11
  import { type DocsConfig, type ThemeColorByMode } from "./validation";
15
12
 
16
13
  type AssistantColorByMode = {
@@ -141,7 +138,9 @@ function getAssistantButtonColor(
141
138
  config.assistant?.button?.color,
142
139
  mode,
143
140
  );
144
- return configuredAssistantColor ?? getDefaultAssistantButtonColor(config, mode);
141
+ return (
142
+ configuredAssistantColor ?? getDefaultAssistantButtonColor(config, mode)
143
+ );
145
144
  }
146
145
 
147
146
  export function getAssistantLauncherIconConfig(
@@ -905,6 +904,7 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
905
904
  }
906
905
 
907
906
  updatePanelLayout();
907
+ postPanelPageContextMessage();
908
908
 
909
909
  try {
910
910
  panelFrame.contentWindow.postMessage(
@@ -916,6 +916,28 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
916
916
  }
917
917
  }
918
918
 
919
+ function postPanelPageContextMessage(requestId) {
920
+ if (!panelFrame || !panelFrame.contentWindow) {
921
+ return;
922
+ }
923
+
924
+ var message = {
925
+ type: "assistant-embed:set-page-context",
926
+ hostname: window.location.hostname || null,
927
+ pagePath: window.location.pathname || "/",
928
+ };
929
+
930
+ if (typeof requestId === "string" && requestId.length > 0) {
931
+ message.requestId = requestId;
932
+ }
933
+
934
+ try {
935
+ panelFrame.contentWindow.postMessage(message, getPanelTargetOrigin());
936
+ } catch {
937
+ // Ignore cross-window messaging failures.
938
+ }
939
+ }
940
+
919
941
  function getPanelTargetOrigin() {
920
942
  if (!panelFrame || !panelFrame.src) {
921
943
  return "*";
@@ -1154,6 +1176,7 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
1154
1176
  frame.addEventListener("load", function () {
1155
1177
  postPanelThemeMessage();
1156
1178
  postPanelLayoutMessage();
1179
+ postPanelPageContextMessage();
1157
1180
  if (isOpen) {
1158
1181
  notifyPanelOpened();
1159
1182
  }
@@ -1293,6 +1316,11 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
1293
1316
 
1294
1317
  if (event.data.type === "assistant-embed:get-panel-layout") {
1295
1318
  postPanelLayoutMessage();
1319
+ return;
1320
+ }
1321
+
1322
+ if (event.data.type === "assistant-embed:get-page-context") {
1323
+ postPanelPageContextMessage(event.data.requestId);
1296
1324
  }
1297
1325
  });
1298
1326