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
@@ -9,6 +9,7 @@ import SidebarOpenApi from "./sidebar/SidebarOpenApi.astro";
9
9
  import SidebarOpenApiPageLink from "./sidebar/SidebarOpenApiPageLink.astro";
10
10
  import SidebarDropdown from "./SidebarDropdown.astro";
11
11
  import SidebarSegmented from "./SidebarSegmented.astro";
12
+ import SidebarTabs from "./SidebarTabs.astro";
12
13
  import SidebarGroup from "./SidebarGroup.astro";
13
14
  import SidebarLink from "./SidebarLink.astro";
14
15
 
@@ -58,5 +59,7 @@ let { navigation, parentSlug = "" } = Astro.props;
58
59
  ) : null
59
60
  ) : navigation.openapi ? (
60
61
  <SidebarOpenApi openapi={navigation.openapi} parentSlug={parentSlug} />
62
+ ) : navigation.tabs ? (
63
+ <SidebarTabs tabs={navigation.tabs} parentSlug={parentSlug} />
61
64
  ) : null
62
65
  }
@@ -16,9 +16,15 @@ const { menu, parentSlug = "" } = Astro.props;
16
16
  const pathname = stripBasePath(Astro.url.pathname);
17
17
  const config = await getConfig();
18
18
  const routes = (await getAllRoutes()).filter((route) => !route.hidden);
19
+ const normalizedPathParts = pathname
20
+ .replace(/^\/+/, "")
21
+ .replace(/\/+$/, "")
22
+ .split("/")
23
+ .filter(Boolean);
24
+ const parentParts = parentSlug.split("/").filter(Boolean);
19
25
 
20
26
  let currentMenuIndex = menu.items.findIndex(
21
- (i) => slugify(i.label) === pathname.split("/")[1],
27
+ (i) => slugify(i.label) === normalizedPathParts[parentParts.length],
22
28
  );
23
29
 
