radiant-docs 0.1.61 → 0.1.63

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 (39) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +38 -27
  3. package/template/package-lock.json +2858 -1140
  4. package/template/package.json +18 -13
  5. package/template/scripts/generate-proxy-allowed-origins.mjs +10 -179
  6. package/template/scripts/publish-shiki-platform-assets.mjs +1177 -0
  7. package/template/src/components/Header.astro +6 -1
  8. package/template/src/components/NavigationTabList.astro +65 -0
  9. package/template/src/components/NavigationTabs.astro +109 -0
  10. package/template/src/components/OpenApiPage.astro +17 -1
  11. package/template/src/components/Sidebar.astro +2 -2
  12. package/template/src/components/SidebarDropdown.astro +105 -44
  13. package/template/src/components/SidebarMenu.astro +3 -0
  14. package/template/src/components/SidebarSegmented.astro +87 -52
  15. package/template/src/components/SidebarTabs.astro +86 -0
  16. package/template/src/components/chat/AssistantDocsWidget.tsx +127 -2
  17. package/template/src/components/chat/AssistantEmbedPanel.tsx +401 -283
  18. package/template/src/components/endpoint/PlaygroundForm.astro +69 -55
  19. package/template/src/components/endpoint/ResponseDisplay.astro +2 -2
  20. package/template/src/components/user/Accordion.astro +1 -1
  21. package/template/src/components/user/Callout.astro +2 -2
  22. package/template/src/components/user/CodeBlock.astro +58 -7
  23. package/template/src/components/user/CodeGroup.astro +52 -1
  24. package/template/src/components/user/Column.astro +1 -1
  25. package/template/src/components/user/Step.astro +1 -1
  26. package/template/src/components/user/Tabs.astro +1 -1
  27. package/template/src/generated/shiki-platform-assets.json +24 -0
  28. package/template/src/layouts/Layout.astro +111 -8
  29. package/template/src/lib/assistant-panel-config.ts +4 -0
  30. package/template/src/lib/assistant-shiki-client.ts +522 -0
  31. package/template/src/lib/client-shiki-config.ts +60 -0
  32. package/template/src/lib/dev-playground-proxy.mjs +597 -0
  33. package/template/src/lib/mdx/remark-resolve-internal-links.ts +334 -17
  34. package/template/src/lib/proxy-allowed-origins.mjs +189 -0
  35. package/template/src/lib/routes.ts +66 -24
  36. package/template/src/styles/global.css +16 -4
  37. package/template/src/components/ui/demo/CodeDemo.astro +0 -15
  38. package/template/src/components/ui/demo/Demo.astro +0 -3
  39. package/template/src/components/ui/demo/UiDisplay.astro +0 -13
@@ -4,6 +4,7 @@ import { withBasePath } from "../../lib/base-path";
4
4
  import type { OpenApiRoute } from "../../lib/routes";
5
5
  import { getConfig } from "../../lib/validation";
6
6
  import { renderMarkdown } from "../../lib/utils";
7
+ import { getClientShikiRuntimeConfig } from "../../lib/client-shiki-config";
7
8
  import {
8
9
  OPENAPI_REQUEST_SECTION_LABELS,
9
10
  type OpenApiRequestFields,
@@ -38,6 +39,7 @@ const {
38
39
  } = Astro.props;
39
40
  const headers: Record<string, string> = OPENAPI_REQUEST_SECTION_LABELS;
40
41
  const config = await getConfig();
42
+ const shikiRuntimeConfig = getClientShikiRuntimeConfig(config);
41
43
  const configuredProxyUrl =
42
44
  typeof import.meta.env.PUBLIC_PROXY_URL === "string"
43
45
  ? import.meta.env.PUBLIC_PROXY_URL.trim()
@@ -77,6 +79,7 @@ const sectionVariantFieldNames = Object.fromEntries(
77
79
  queryFieldMeta: ${JSON.stringify(queryFieldMeta)},
78
80
  sectionVariantFieldNames: ${JSON.stringify(sectionVariantFieldNames)},
79
81
  bodyDefaultKind: ${JSON.stringify(bodyDefaultKind ?? null)},
82
+ shiki: ${JSON.stringify(shikiRuntimeConfig ?? null)},
80
83
  selectedSectionVariants: {},
81
84
  inputs: {
82
85
  header: {},
@@ -476,7 +479,7 @@ const sectionVariantFieldNames = Object.fromEntries(
476
479
  }
477
480
  };
478
481
  }
479
- console.log("SEND", finalUrl, finalOptions)
482
+
480
483
  // 6. Execute Request
481
484
  const res = await fetch(finalUrl, finalOptions);
482
485
 
@@ -487,11 +490,12 @@ const sectionVariantFieldNames = Object.fromEntries(
487
490
  responseHeaders[key] = value;
488
491
  });
