radiant-docs 0.1.61 → 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 (30) 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 +17 -1
  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 +269 -283
  17. package/template/src/components/user/Accordion.astro +1 -1
  18. package/template/src/components/user/Callout.astro +2 -2
  19. package/template/src/components/user/CodeBlock.astro +58 -7
  20. package/template/src/components/user/CodeGroup.astro +52 -1
  21. package/template/src/components/user/Column.astro +1 -1
  22. package/template/src/components/user/Step.astro +1 -1
  23. package/template/src/components/user/Tabs.astro +1 -1
  24. package/template/src/generated/shiki-platform-assets.json +24 -0
  25. package/template/src/layouts/Layout.astro +111 -8
  26. package/template/src/lib/assistant-panel-config.ts +59 -0
  27. package/template/src/lib/assistant-shiki-client.ts +506 -0
  28. package/template/src/lib/mdx/remark-resolve-internal-links.ts +334 -17
  29. package/template/src/lib/routes.ts +66 -24
  30. package/template/src/styles/global.css +12 -0
@@ -9,10 +9,12 @@ import {
9
9
  resolveDocsHref,
10
10
  type DocsConfig,
11
11
  type NavGroup,
12
+ type NavMenu,
12
13
  type NavMenuItem,
13
14
  type NavOpenApi,
14
15
  type NavOpenApiPage,
15
16
  type NavPage,
17
+ type NavTabItem,
16
18
  } from "../validation";
17
19
  import { prependBasePath, withBasePath } from "../base-path";