24
30
  if (pathname === "/" && config.home) {
@@ -27,9 +33,10 @@ if (pathname === "/" && config.home) {
27
33
  );
28
34
 
29
35
  if (homeRoute) {
30
- const homeTopLevelSegment = homeRoute.slug.split("/")[0];
36
+ const homeRouteParts = homeRoute.slug.split("/").filter(Boolean);
37
+ const homeMenuSegment = homeRouteParts[parentParts.length];
31
38
  const homeMenuIndex = menu.items.findIndex(
32
- (item) => slugify(item.label) === homeTopLevelSegment,
39
+ (item) => slugify(item.label) === homeMenuSegment,
33
40
  );
34
41
  if (homeMenuIndex !== -1) {
35
42
  currentMenuIndex = homeMenuIndex;
@@ -39,62 +46,90 @@ if (pathname === "/" && config.home) {
39
46
 
40
47
  currentMenuIndex = currentMenuIndex === -1 ? 0 : currentMenuIndex;
41
48
 
42
- let firstHrefOfMenuItems: string[] = [];
43
-
44
- const seen = new Set();
45
-
46
- for (const route of routes) {
47
- const slug = route.slug;
49
+ function getMenuItemPrefix(label: string): string {
50
+ const menuSlug = slugify(label);
51
+ return parentSlug ? `${parentSlug}/${menuSlug}` : menuSlug;
52
+ }
48
53
 
49
- const parts = slug.split("/");
50
- const topLevel = parts[0];
54
+ let firstHrefOfMenuItems = menu.items.map((item) => {
55
+ const itemPrefix = getMenuItemPrefix(item.label);
56
+ const firstRoute = routes.find(
57
+ (route) => route.slug === itemPrefix || route.slug.startsWith(`${itemPrefix}/`),
58
+ );
51
59
 
52
- if (!seen.has(topLevel)) {
53
- seen.add(topLevel);
54
- const href =
55
- route.type === "mdx" && config.home && route.filePath === config.home
56
- ? prependBasePath("/")
57
- : prependBasePath(`/${slug}`);
58
- firstHrefOfMenuItems.push(href);
60
+ if (firstRoute) {
61
+ return firstRoute.type === "mdx" &&
62
+ config.home &&
63
+ firstRoute.filePath === config.home
64
+ ? prependBasePath("/")
65
+ : prependBasePath(`/${firstRoute.slug}`);
59
66
  }
60
- }
67
+
68
+ return prependBasePath("/");
69
+ });
61
70
 
62
71
  const selectedMenuItem = menu.items[currentMenuIndex];
63
- const menuItemSlug = slugify(selectedMenuItem.label);
64
- const currentPrefix = parentSlug
65
- ? `${parentSlug}/${menuItemSlug}`
66
- : menuItemSlug;
72
+ const currentPrefix = getMenuItemPrefix(selectedMenuItem.label);
73
+
74
+ const controlBaseClass =
75
+ "items-center px-3 py-1.5 cursor-pointer text-sm rounded-md border-x border-t transition-colors duration-150";
76
+ const activeControlClass =
77
+ "bg-white dark:bg-neutral-700/30 border-neutral-200/70 dark:border-neutral-700/40 dark:border-b shadow-sm shadow-neutral-200/80 dark:shadow-neutral-950/20 text-neutral-900 dark:text-white";
78
+ const inactiveControlClass =
79
+ "text-neutral-600 dark:text-neutral-300 border-transparent hover:text-primary dark:hover:text-primary";
67
80
  ---
68
81
 
69
- <div class="mt-3 mx-2 z-10 relative dark:bg-neutral-900 rounded-lg">
70
- {
71
- menu.label && (
72
- <label class="font-semibold text-xs px-2 pb-1 block">{menu.label}</label>
73
- )
74
- }
75
- <ul class="rounded-lg bg-neutral-100 dark:bg-neutral-800/60 p-[3px]">
82
+ <div
83
+ x-data={`{ selectedMenuIndex: ${currentMenuIndex} }`}
84
+ x-init={`$watch('open', (value) => { if (value) selectedMenuIndex = ${currentMenuIndex} })`}
85
+ >
86
+ <div class="mt-3 mx-2 z-10 relative dark:bg-neutral-900 rounded-lg">
76
87
  {
77
- menu.items.map((item, index) => (
78
- <li>
79
- <a
80
- class="flex items-center px-3 py-1.5 cursor-pointer text-sm rounded-md border-x border-t transition-colors duration-150"
81
- class:list={[
82
- index === currentMenuIndex
83
- ? "bg-white dark:bg-neutral-700/30 border-neutral-200/70 dark:border-neutral-700/40 dark:border-b shadow-sm shadow-neutral-200/80 dark:shadow-neutral-950/20 text-neutral-900 dark:text-white"
84
- : "text-neutral-600 dark:text-neutral-300 border-transparent hover:text-primary dark:hover:text-primary",
85
- ]}
86
- href={firstHrefOfMenuItems[index] ?? prependBasePath("/")}
87
- >
88
- {item.icon && <Icon class="mr-2 size-4" name={item.icon} />}
89
- {item.label}
90
- </a>
91
- </li>
92
- ))
88
+ menu.label && (
89
+ <label class="font-semibold text-xs px-2 pb-1 block">{menu.label}</label>
90
+ )
93
91
  }
94
- </ul>
95
- </div>
92
+ <ul class="rounded-lg bg-neutral-100 dark:bg-neutral-800/60 p-[3px]">
93
+ {
94
+ menu.items.map((item, index) => (
95
+ <li>
96
+ <a
97
+ class:list={[
98
+ controlBaseClass,
99
+ "hidden lg:flex",
100
+ index === currentMenuIndex
101
+ ? activeControlClass
102
+ : inactiveControlClass,
103
+ ]}
104
+ href={firstHrefOfMenuItems[index] ?? prependBasePath("/")}
105
+ >
106
+ {item.icon && <Icon class="mr-2 size-4" name={item.icon} />}
107
+ {item.label}
108
+ </a>
109
+ <button
110
+ type="button"
111
+ class:list={[controlBaseClass, "flex lg:hidden w-full"]}
112
+ x-bind:class={`selectedMenuIndex === ${index} ? '${activeControlClass}' : '${inactiveControlClass}'`}
113
+ x-bind:aria-selected={`selectedMenuIndex === ${index}`}
114
+ x-on:click={`selectedMenuIndex = ${index}`}
115
+ >
116
+ {item.icon && <Icon class="mr-2 size-4" name={item.icon} />}
117
+ {item.label}
118
+ </button>
119
+ </li>
120
+ ))
121
+ }
122
+ </ul>
123
+ </div>
96
124
 
97
- <SidebarMenu
98
- navigation={menu.items[currentMenuIndex].submenu}
99
- parentSlug={currentPrefix}
100
- />
125
+ <div class="hidden lg:block">
126
+ <SidebarMenu navigation={selectedMenuItem} parentSlug={currentPrefix} />
127
+ </div>
128
+ <div class="lg:hidden">
129
+ {menu.items.map((item, index) => (
130
+ <div x-show={`selectedMenuIndex === ${index}`} x-cloak>
131
+ <SidebarMenu navigation={item} parentSlug={getMenuItemPrefix(item.label)} />
132
+ </div>
133
+ ))}
134
+ </div>
135
+ </div>
@@ -0,0 +1,86 @@
1
+ ---
2
+ import { getConfig, type NavTabs } from "../lib/validation";
3
+ import SidebarMenu from "./SidebarMenu.astro";
4
+ import { slugify } from "../lib/utils";
5
+ import { stripBasePath } from "../lib/base-path";
6
+ import { getAllRoutes } from "../lib/routes";
7
+ import NavigationTabList from "./NavigationTabList.astro";
8
+
9
+ interface Props {
10
+ tabs: NavTabs;
11
+ parentSlug?: string;
12
+ }
13
+
14
+ const { tabs, parentSlug = "" } = Astro.props;
15
+ const tabItems = tabs.items ?? [];
16
+ const pathname = stripBasePath(Astro.url.pathname);
17
+ const config = await getConfig();
18
+ const routes = (await getAllRoutes()).filter((route) => !route.hidden);
19
+ const normalizedPathParts = pathname
20
+ .replace(/^\/+/, "")
21
+ .replace(/\/+$/, "")
22
+ .split("/")
23
+ .filter(Boolean);
24
+ const parentParts = parentSlug.split("/").filter(Boolean);
25
+
26
+ function getTabSlug(tab: (typeof tabItems)[number]): string {
27
+ return slugify(tab.slug || tab.label);
28
+ }
29
+
30
+ function getTabPrefix(tab: (typeof tabItems)[number]): string {
31
+ const tabSlug = getTabSlug(tab);
32
+ return parentSlug ? `${parentSlug}/${tabSlug}` : tabSlug;
33
+ }
34
+
35
+ let currentTabIndex = tabItems.findIndex(
36
+ (item) => getTabSlug(item) === normalizedPathParts[parentParts.length],
37
+ );
38
+
39
+ if (pathname === "/" && config.home) {
40
+ const homeRoute = routes.find(
41
+ (route) => route.type === "mdx" && route.filePath === config.home,
42
+ );
43
+
44
+ if (homeRoute) {
45
+ const homeRouteParts = homeRoute.slug.split("/").filter(Boolean);
46
+ const homeTabSegment = homeRouteParts[parentParts.length];
47
+ const homeTabIndex = tabItems.findIndex(
48
+ (item) => getTabSlug(item) === homeTabSegment,
49
+ );
50
+ if (homeTabIndex !== -1) {
51
+ currentTabIndex = homeTabIndex;
52
+ }
53
+ }
54
+ }
55
+
56
+ currentTabIndex = currentTabIndex === -1 ? 0 : currentTabIndex;
57
+
58
+ const selectedTabItem = tabItems[currentTabIndex];
59
+ ---
60
+
61
+ {
62
+ selectedTabItem ? (
63
+ <div
64
+ x-data={`{ selectedTabIndex: ${currentTabIndex} }`}
65
+ x-init={`$watch('open', (value) => { if (value) selectedTabIndex = ${currentTabIndex} })`}
66
+ >
67
+ <div class="lg:hidden h-11 border-b border-border-light bg-background">
68
+ <nav
69
+ class="h-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
70
+ aria-label="Documentation sections"
71
+ >
72
+ <NavigationTabList
73
+ items={tabItems}
74
+ currentIndex={currentTabIndex}
75
+ mode="button"
76
+ />
77
+ </nav>
78
+ </div>
79
+ {tabItems.map((item, index) => (
80
+ <div x-show={`selectedTabIndex === ${index}`} x-cloak>
81
+ <SidebarMenu navigation={item} parentSlug={getTabPrefix(item)} />
82
+ </div>
83
+ ))}
84
+ </div>
85
+ ) : null
86
+ }
@@ -1,11 +1,12 @@
1
1
  import type { JSX } from "preact";
2
2
  import { navigate } from "astro:transitions/client";
3
- import { useEffect, useRef, useState } from "preact/hooks";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
4
4
  import AssistantEmbedPanel, {
5
5
  ASSISTANT_PANEL_STORAGE_KEY,
6
6
  AssistantPanelIcon,
7
7
  type AssistantPanelSize,
8
8
  } from "./AssistantEmbedPanel";
9
+ import type { AssistantShikiRuntimeConfig } from "../../lib/assistant-shiki-client";
9
10
  import {
10
11
  DEFAULT_ASSISTANT_CHROME_CONFIG,
11
12
  type AssistantChromeConfig,
@@ -31,6 +32,7 @@ type AssistantDocsWidgetProps = {
31
32
  emptyStateQuestions?: string[];
32
33
  devProxyToken?: string;
33
34
  chrome?: AssistantChromeConfig;
35
+ shiki?: AssistantShikiRuntimeConfig;
34
36
  };
35
37
 
36
38
  type AssistantOpenRequestWindow = Window & {
@@ -39,6 +41,20 @@ type AssistantOpenRequestWindow = Window & {
39
41
  __assistantDocsLauncherEntered?: boolean;
40
42
  };
41
43
 
44
+ type PageScrollLockState = {
45
+ scrollX: number;
46
+ scrollY: number;
47
+ htmlOverflow: string;
48
+ htmlOverscrollBehavior: string;
49
+ bodyOverflow: string;
50
+ bodyOverscrollBehavior: string;
51
+ bodyPosition: string;
52
+ bodyTop: string;
53
+ bodyLeft: string;
54
+ bodyRight: string;
55
+ bodyWidth: string;
56
+ };
57
+
42
58
  function normalizePanelSize(value: unknown): AssistantPanelSize {
43
59
  return value === "expanded" ? "expanded" : "default";
44
60
  }
@@ -75,6 +91,7 @@ export default function AssistantDocsWidget({
75
91
  emptyStateQuestions,
76
92
  devProxyToken,
77
93
  chrome,
94
+ shiki,
78
95
  }: AssistantDocsWidgetProps) {
79
96
  const [isOpen, setIsOpen] = useState(false);
80
97
  const [panelSize, setPanelSize] = useState<AssistantPanelSize>(() =>
@@ -99,6 +116,7 @@ export default function AssistantDocsWidget({
99
116
  const navigationTransitionCleanupRef = useRef<(() => void) | null>(null);
100
117
  const navigationTransitionTimeoutRef = useRef<number | null>(null);
101
118
  const panelSizeTransitionTimeoutRef = useRef<number | null>(null);
119
+ const pageScrollLockRef = useRef<PageScrollLockState | null>(null);
102
120
  const chromeConfig = chrome ?? DEFAULT_ASSISTANT_CHROME_CONFIG;
103
121
  const numericZIndex =
104
122
  Number.parseInt(chromeConfig.zIndex, 10) ||
@@ -159,6 +177,13 @@ export default function AssistantDocsWidget({
159
177
  };
160
178
 
161
179
  const closePanel = () => {
180
+ if (typeof document !== "undefined") {
181
+ const activeElement = document.activeElement;
182
+ if (activeElement instanceof HTMLElement) {
183
+ activeElement.blur();
184
+ }
185
+ }
186
+
162
187
  setIsOpen(false);
163
188
  };
164
189
 
@@ -267,6 +292,96 @@ export default function AssistantDocsWidget({
267
292
  };
268
293
  }, []);
269
294
 
295
+ useLayoutEffect(() => {
296
+ if (!isOpen || typeof window === "undefined") {
297
+ return;
298
+ }
299
+
300
+ const unlockPageScroll = () => {
301
+ const lockState = pageScrollLockRef.current;
302
+ if (!lockState) {
303
+ return;
304
+ }
305
+
306
+ document.documentElement.style.overflow = lockState.htmlOverflow;
307
+ document.documentElement.style.overscrollBehavior =
308
+ lockState.htmlOverscrollBehavior;
309
+ document.body.style.overflow = lockState.bodyOverflow;
310
+ document.body.style.overscrollBehavior =
311
+ lockState.bodyOverscrollBehavior;
312
+ document.body.style.position = lockState.bodyPosition;
313
+ document.body.style.top = lockState.bodyTop;
314
+ document.body.style.left = lockState.bodyLeft;
315
+ document.body.style.right = lockState.bodyRight;
316
+ document.body.style.width = lockState.bodyWidth;
317
+ pageScrollLockRef.current = null;
318
+
319
+ window.scrollTo(lockState.scrollX, lockState.scrollY);
320
+ };
321
+
322
+ const lockPageScroll = () => {
323
+ if (pageScrollLockRef.current) {
324
+ return;
325
+ }
326
+
327
+ const html = document.documentElement;
328
+ const body = document.body;
329
+ const scrollX = window.scrollX;
330
+ const scrollY = window.scrollY;
331
+ pageScrollLockRef.current = {
332
+ scrollX,
333
+ scrollY,
334
+ htmlOverflow: html.style.overflow,
335
+ htmlOverscrollBehavior: html.style.overscrollBehavior,
336
+ bodyOverflow: body.style.overflow,
337
+ bodyOverscrollBehavior: body.style.overscrollBehavior,
338
+ bodyPosition: body.style.position,
339
+ bodyTop: body.style.top,
340
+ bodyLeft: body.style.left,
341
+ bodyRight: body.style.right,
342
+ bodyWidth: body.style.width,
343
+ };
344
+
345
+ html.style.overflow = "hidden";
346
+ html.style.overscrollBehavior = "none";
347
+ body.style.overflow = "hidden";
348
+ body.style.overscrollBehavior = "none";
349
+ body.style.position = "fixed";
350
+ body.style.top = `-${scrollY}px`;
351
+ body.style.left = "0";
352
+ body.style.right = "0";
353
+ body.style.width = "100%";
354
+ };
355
+
356
+ const fullscreenPanelQuery = window.matchMedia(
357
+ `(max-width: ${chromeConfig.mobileBreakpoint}), (max-height: ${chromeConfig.panelFullscreenHeightBreakpoint})`,
358
+ );
359
+ const syncPageScrollLock = () => {
360
+ if (fullscreenPanelQuery.matches) {
361
+ lockPageScroll();
362
+ return;
363
+ }
364
+
365
+ unlockPageScroll();
366
+ };
367
+
368
+ syncPageScrollLock();
369
+ fullscreenPanelQuery.addEventListener("change", syncPageScrollLock);
370
+ window.addEventListener("resize", syncPageScrollLock);
371
+ window.addEventListener("orientationchange", syncPageScrollLock);
372
+
373
+ return () => {
374
+ fullscreenPanelQuery.removeEventListener("change", syncPageScrollLock);
375
+ window.removeEventListener("resize", syncPageScrollLock);
376
+ window.removeEventListener("orientationchange", syncPageScrollLock);
377
+ unlockPageScroll();
378
+ };
379
+ }, [
380
+ chromeConfig.mobileBreakpoint,
381
+ chromeConfig.panelFullscreenHeightBreakpoint,
382
+ isOpen,
383
+ ]);
384
+
270
385
  useEffect(() => {
271
386
  return () => {
272
387
  navigationTransitionCleanupRef.current?.();
@@ -320,10 +435,15 @@ export default function AssistantDocsWidget({
320
435
  emptyStateHeading={emptyStateHeading}
321
436
  emptyStateQuestions={emptyStateQuestions}
322
437
  devProxyToken={devProxyToken}
438
+ shiki={shiki}
323
439
  panelSurface="inline"
324
440
  linkTarget="current"
325
441
  allowApiPathQueryOverride={false}
326
442
  openSignal={openSignal}
443
+ mobileBreakpoint={chromeConfig.mobileBreakpoint}
444
+ panelFullscreenHeightBreakpoint={
445
+ chromeConfig.panelFullscreenHeightBreakpoint
446
+ }
327
447
  onRequestOpen={openPanel}
328
448
  onRequestClose={closePanel}
329
449
  onRequestPanelSizeToggle={handlePanelSizeChange}
@@ -571,12 +691,17 @@ export default function AssistantDocsWidget({
571
691
 
572
692
  @media (max-width: ${chromeConfig.mobileBreakpoint}), (max-height: ${chromeConfig.panelFullscreenHeightBreakpoint}) {
573
693
  .assistant-docs-panel-shell {
574
- inset: 0 !important;
694
+ top: 0 !important;
695
+ right: auto !important;
696
+ bottom: auto !important;
697
+ left: 0 !important;
575
698
  z-index: var(--assistant-docs-mobile-panel-z-index) !important;
576
699
  width: 100vw !important;
577
700
  height: 100dvh !important;
578
701
  border: 0 !important;
579
702
  border-radius: 0 !important;
703
+ transition: none !important;
704
+ transition-behavior: normal !important;
580
705
  }
581
706
 
582
707
  .assistant-docs-panel-shell > div {