489
492
 
490
- let highlightedData;
491
- const highlightedHeaders = Prism.highlight(
493
+ let responseDataText;
494
+ let responseDataLanguage = 'plaintext';
495
+ const highlightedHeaders = await window.RadiantPlaygroundHighlightCode(
492
496
  JSON.stringify(responseHeaders, null, 2),
493
- Prism.languages.json,
494
- "json",
497
+ 'json',
498
+ this.shiki,
495
499
  );
496
500
  const contentType = res.headers.get('content-type') || '';
497
501
  const responseText = await res.text();
@@ -500,25 +504,19 @@ const sectionVariantFieldNames = Object.fromEntries(
500
504
  try {
501
505
  const parsed =
502
506
  responseText.trim().length > 0 ? JSON.parse(responseText) : null;
503
- highlightedData = Prism.highlight(
504
- JSON.stringify(parsed, null, 2),
505
- Prism.languages.json,
506
- 'json'
507
- );
507
+ responseDataText = JSON.stringify(parsed, null, 2);
508
+ responseDataLanguage = 'json';
508
509
  } catch (parseError) {
509
- highlightedData = Prism.highlight(
510
- responseText || '(empty response)',
511
- Prism.languages.plaintext,
512
- 'plaintext'
513
- );
510
+ responseDataText = responseText || '(empty response)';
514
511
  }
515
512
  } else {
516
- highlightedData = Prism.highlight(
517
- responseText || '(empty response)',
518
- Prism.languages.plaintext,
519
- 'plaintext'
520
- );
513
+ responseDataText = responseText || '(empty response)';
521
514
  }
515
+ const highlightedData = await window.RadiantPlaygroundHighlightCode(
516
+ responseDataText,
517
+ responseDataLanguage,
518
+ this.shiki,
519
+ );
522
520
 
523
521
  this.response = {
524
522
  status,
@@ -682,45 +680,61 @@ const sectionVariantFieldNames = Object.fromEntries(
682
680
  </div>
683
681
 
684
682
  <script>
685
- import Prism from "prismjs";
686
- import "prismjs/components/prism-json";
687
- // Use a dark theme that matches the #0d1117 background
688
- import "prismjs/themes/prism-tomorrow.css";
689
- // Make Prism available to Alpine
690
- window.Prism = Prism;
691
- </script>
692
-
693
- <style>
694
- @reference "../../styles/global.css";
695
-
696
- :global(pre code span.token.punctuation) {
697
- color: #24292e;
683
+ import {
684
+ highlightAssistantCodeToHtml,
685
+ type AssistantShikiRuntimeConfig,
686
+ } from "../../lib/assistant-shiki-client";
687
+
688
+ type PlaygroundShikiConfig = AssistantShikiRuntimeConfig | null | undefined;
689
+
690
+ function escapeHtml(value: string): string {
691
+ return value
692
+ .replaceAll("&", "&amp;")
693
+ .replaceAll("<", "&lt;")
694
+ .replaceAll(">", "&gt;")
695
+ .replaceAll('"', "&quot;")
696
+ .replaceAll("'", "&#39;");
698
697
  }
699
698
 
700
- :global(pre code span.token.operator) {
701
- color: #24292e;
702
- }
699
+ function renderPlainCodeToHtml(code: string): string {
700
+ const lines = code.split("\n");
701
+ const normalizedLines = lines.length > 0 ? lines : [""];
703
702
 
704
- :global(pre code span.token.property) {
705
- color: #c62c2c;
706
- }
707
- :global(pre code span.token.string) {
708
- color: #1e7734;
703
+ return normalizedLines
704
+ .map((line) => {
705
+ const lineHtml = line.length > 0 ? escapeHtml(line) : "&nbsp;";
706
+ return `<span class="flex w-max min-w-full"><span class="flex-1 whitespace-pre pl-3 pr-3">${lineHtml}</span></span>`;
707
+ })
708
+ .join("");
709
709
  }
710
710
 
711
- :global(.dark pre code span.token.punctuation) {
712
- color: #c9d1d9;
713
- }
714
-
715
- :global(.dark pre code span.token.operator) {
716
- color: #c9d1d9;
717
- }
718
-
719
- :global(.dark pre code span.token.property) {
720
- color: #ff7b72;
711
+ async function highlightPlaygroundCodeToHtml(
712
+ code: unknown,
713
+ language: string,
714
+ config: PlaygroundShikiConfig,
715
+ ): Promise<string> {
716
+ const normalizedCode = String(code ?? "");
717
+ if (!config) return renderPlainCodeToHtml(normalizedCode);
718
+
719
+ try {
720
+ const highlighted = await highlightAssistantCodeToHtml({
721
+ code: normalizedCode,
722
+ config,
723
+ language,
724
+ });
725
+ return highlighted.html;
726
+ } catch (error) {
727
+ console.error("Playground code highlighting failed", {
728
+ error,
729
+ language,
730
+ });
731
+ return renderPlainCodeToHtml(normalizedCode);
732
+ }
721
733
  }
722
734
 
723
- :global(.dark pre code span.token.string) {
724
- color: #a5d6ff;
725
- }
726
- </style>
735
+ (
736
+ window as typeof window & {
737
+ RadiantPlaygroundHighlightCode?: typeof highlightPlaygroundCodeToHtml;
738
+ }
739
+ ).RadiantPlaygroundHighlightCode = highlightPlaygroundCodeToHtml;
740
+ </script>
@@ -161,7 +161,7 @@ import { Icon } from "astro-icon/components";
161
161
  </div>
162
162
  <div x-show="response && response.highlightedData">
163
163
  <pre
164
- class="relative m-0 min-w-full p-0 text-[13px] leading-6"><code data-rd-code-theme class="block min-w-full px-4 py-2.5 font-mono text-neutral-800 dark:text-neutral-200" x-html="response?.highlightedData" /></pre>
164
+ class="relative m-0 min-w-full 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" x-html="response?.highlightedData" /></pre>
165
165
  </div>
166
166
  </div>
167
167
 
@@ -185,7 +185,7 @@ import { Icon } from "astro-icon/components";
185
185
  </div>
186
186
  <div x-show="response && response.highlightedHeaders">
187
187
  <pre
188
- 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 px-4 py-2.5 font-mono text-neutral-800 dark:text-neutral-200" x-html="response?.highlightedHeaders" /></pre>
188
+ 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" x-html="response?.highlightedHeaders" /></pre>
189
189
  </div>
190
190
  </div>
191
191
  </div>
@@ -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>
@@ -190,7 +190,7 @@ class="rd-prose-block">
190
190
  >
191
191
  <div
192
192
  data-rd-tabs-panel-content
193
- class="prose-rules w-full max-w-none! *:max-w-none!"
193
+ class="prose-rules w-full max-w-none!"
194
194
  set:html={content}
195
195
  />
196
196
  </div>
@@ -0,0 +1,24 @@
1
+ {
2
+ "assetVersion": "shiki-4.2.0-c0d7e21cea71",
3
+ "counts": {
4
+ "languages": 347,
5
+ "themes": 66
6
+ },
7
+ "manifest": {
8
+ "bytes": 172635,
9
+ "module": "manifest.json",
10
+ "sha256": "1840b6f21a79266d9bf3a74d5e6973d407fb06b96d71decd5542ca37eebf182d"
11
+ },
12
+ "packageVersions": {
13
+ "@shikijs/core": "4.2.0",
14
+ "@shikijs/engine-javascript": "4.2.0",
15
+ "@shikijs/langs": "4.2.0",
16
+ "@shikijs/themes": "4.2.0",
17
+ "shiki": "4.2.0"
18
+ },
19
+ "prefix": "_platform/shiki",
20
+ "runtime": {
21
+ "engine": "javascript-regexp",
22
+ "module": "runtime.mjs"
23
+ }
24
+ }
@@ -4,6 +4,7 @@ import Sidebar from "../components/Sidebar.astro";
4
4
  import { getConfig } from "../lib/validation";
5
5
  import Header from "../components/Header.astro";
6
6
  import Footer from "../components/Footer.astro";
7
+ import NavigationTabs from "../components/NavigationTabs.astro";
7
8
  import AssistantDocsWidget from "../components/chat/AssistantDocsWidget.astro";
8
9
  import { ClientRouter } from "astro:transitions";
9
10
  import { prependBasePath, stripBasePath } from "../lib/base-path";
@@ -18,6 +19,8 @@ interface Props {
18
19
  pageDescription?: string;
19
20
  }
20
21
 
22
+ type TabsPresentation = "topbar" | "sidebar";
23
+
21
24
  function normalizeRoutePath(routePath: string): string {
22
25
  if (!routePath || routePath === "/") return "/";
23
26
  if (!routePath.startsWith("/")) routePath = `/${routePath}`;
@@ -66,6 +69,12 @@ const orgTier =
66
69
  Number.isFinite(parsedOrgTier) && parsedOrgTier > 0 ? parsedOrgTier : 1;
67
70
  const isDev = import.meta.env.DEV;
68
71
  const askAiEnabled = isDev || orgTier >= 3;
72
+ const navigationTabs = config.navigation.tabs as
73
+ | (typeof config.navigation.tabs & { presentation?: TabsPresentation })
74
+ | undefined;
75
+ const hasNavigationTabs =
76
+ Array.isArray(navigationTabs?.items) && navigationTabs.items.length > 0;
77
+ const tabsPresentation = navigationTabs?.presentation ?? "topbar";
69
78
  ---
70
79
 
71
80
  <!doctype html>
@@ -73,7 +82,7 @@ const askAiEnabled = isDev || orgTier >= 3;
73
82
  <head>
74
83
  <style is:inline is:global set:html={neutralPaletteCss}></style>
75
84
  <style is:inline is:global set:html={fontCss}></style>
76
- <ClientRouter />
85
+ <ClientRouter fallback="swap" />
77
86
  <script is:inline>
78
87
  const applyTheme = () => {
79
88
  const modeParam = new URLSearchParams(window.location.search).get(
@@ -192,6 +201,71 @@ const askAiEnabled = isDev || orgTier >= 3;
192
201
  }
193
202
  })();
194
203
  </script>
204
+ <script is:inline>
205
+ (() => {
206
+ const root = document.documentElement;
207
+ let viewportFrameId = null;
208
+
209
+ const syncVisualViewport = () => {
210
+ viewportFrameId = null;
211
+
212
+ const viewport = window.visualViewport;
213
+ const width =
214
+ viewport && Number.isFinite(viewport.width)
215
+ ? viewport.width
216
+ : window.innerWidth;
217
+ const height =
218
+ viewport && Number.isFinite(viewport.height)
219
+ ? viewport.height
220
+ : window.innerHeight;
221
+ const offsetLeft =
222
+ viewport && Number.isFinite(viewport.offsetLeft)
223
+ ? viewport.offsetLeft
224
+ : 0;
225
+ const offsetTop =
226
+ viewport && Number.isFinite(viewport.offsetTop)
227
+ ? viewport.offsetTop
228
+ : 0;
229
+
230
+ root.style.setProperty(
231
+ "--rd-visual-viewport-left",
232
+ `${Math.max(0, offsetLeft)}px`,
233
+ );
234
+ root.style.setProperty(
235
+ "--rd-visual-viewport-top",
236
+ `${Math.max(0, offsetTop)}px`,
237
+ );
238
+ root.style.setProperty(
239
+ "--rd-visual-viewport-width",
240
+ `${Math.max(1, width)}px`,
241
+ );
242
+ root.style.setProperty(
243
+ "--rd-visual-viewport-height",
244
+ `${Math.max(1, height)}px`,
245
+ );
246
+ };
247
+
248
+ const scheduleVisualViewportSync = () => {
249
+ if (viewportFrameId !== null) return;
250
+ viewportFrameId = window.requestAnimationFrame(syncVisualViewport);
251
+ };
252
+
253
+ scheduleVisualViewportSync();
254
+ window.visualViewport?.addEventListener(
255
+ "resize",
256
+ scheduleVisualViewportSync,
257
+ );
258
+ window.visualViewport?.addEventListener(
259
+ "scroll",
260
+ scheduleVisualViewportSync,
261
+ );
262
+ window.addEventListener("resize", scheduleVisualViewportSync);
263
+ window.addEventListener(
264
+ "orientationchange",
265
+ scheduleVisualViewportSync,
266
+ );
267
+ })();
268
+ </script>
195
269
  {
196
270
  fontPreloads.map((font) => (
197
271
  <link
@@ -233,12 +307,10 @@ const askAiEnabled = isDev || orgTier >= 3;
233
307
  <body
234
308
  class="bg-background text-neutral-900 dark:text-white"
235
309
  x-data="{ open: false }"
236
- x-bind:class="open ? 'overflow-hidden touch-none' : ''"
310
+ x-bind:class="open ? 'overflow-hidden' : ''"
237
311
  >
238
312
  <!-- Edges -->
239
- <div
240
- class="fixed top-1 inset-x-1 h-16 -z-10 bg-background-dark"
241
- >
313
+ <div class="fixed top-1 inset-x-1 h-16 -z-10 bg-background-dark">
242
314
  <div class="bg-background w-full h-full rounded-t-2xl"></div>
243
315
  </div>
244
316
  <div
@@ -252,10 +324,21 @@ const askAiEnabled = isDev || orgTier >= 3;
252
324
 
253
325
  <!-- Header -->
254
326
  <Header showAskAiTrigger={askAiEnabled} />
327
+ {
328
+ hasNavigationTabs ? (
329
+ <NavigationTabs
330
+ tabs={navigationTabs!}
331
+ presentation={tabsPresentation}
332
+ />
333
+ ) : null
334
+ }
255
335
 
256
336
  <!-- Desktop Sidebar -->
257
337
  <div
258
- class="bg-background w-[283px] ml-[5px] fixed inset-y-0 mt-17 hidden lg:block border-r border-r-border-light"
338
+ class:list={[
339
+ "bg-background w-[283px] ml-[5px] fixed inset-y-0 hidden lg:block border-r border-r-border-light",
340
+ hasNavigationTabs ? "mt-[112px]" : "mt-17",
341
+ ]}
259
342
  data-pagefind-ignore
260
343
  >
261
344
  <Sidebar />
@@ -265,7 +348,7 @@ const askAiEnabled = isDev || orgTier >= 3;
265
348
  <div
266
349
  x-show="open"
267
350
  x-cloak
268
- class="bg-background mx-[5px] min-h-[calc(100vh-68px)] mt-17 fixed inset-0 lg:hidden z-50 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
351
+ class="rd-mobile-menu bg-background fixed lg:hidden z-50 overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
269
352
  x-transition.opacity
270
353
  >
271
354
  <Sidebar askAiEnabled={askAiEnabled} />
@@ -275,11 +358,31 @@ const askAiEnabled = isDev || orgTier >= 3;
275
358
  <div
276
359
  class="mx-1 mt-1 px-4 sm:px-6 lg:pl-[calc(288px+32px)] pt-16 lg:pr-8 bg-background"
277
360
  >
278
- <main class="mx-auto pt-16 pb-16 min-h-[calc(100vh-64px)]">
361
+ <main class="mx-auto pt-16 pb-20 min-h-[calc(100vh-64px)]">
279
362
  <slot />
280
363
  </main>
281
364
  <Footer askAiEnabled={askAiEnabled} />
282
365
  </div>
283
366
  {askAiEnabled ? <AssistantDocsWidget /> : null}
367
+ <style is:global>
368
+ :root {
369
+ --rd-visual-viewport-left: 0px;
370
+ --rd-visual-viewport-top: 0px;
371
+ --rd-visual-viewport-width: 100vw;
372
+ --rd-visual-viewport-height: 100dvh;
373
+ }
374
+
375
+ .rd-mobile-menu {
376
+ --rd-mobile-menu-top-offset: 68px;
377
+ left: calc(var(--rd-visual-viewport-left) + 5px);
378
+ top: calc(
379
+ var(--rd-visual-viewport-top) + var(--rd-mobile-menu-top-offset)
380
+ );
381
+ width: calc(var(--rd-visual-viewport-width) - 10px);
382
+ height: calc(
383
+ var(--rd-visual-viewport-height) - var(--rd-mobile-menu-top-offset)
384
+ );
385
+ }
386
+ </style>
284
387
  </body>
285
388
  </html>