18
20
  import {
@@ -26,6 +28,11 @@ import {
26
28
  } from "../utils";
27
29
 
28
30
  type MdxNavItem = string | NavPage | NavGroup | NavOpenApiPage;
31
+ type NavigationContentContainer = {
32
+ pages?: MdxNavItem[];
33
+ menu?: NavMenu;
34
+ openapi?: string | NavOpenApi;
35
+ };
29
36
 
30
37
  type AuxiliaryPageRef = {
31
38
  filePath: string;
@@ -36,9 +43,24 @@ type DocsConfigWithAuxiliaryRefs = DocsConfig & {
36
43
  auxiliaryPageRefs?: AuxiliaryPageRef[];
37
44
  };
38
45
 
46
+ type OpenApiEndpointLinkRef = {
47
+ source?: string;
48
+ method: string;
49
+ path: string;
50
+ endpoint: string;
51
+ };
52
+
53
+ type OpenApiEndpointRouteTarget = OpenApiEndpointLinkRef & {
54
+ source: string;
55
+ targetKey: string;
56
+ };
57
+
39
58
  type ResolvedRouteIndex = {
40
59
  canonicalHrefByFilePath: Map<string, string>;
41
60
  allHrefsByFilePath: Map<string, string[]>;
61
+ canonicalHrefByOpenApiEndpointKey: Map<string, string>;
62
+ allHrefsByOpenApiEndpointKey: Map<string, string[]>;
63
+ openApiEndpointTargetsByEndpoint: Map<string, OpenApiEndpointRouteTarget[]>;
42
64
  validRoutePaths: Set<string>;
43
65
  allDocsFilePaths: Set<string>;
44
66
  routeSlugs: ReturnType<typeof createRouteSlugRegistry>;
@@ -76,10 +98,24 @@ const HTTP_METHODS = [
76
98
  "options",
77
99
  "trace",
78
100
  ];
101
+ const HTTP_METHOD_SET = new Set<string>(HTTP_METHODS);
79
102
 
80
103
  let cachedRouteIndex: ResolvedRouteIndex | null = null;
81
104
  let cachedConfig: DocsConfig | null = null;
82
105
 
106
+ function isUrl(value: string): boolean {
107
+ try {
108
+ const url = new URL(value);
109
+ return url.protocol === "http:" || url.protocol === "https:";
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ function containsPathTraversal(value: string): boolean {
116
+ return value.replace(/\\/g, "/").split("/").includes("..");
117
+ }
118
+
83
119
  function normalizeDocsFilePath(value: string): string {
84
120
  let normalized = value.replace(/\\/g, "/").trim();
85
121
  if (!normalized) return "";
@@ -94,6 +130,102 @@ function normalizeDocsFilePath(value: string): string {
94
130
  return posixNormalized === "." ? "" : posixNormalized;
95
131
  }
96
132
 
133
+ function normalizeOpenApiLinkSource(source: string): string {
134
+ const trimmedSource = source.trim();
135
+ if (isUrl(trimmedSource)) {
136
+ return trimmedSource;
137
+ }
138
+
139
+ let normalizedSource = trimmedSource.replace(/\\/g, "/").trim();
140
+ normalizedSource = normalizedSource.replace(/^\/+/, "").replace(/\/+$/, "");
141
+ normalizedSource = normalizedSource.replace(/^src\/content\/docs\//, "");
142
+
143
+ const posixNormalized = path.posix
144
+ .normalize(`/${normalizedSource}`)
145
+ .replace(/^\/+/, "");
146
+
147
+ return posixNormalized === "." ? "" : posixNormalized;
148
+ }
149
+
150
+ function formatOpenApiEndpoint(
151
+ method: string,
152
+ endpointPath: string,
153
+ ): string {
154
+ return `${method.toUpperCase()} ${endpointPath.toLowerCase()}`;
155
+ }
156
+
157
+ function formatOpenApiEndpointTargetKey(
158
+ source: string,
159
+ method: string,
160
+ endpointPath: string,
161
+ ): string {
162
+ return `${normalizeOpenApiLinkSource(source)}\0${formatOpenApiEndpoint(
163
+ method,
164
+ endpointPath,
165
+ )}`;
166
+ }
167
+
168
+ function parseOpenApiEndpointLinkEndpoint(
169
+ endpointStr: string,
170
+ ): Pick<OpenApiEndpointLinkRef, "method" | "path" | "endpoint"> | null {
171
+ const trimmed = endpointStr.trim();
172
+ const parts = trimmed.split(/\s+/);
173
+
174
+ if (parts.length !== 2) {
175
+ return null;
176
+ }
177
+
178
+ const method = parts[0].toUpperCase();
179
+ const endpointPath = parts[1];
180
+ if (
181
+ !HTTP_METHOD_SET.has(method.toLowerCase()) ||
182
+ !endpointPath.startsWith("/")
183
+ ) {
184
+ return null;
185
+ }
186
+
187
+ const normalizedPath = endpointPath.toLowerCase();
188
+ return {
189
+ method,
190
+ path: normalizedPath,
191
+ endpoint: formatOpenApiEndpoint(method, normalizedPath),
192
+ };
193
+ }
194
+
195
+ function parseOpenApiEndpointLinkHref(
196
+ href: string,
197
+ ): OpenApiEndpointLinkRef | null {
198
+ const trimmedHref = href.trim();
199
+ if (!trimmedHref) return null;
200
+
201
+ const hashIndex = trimmedHref.indexOf("#");
202
+ if (hashIndex > 0) {
203
+ const rawSource = trimmedHref.slice(0, hashIndex).trim();
204
+ if (!isUrl(rawSource) && containsPathTraversal(rawSource)) {
205
+ return null;
206
+ }
207
+
208
+ const source = normalizeOpenApiLinkSource(rawSource);
209
+ if (!source) {
210
+ return null;
211
+ }
212
+
213
+ const parsedEndpoint = parseOpenApiEndpointLinkEndpoint(
214
+ trimmedHref.slice(hashIndex + 1),
215
+ );
216
+ if (!parsedEndpoint) {
217
+ return null;
218
+ }
219
+
220
+ return {
221
+ source,
222
+ ...parsedEndpoint,
223
+ };
224
+ }
225
+
226
+ return parseOpenApiEndpointLinkEndpoint(trimmedHref);
227
+ }
228
+
97
229
  function normalizeRoutePath(value: string): string {
98
230
  const trimmed = value.trim();
99
231
  if (!trimmed || trimmed === "/") {
@@ -208,6 +340,38 @@ function addMdxRoute(args: {
208
340
  args.index.allHrefsByFilePath.set(normalizedFilePath, aliases);
209
341
  }
210
342
 
343
+ function createOpenApiEndpointRouteTarget(args: {
344
+ source: string;
345
+ method: string;
346
+ endpointPath: string;
347
+ }): OpenApiEndpointRouteTarget {
348
+ const source = normalizeOpenApiLinkSource(args.source);
349
+ const endpoint = formatOpenApiEndpoint(args.method, args.endpointPath);
350
+
351
+ return {
352
+ source,
353
+ method: args.method.toUpperCase(),
354
+ path: args.endpointPath.toLowerCase(),
355
+ endpoint,
356
+ targetKey: formatOpenApiEndpointTargetKey(
357
+ source,
358
+ args.method,
359
+ args.endpointPath,
360
+ ),
361
+ };
362
+ }
363
+
364
+ function addOpenApiEndpointTargetToMap(
365
+ map: Map<string, OpenApiEndpointRouteTarget[]>,
366
+ target: OpenApiEndpointRouteTarget,
367
+ ): void {
368
+ const targets = map.get(target.endpoint) ?? [];
369
+ if (!targets.some((candidate) => candidate.targetKey === target.targetKey)) {
370
+ targets.push(target);
371
+ }
372
+ map.set(target.endpoint, targets);
373
+ }
374
+
211
375
  function addOpenApiEndpointRoute(args: {
212
376
  index: ResolvedRouteIndex;
213
377
  parentSlug: string;
@@ -232,10 +396,30 @@ function addOpenApiEndpointRoute(args: {
232
396
  }),
233
397
  identity: `openapi:${args.source}:${args.method.toUpperCase()} ${args.endpointPath.toLowerCase()}`,
234
398
  }).slug;
235
- addValidRoutePath(
399
+ const href = addValidRoutePath(
236
400
  args.index,
237
401
  prependBasePath(`/${registeredSlug}`),
238
402
  );
403
+ const target = createOpenApiEndpointRouteTarget({
404
+ source: args.source,
405
+ method: args.method,
406
+ endpointPath: args.endpointPath,
407
+ });
408
+
409
+ if (!args.index.canonicalHrefByOpenApiEndpointKey.has(target.targetKey)) {
410
+ args.index.canonicalHrefByOpenApiEndpointKey.set(target.targetKey, href);
411
+ }
412
+
413
+ const aliases =
414
+ args.index.allHrefsByOpenApiEndpointKey.get(target.targetKey) ?? [];
415
+ if (!aliases.includes(href)) {
416
+ aliases.push(href);
417
+ }
418
+ args.index.allHrefsByOpenApiEndpointKey.set(target.targetKey, aliases);
419
+ addOpenApiEndpointTargetToMap(
420
+ args.index.openApiEndpointTargetsByEndpoint,
421
+ target,
422
+ );
239
423
  }
240
424
 
241
425
  function addAuxiliaryPageRef(
@@ -414,27 +598,69 @@ async function processMenuItems(args: {
414
598
  index: ResolvedRouteIndex;
415
599
  homePath: string | null;
416
600
  items: NavMenuItem[];
601
+ parentSlug?: string;
417
602
  }): Promise<void> {
418
603
  for (const menuItem of args.items) {
419
604
  const menuSlug = slugify(menuItem.label);
420
- const parentSlug = menuSlug;
605
+ const parentSlug = args.parentSlug
606
+ ? `${args.parentSlug}/${menuSlug}`
607
+ : menuSlug;
608
+
609
+ await processNavigationContent({
610
+ index: args.index,
611
+ parentSlug,
612
+ homePath: args.homePath,
613
+ item: menuItem,
614
+ });
615
+ }
616
+ }
421
617
 
422
- if (Array.isArray(menuItem.submenu.pages)) {
423
- await processPages({
424
- index: args.index,
425
- parentSlug,
426
- homePath: args.homePath,
427
- items: menuItem.submenu.pages as MdxNavItem[],
428
- });
429
- }
618
+ async function processTabItems(args: {
619
+ index: ResolvedRouteIndex;
620
+ homePath: string | null;
621
+ items: NavTabItem[];
622
+ }): Promise<void> {
623
+ for (const tabItem of args.items) {
624
+ const tabSlug = slugify(tabItem.slug || tabItem.label);
625
+ await processNavigationContent({
626
+ index: args.index,
627
+ parentSlug: tabSlug,
628
+ homePath: args.homePath,
629
+ item: tabItem,
630
+ });
631
+ }
632
+ }
430
633
 
431
- if (menuItem.submenu.openapi) {
432
- await processOpenApiFile({
433
- index: args.index,
434
- parentSlug,
435
- openApiPathOrConfig: menuItem.submenu.openapi,
436
- });
437
- }
634
+ async function processNavigationContent(args: {
635
+ index: ResolvedRouteIndex;
636
+ parentSlug: string;
637
+ homePath: string | null;
638
+ item: NavigationContentContainer;
639
+ }): Promise<void> {
640
+ if (Array.isArray(args.item.pages)) {
641
+ await processPages({
642
+ index: args.index,
643
+ parentSlug: args.parentSlug,
644
+ homePath: args.homePath,
645
+ items: args.item.pages as MdxNavItem[],
646
+ });
647
+ }
648
+
649
+ if (args.item.menu?.items) {
650
+ await processMenuItems({
651
+ index: args.index,
652
+ parentSlug: args.parentSlug,
653
+ homePath: args.homePath,
654
+ items: args.item.menu.items,
655
+ });
656
+ }
657
+
658
+ if (args.item.openapi) {
659
+ await processOpenApiFile({
660
+ index: args.index,
661
+ parentSlug: args.parentSlug,
662
+ openApiPathOrConfig: args.item.openapi,
663
+ });
438
664
  }
439
665
  }
440
666
 
@@ -444,6 +670,12 @@ async function buildRouteIndex(
444
670
  const index: ResolvedRouteIndex = {
445
671
  canonicalHrefByFilePath: new Map<string, string>(),
446
672
  allHrefsByFilePath: new Map<string, string[]>(),
673
+ canonicalHrefByOpenApiEndpointKey: new Map<string, string>(),
674
+ allHrefsByOpenApiEndpointKey: new Map<string, string[]>(),
675
+ openApiEndpointTargetsByEndpoint: new Map<
676
+ string,
677
+ OpenApiEndpointRouteTarget[]
678
+ >(),
447
679
  validRoutePaths: new Set<string>(),
448
680
  allDocsFilePaths: new Set<string>(),
449
681
  routeSlugs: createRouteSlugRegistry(),
@@ -473,6 +705,14 @@ async function buildRouteIndex(
473
705
  });
474
706
  }
475
707
 
708
+ if (config.navigation.tabs?.items) {
709
+ await processTabItems({
710
+ index,
711
+ homePath,
712
+ items: config.navigation.tabs.items,
713
+ });
714
+ }
715
+
476
716
  const rootOpenApi = (config.navigation as any).openapi;
477
717
  if (
478
718
  typeof rootOpenApi === "string" ||
@@ -527,12 +767,89 @@ function buildInternalLinkRewriteError(args: {
527
767
  );
528
768
  }
529
769
 
770
+ function formatOpenApiEndpointSourceList(
771
+ targets: OpenApiEndpointRouteTarget[],
772
+ ): string {
773
+ const sources = [...new Set(targets.map((target) => target.source))];
774
+ return sources.map((source) => `"${source}"`).join(", ");
775
+ }
776
+
777
+ function resolveOpenApiEndpointLinkToCanonicalHref(args: {
778
+ rawHref: string;
779
+ endpointLink: OpenApiEndpointLinkRef;
780
+ currentDocFilePath: string | null;
781
+ routeIndex: ResolvedRouteIndex;
782
+ warn: (message: string) => void;
783
+ }): string {
784
+ const target =
785
+ args.endpointLink.source !== undefined
786
+ ? {
787
+ source: args.endpointLink.source,
788
+ targetKey: formatOpenApiEndpointTargetKey(
789
+ args.endpointLink.source,
790
+ args.endpointLink.method,
791
+ args.endpointLink.path,
792
+ ),
793
+ }
794
+ : (() => {
795
+ const targets =
796
+ args.routeIndex.openApiEndpointTargetsByEndpoint.get(
797
+ args.endpointLink.endpoint,
798
+ ) ?? [];
799
+ if (targets.length === 1) {
800
+ return targets[0];
801
+ }
802
+
803
+ throw buildInternalLinkRewriteError({
804
+ rawHref: args.rawHref,
805
+ currentDocFilePath: args.currentDocFilePath,
806
+ detail:
807
+ targets.length > 1
808
+ ? `The endpoint "${args.endpointLink.endpoint}" is ambiguous across OpenAPI sources (${formatOpenApiEndpointSourceList(
809
+ targets,
810
+ )}).`
811
+ : `No generated OpenAPI endpoint route matched "${args.endpointLink.endpoint}".`,
812
+ });
813
+ })();
814
+
815
+ const canonicalHref =
816
+ args.routeIndex.canonicalHrefByOpenApiEndpointKey.get(target.targetKey);
817
+ if (!canonicalHref) {
818
+ throw buildInternalLinkRewriteError({
819
+ rawHref: args.rawHref,
820
+ currentDocFilePath: args.currentDocFilePath,
821
+ detail: `The OpenAPI endpoint "${args.endpointLink.endpoint}" from "${target.source}" did not resolve to a canonical route.`,
822
+ });
823
+ }
824
+
825
+ const aliases =
826
+ args.routeIndex.allHrefsByOpenApiEndpointKey.get(target.targetKey) ?? [];
827
+ if (aliases.length > 1) {
828
+ args.warn(
829
+ `[INTERNAL_LINK_WARNING] "${args.rawHref}" matched OpenAPI endpoint "${args.endpointLink.endpoint}" in "${target.source}", which has multiple URLs (${aliases.join(", ")}). Rewriting to canonical URL "${canonicalHref}".`,
830
+ );
831
+ }
832
+
833
+ return withBasePath(canonicalHref);
834
+ }
835
+
530
836
  function resolveLinkToCanonicalHref(args: {
531
837
  rawHref: string;
532
838
  currentDocFilePath: string | null;
533
839
  routeIndex: ResolvedRouteIndex;
534
840
  warn: (message: string) => void;
535
841
  }): string | null {
842
+ const endpointLink = parseOpenApiEndpointLinkHref(args.rawHref);
843
+ if (endpointLink) {
844
+ return resolveOpenApiEndpointLinkToCanonicalHref({
845
+ rawHref: args.rawHref,
846
+ endpointLink,
847
+ currentDocFilePath: args.currentDocFilePath,
848
+ routeIndex: args.routeIndex,
849
+ warn: args.warn,
850
+ });
851
+ }
852
+
536
853
  const resolvedHref = resolveDocsHref(args.rawHref);
537
854
  if (resolvedHref.kind === "ignored" || resolvedHref.kind === "local-asset") {
538
855
  return null;
@@ -3,7 +3,9 @@ import {
3
3
  type NavGroup,
4
4
  type NavPage,
5
5
  type NavOpenApiPage,
6
+ type NavMenu,
6
7
  type NavMenuItem,
8
+ type NavTabItem,
7
9
  type NavOpenApi,
8
10
  type DocsConfig,
9
11
  loadOpenApiSpec,
@@ -22,6 +24,11 @@ import { prependBasePath, withBasePath } from "./base-path";
22
24
  import { getCollection } from "astro:content";
23
25
 
24
26
  type MdxNavPageItem = string | NavPage;
27
+ type NavigationContentContainer = {
28
+ pages?: (string | NavPage | NavGroup | NavOpenApiPage)[];
29
+ menu?: NavMenu;
30
+ openapi?: string | NavOpenApi;
31
+ };
25
32
 
26
33
  // Base route interface
27
34
  export interface BaseRoute {
@@ -420,30 +427,66 @@ async function processMenuItem(
420
427
  ? `${parentSlug}/${menuItemSlug}`
421
428
  : menuItemSlug;
422
429
 
423
- const submenu = menuItem.submenu;
430
+ routes = routes.concat(
431
+ await processNavigationContent(menuItem, currentPrefix, docs),
432
+ );
424
433
 
425
- // Process pages if they exist (pages array can contain pages and groups)
426
- if (submenu.pages) {
427
- for (const item of submenu.pages) {
428
- if (typeof item === "string" || "page" in item) {
429
- routes.push(processPageItem(item, currentPrefix, docs));
430
- } else if ("group" in item) {
434
+ return routes;
435
+ }
436
+
437
+ async function processMenu(
438
+ menu: NavMenu,
439
+ parentSlug: string = "",
440
+ docs: any[],
441
+ ): Promise<Route[]> {
442
+ let routes: Route[] = [];
443
+
444
+ for (const menuItem of menu.items) {
445
+ const menuItemRoutes = await processMenuItem(menuItem, parentSlug, docs);
446
+ routes = routes.concat(menuItemRoutes);
447
+ }
448
+
449
+ return routes;
450
+ }
451
+
452
+ async function processTabItem(
453
+ tabItem: NavTabItem,
454
+ parentSlug: string = "",
455
+ docs: any[],
456
+ ): Promise<Route[]> {
457
+ const tabSlug = slugify(tabItem.slug || tabItem.label);
458
+ const currentPrefix = parentSlug ? `${parentSlug}/${tabSlug}` : tabSlug;
459
+
460
+ return processNavigationContent(tabItem, currentPrefix, docs);
461
+ }
462
+
463
+ async function processNavigationContent(
464
+ item: NavigationContentContainer,
465
+ parentSlug: string,
466
+ docs: any[],
467
+ ): Promise<Route[]> {
468
+ let routes: Route[] = [];
469
+
470
+ if (item.pages) {
471
+ for (const pageItem of item.pages) {
472
+ if (typeof pageItem === "string" || "page" in pageItem) {
473
+ routes.push(processPageItem(pageItem, parentSlug, docs));
474
+ } else if ("group" in pageItem) {
431
475
  routes = routes.concat(
432
- await processGroup(item as NavGroup, currentPrefix, docs),
476
+ await processGroup(pageItem as NavGroup, parentSlug, docs),
433
477
  );
434
- } else if ("openapi" in item) {
435
- routes.push(await processOpenApiPageItem(item, currentPrefix));
478
+ } else if ("openapi" in pageItem) {
479
+ routes.push(await processOpenApiPageItem(pageItem, parentSlug));
436
480
  }
437
481
  }
438
482
  }
439
483
 
440
- // Process OpenAPI file if it exists
441
- if (submenu.openapi) {
442
- const openApiRoutes = await processOpenApiFile(
443
- submenu.openapi,
444
- currentPrefix,
445
- );
446
- routes = routes.concat(openApiRoutes);
484
+ if (item.menu) {
485
+ routes = routes.concat(await processMenu(item.menu, parentSlug, docs));
486
+ }
487
+
488
+ if (item.openapi) {
489
+ routes = routes.concat(await processOpenApiFile(item.openapi, parentSlug));
447
490
  }
448
491
 
449
492
  return routes;
@@ -483,7 +526,7 @@ export async function getAllRoutes(): Promise<Route[]> {
483
526
 
484
527
  let allRoutes: Route[] = [];
485
528
 
486
- // 3. Identify the active navigation container (pages or menu)
529
+ // 3. Identify the active navigation container
487
530
  // We use the XOR guarantee from validateNavigation (only one key exists)
488
531
  if (navigation.pages) {
489
532
  // Case 1: Pages array at the top level (can contain pages and groups)
@@ -499,15 +542,14 @@ export async function getAllRoutes(): Promise<Route[]> {
499
542
  }
500
543
  }
501
544
  } else if (navigation.menu) {
502
- // Case 3: Menu object at the top level
503
- // Need to await all menu items since processMenuItem is now async
504
- for (const menuItem of navigation.menu.items) {
505
- const menuItemRoutes = await processMenuItem(menuItem, "", docs);
506
- allRoutes = allRoutes.concat(menuItemRoutes);
507
- }
545
+ allRoutes = allRoutes.concat(await processMenu(navigation.menu, "", docs));
508
546
  } else if (navigation.openapi) {
509
547
  const openApiRoutes = await processOpenApiFile(navigation.openapi, "");
510
548
  allRoutes = allRoutes.concat(openApiRoutes);
549
+ } else if (navigation.tabs) {
550
+ for (const tabItem of navigation.tabs.items) {
551
+ allRoutes = allRoutes.concat(await processTabItem(tabItem, "", docs));
552
+ }
511
553
  }
512
554
 
513
555
  for (const auxiliaryPageRef of getAuxiliaryPageRefs(config)) {
@@ -139,6 +139,18 @@
139
139
  display: none !important;
140
140
  }
141
141
 
142
+ /* Disable route transition animation while preserving Astro client navigation. */
143
+ ::view-transition,
144
+ ::view-transition-group(*),
145
+ ::view-transition-image-pair(*),
146
+ ::view-transition-old(*),
147
+ ::view-transition-new(*) {
148
+ animation: none !important;
149
+ animation-delay: 0s !important;
150
+ animation-duration: 0s !important;
151
+ mix-blend-mode: normal !important;
152
+ }
153
+
142
154
  /* Prose styling */
143
155
  .prose-rules {
144
156
  @apply prose max-w-none *:my-6 *:first:mt-0 *:last:mb-0 prose-h2:mt-12 prose-h2:mb-2 prose-h2:scroll-mt-24 prose-h3:mt-8 prose-h3:mb-2 prose-h3:scroll-mt-20 prose-headings:font-semibold prose-p:mt-0 prose-p:mb-4 prose-ol:mt-0 prose-ol:mb-5 prose-ul:mt-0 prose-ul:mb-5 prose-a:decoration-(--color-theme) prose-a:decoration-from-font prose-blockquote:border-(--color-theme)/30 dark:prose-blockquote:border-(--color-theme)/30;