zudoku 0.3.0-dev.83 → 0.3.0-dev.84

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 (185) hide show
  1. package/cli.js +5 -1
  2. package/dist/app/demo.js +5 -4
  3. package/dist/app/demo.js.map +1 -1
  4. package/dist/app/main.js +3 -1
  5. package/dist/app/main.js.map +1 -1
  6. package/dist/app/standalone.js +5 -4
  7. package/dist/app/standalone.js.map +1 -1
  8. package/dist/config/validators/ResolvedSidebarSchema.d.ts +18 -0
  9. package/dist/config/validators/ResolvedSidebarSchema.js +76 -0
  10. package/dist/config/validators/ResolvedSidebarSchema.js.map +1 -0
  11. package/dist/config/validators/SidebarSchema.d.ts +177 -0
  12. package/dist/config/validators/SidebarSchema.js +71 -0
  13. package/dist/config/validators/SidebarSchema.js.map +1 -0
  14. package/dist/config/validators/validate.d.ts +411 -59
  15. package/dist/config/validators/validate.js +22 -4
  16. package/dist/config/validators/validate.js.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/lib/components/DevPortal.js +1 -1
  19. package/dist/lib/components/DevPortal.js.map +1 -1
  20. package/dist/lib/components/Header.js.map +1 -1
  21. package/dist/lib/components/Heading.d.ts +1 -1
  22. package/dist/lib/components/Layout.js +2 -2
  23. package/dist/lib/components/Layout.js.map +1 -1
  24. package/dist/lib/components/TopNavigation.js +5 -5
  25. package/dist/lib/components/TopNavigation.js.map +1 -1
  26. package/dist/lib/components/context/DevPortalProvider.d.ts +9 -3
  27. package/dist/lib/components/context/DevPortalProvider.js +11 -23
  28. package/dist/lib/components/context/DevPortalProvider.js.map +1 -1
  29. package/dist/lib/components/context/ThemeContext.d.ts +1 -4
  30. package/dist/lib/components/context/ThemeContext.js +3 -29
  31. package/dist/lib/components/context/ThemeContext.js.map +1 -1
  32. package/dist/lib/components/context/ThemeProvider.d.ts +4 -0
  33. package/dist/lib/components/context/ThemeProvider.js +23 -0
  34. package/dist/lib/components/context/ThemeProvider.js.map +1 -0
  35. package/dist/lib/components/navigation/Sidebar.d.ts +1 -0
  36. package/dist/lib/components/navigation/Sidebar.js +12 -0
  37. package/dist/lib/components/navigation/Sidebar.js.map +1 -0
  38. package/dist/lib/components/navigation/SidebarBadge.d.ts +22 -0
  39. package/dist/lib/components/navigation/SidebarBadge.js +24 -0
  40. package/dist/lib/components/navigation/SidebarBadge.js.map +1 -0
  41. package/dist/lib/components/navigation/SidebarCategory.d.ts +5 -0
  42. package/dist/lib/components/navigation/SidebarCategory.js +33 -0
  43. package/dist/lib/components/navigation/SidebarCategory.js.map +1 -0
  44. package/dist/lib/components/navigation/SidebarItem.d.ts +12 -0
  45. package/dist/lib/components/navigation/SidebarItem.js +42 -0
  46. package/dist/lib/components/navigation/SidebarItem.js.map +1 -0
  47. package/dist/lib/components/navigation/{SideNavigationWrapper.d.ts → SidebarWrapper.d.ts} +1 -1
  48. package/dist/lib/components/navigation/{SideNavigationWrapper.js → SidebarWrapper.js} +2 -2
  49. package/dist/lib/components/navigation/SidebarWrapper.js.map +1 -0
  50. package/dist/lib/components/navigation/utils.d.ts +16 -0
  51. package/dist/lib/components/navigation/utils.js +85 -0
  52. package/dist/lib/components/navigation/utils.js.map +1 -0
  53. package/dist/lib/core/DevPortalContext.d.ts +9 -32
  54. package/dist/lib/core/DevPortalContext.js +8 -5
  55. package/dist/lib/core/DevPortalContext.js.map +1 -1
  56. package/dist/lib/core/plugins.d.ts +6 -8
  57. package/dist/lib/core/plugins.js.map +1 -1
  58. package/dist/lib/plugins/markdown/MdxPage.js +5 -36
  59. package/dist/lib/plugins/markdown/MdxPage.js.map +1 -1
  60. package/dist/lib/plugins/markdown/generateRoutes.js +20 -43
  61. package/dist/lib/plugins/markdown/generateRoutes.js.map +1 -1
  62. package/dist/lib/plugins/openapi/Sidecar.js +12 -2
  63. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  64. package/dist/lib/plugins/openapi/index.js +14 -11
  65. package/dist/lib/plugins/openapi/index.js.map +1 -1
  66. package/dist/lib/plugins/openapi/interfaces.d.ts +1 -1
  67. package/dist/lib/util/useScrollToAnchor.js +31 -17
  68. package/dist/lib/util/useScrollToAnchor.js.map +1 -1
  69. package/dist/vite/plugin-sidebar.d.ts +3 -0
  70. package/dist/vite/plugin-sidebar.js +23 -0
  71. package/dist/vite/plugin-sidebar.js.map +1 -0
  72. package/dist/vite/plugin.js +2 -0
  73. package/dist/vite/plugin.js.map +1 -1
  74. package/lib/{AuthenticationPlugin-XS0DoAhE.js → AuthenticationPlugin-DgwV0hVu.js} +7 -7
  75. package/lib/{AuthenticationPlugin-XS0DoAhE.js.map → AuthenticationPlugin-DgwV0hVu.js.map} +1 -1
  76. package/lib/{CategoryHeading-DCmchnA1.js → CategoryHeading-BWq12Bfa.js} +3 -3
  77. package/lib/{CategoryHeading-DCmchnA1.js.map → CategoryHeading-BWq12Bfa.js.map} +1 -1
  78. package/lib/{Combination-C442XfGG.js → Combination-DkycFHkm.js} +4 -4
  79. package/lib/{Combination-C442XfGG.js.map → Combination-DkycFHkm.js.map} +1 -1
  80. package/lib/{DevPortalProvider-BWeAysxF.js → DevPortalProvider-CTxoCHIT.js} +375 -417
  81. package/lib/DevPortalProvider-CTxoCHIT.js.map +1 -0
  82. package/lib/DeveloperHint-BQSFXH01.js +10 -0
  83. package/lib/{DeveloperHint-DQVwIery.js.map → DeveloperHint-BQSFXH01.js.map} +1 -1
  84. package/lib/{Input-3IEt27jb.js → Input-BclXSY0g.js} +5 -5
  85. package/lib/{Input-3IEt27jb.js.map → Input-BclXSY0g.js.map} +1 -1
  86. package/lib/{Markdown-QsZ-PHET.js → Markdown-B_Gax7at.js} +1136 -1152
  87. package/lib/{Markdown-QsZ-PHET.js.map → Markdown-B_Gax7at.js.map} +1 -1
  88. package/lib/{MdxPage-CA1WmW14.js → MdxPage-Crlr0GmN.js} +67 -81
  89. package/lib/MdxPage-Crlr0GmN.js.map +1 -0
  90. package/lib/{OperationList-CHK_erYP.js → OperationList-nQ0bd8TU.js} +8 -8
  91. package/lib/{OperationList-CHK_erYP.js.map → OperationList-nQ0bd8TU.js.map} +1 -1
  92. package/lib/Route-CNvxEBnR.js +14 -0
  93. package/lib/{Route-D70pGn9n.js.map → Route-CNvxEBnR.js.map} +1 -1
  94. package/lib/{SlotletProvider-B71hNEUL.js → SlotletProvider-CzMAO73_.js} +12 -12
  95. package/lib/{SlotletProvider-B71hNEUL.js.map → SlotletProvider-CzMAO73_.js.map} +1 -1
  96. package/lib/Spinner-fF-Xv-gw.js +274 -0
  97. package/lib/Spinner-fF-Xv-gw.js.map +1 -0
  98. package/lib/index-7kcHaXD6.js +1771 -0
  99. package/lib/index-7kcHaXD6.js.map +1 -0
  100. package/lib/{index-Bl6YeerK.js → index-CgCPw6Jn.js} +1026 -1043
  101. package/lib/index-CgCPw6Jn.js.map +1 -0
  102. package/lib/{index-BH-Ub36F.js → index-DkuZvRNP.js} +4 -4
  103. package/lib/{index-BH-Ub36F.js.map → index-DkuZvRNP.js.map} +1 -1
  104. package/lib/joinPath-VeNuJa7y.js +8 -0
  105. package/lib/joinPath-VeNuJa7y.js.map +1 -0
  106. package/lib/jsx-runtime-B6kdoens.js +635 -0
  107. package/lib/jsx-runtime-B6kdoens.js.map +1 -0
  108. package/lib/{AnchorLink-BZcpTwOs.js → utils-CzT_9Tsn.js} +258 -214
  109. package/lib/utils-CzT_9Tsn.js.map +1 -0
  110. package/lib/zudoku.auth-clerk.js +1 -1
  111. package/lib/zudoku.auth-openid.js +2 -2
  112. package/lib/zudoku.components.js +1229 -1227
  113. package/lib/zudoku.components.js.map +1 -1
  114. package/lib/zudoku.plugin-api-keys.js +19 -19
  115. package/lib/zudoku.plugin-custom-page.js +2 -2
  116. package/lib/zudoku.plugin-markdown.js +21 -38
  117. package/lib/zudoku.plugin-markdown.js.map +1 -1
  118. package/lib/zudoku.plugin-openapi.js +8 -6
  119. package/lib/zudoku.plugin-openapi.js.map +1 -1
  120. package/package.json +4 -1
  121. package/src/app/demo.tsx +5 -4
  122. package/src/app/main.css +2 -2
  123. package/src/app/main.tsx +3 -1
  124. package/src/app/standalone.tsx +5 -4
  125. package/src/lib/components/DevPortal.tsx +1 -1
  126. package/src/lib/components/Header.tsx +2 -2
  127. package/src/lib/components/Layout.tsx +2 -2
  128. package/src/lib/components/TopNavigation.tsx +5 -5
  129. package/src/lib/components/context/DevPortalProvider.ts +11 -28
  130. package/src/lib/components/context/ThemeContext.tsx +3 -41
  131. package/src/lib/components/context/ThemeProvider.tsx +27 -0
  132. package/src/lib/components/navigation/{SideNavigation.tsx → Sidebar.tsx} +7 -7
  133. package/src/lib/components/navigation/SidebarBadge.tsx +40 -0
  134. package/src/lib/components/navigation/SidebarCategory.tsx +105 -0
  135. package/src/lib/components/navigation/SidebarItem.tsx +96 -0
  136. package/src/lib/components/navigation/{SideNavigationWrapper.tsx → SidebarWrapper.tsx} +1 -1
  137. package/src/lib/components/navigation/utils.ts +120 -0
  138. package/src/lib/core/DevPortalContext.ts +12 -44
  139. package/src/lib/core/plugins.ts +6 -13
  140. package/src/lib/plugins/markdown/MdxPage.tsx +14 -50
  141. package/src/lib/plugins/markdown/generateRoutes.tsx +29 -57
  142. package/src/lib/plugins/openapi/Sidecar.tsx +15 -2
  143. package/src/lib/plugins/openapi/index.tsx +17 -23
  144. package/src/lib/plugins/openapi/interfaces.ts +1 -1
  145. package/src/lib/util/useScrollToAnchor.ts +39 -18
  146. package/dist/lib/components/navigation/SideNavigation.d.ts +0 -1
  147. package/dist/lib/components/navigation/SideNavigation.js +0 -12
  148. package/dist/lib/components/navigation/SideNavigation.js.map +0 -1
  149. package/dist/lib/components/navigation/SideNavigationCategory.d.ts +0 -4
  150. package/dist/lib/components/navigation/SideNavigationCategory.js +0 -26
  151. package/dist/lib/components/navigation/SideNavigationCategory.js.map +0 -1
  152. package/dist/lib/components/navigation/SideNavigationItem.d.ts +0 -9
  153. package/dist/lib/components/navigation/SideNavigationItem.js +0 -44
  154. package/dist/lib/components/navigation/SideNavigationItem.js.map +0 -1
  155. package/dist/lib/components/navigation/SideNavigationWrapper.js.map +0 -1
  156. package/dist/lib/components/navigation/useNavigationCollapsibleState.d.ts +0 -9
  157. package/dist/lib/components/navigation/useNavigationCollapsibleState.js +0 -28
  158. package/dist/lib/components/navigation/useNavigationCollapsibleState.js.map +0 -1
  159. package/dist/lib/components/navigation/util.d.ts +0 -8
  160. package/dist/lib/components/navigation/util.js +0 -15
  161. package/dist/lib/components/navigation/util.js.map +0 -1
  162. package/dist/lib/plugins/openapi/MethodBadge.d.ts +0 -13
  163. package/dist/lib/plugins/openapi/MethodBadge.js +0 -26
  164. package/dist/lib/plugins/openapi/MethodBadge.js.map +0 -1
  165. package/dist/lib/util/traverseNavigation.d.ts +0 -6
  166. package/dist/lib/util/traverseNavigation.js +0 -30
  167. package/dist/lib/util/traverseNavigation.js.map +0 -1
  168. package/lib/AnchorLink-BZcpTwOs.js.map +0 -1
  169. package/lib/DevPortalProvider-BWeAysxF.js.map +0 -1
  170. package/lib/DeveloperHint-DQVwIery.js +0 -10
  171. package/lib/MdxPage-CA1WmW14.js.map +0 -1
  172. package/lib/Route-D70pGn9n.js +0 -13
  173. package/lib/Spinner-Coi7ORUV.js +0 -244
  174. package/lib/Spinner-Coi7ORUV.js.map +0 -1
  175. package/lib/index-Bl6YeerK.js.map +0 -1
  176. package/lib/index-Dt-pU7Vu.js +0 -916
  177. package/lib/index-Dt-pU7Vu.js.map +0 -1
  178. package/lib/jsx-runtime-CJBdjYYx.js +0 -1526
  179. package/lib/jsx-runtime-CJBdjYYx.js.map +0 -1
  180. package/src/lib/components/navigation/SideNavigationCategory.tsx +0 -72
  181. package/src/lib/components/navigation/SideNavigationItem.tsx +0 -148
  182. package/src/lib/components/navigation/useNavigationCollapsibleState.ts +0 -42
  183. package/src/lib/components/navigation/util.ts +0 -38
  184. package/src/lib/plugins/openapi/MethodBadge.tsx +0 -36
  185. package/src/lib/util/traverseNavigation.ts +0 -55
@@ -0,0 +1,105 @@
1
+ import * as Collapsible from "@radix-ui/react-collapsible";
2
+ import { ChevronRightIcon } from "lucide-react";
3
+ import { useEffect, useState } from "react";
4
+ import { NavLink } from "react-router-dom";
5
+ import type { ResolvedSidebarItemCategory } from "../../../config/validators/ResolvedSidebarSchema.js";
6
+ import { cn } from "../../util/cn.js";
7
+ import { joinPath } from "../../util/joinPath.js";
8
+ import { useTopNavigationItem } from "../context/DevPortalProvider.js";
9
+ import { navigationListItem, SidebarItem } from "./SidebarItem.js";
10
+ import { useIsCategoryOpen } from "./utils.js";
11
+
12
+ export const SidebarCategory = ({
13
+ category,
14
+ level,
15
+ }: {
16
+ category: ResolvedSidebarItemCategory;
17
+ level: number;
18
+ }) => {
19
+ const topNavItem = useTopNavigationItem();
20
+ const isCategoryOpen = useIsCategoryOpen(category);
21
+
22
+ const isCollapsible = category.collapsible ?? true;
23
+ const isCollapsed = category.collapsed ?? true;
24
+ const isDefaultOpen = Boolean(
25
+ !isCollapsible || !isCollapsed || isCategoryOpen,
26
+ );
27
+ const [open, setOpen] = useState(isDefaultOpen);
28
+
29
+ useEffect(() => {
30
+ // this is triggered when an item from the sidebar is clicked
31
+ // and the sidebar, enclosing this item, is not opened
32
+ if (isCategoryOpen) {
33
+ setOpen(true);
34
+ }
35
+ }, [isCategoryOpen]);
36
+
37
+ const ToggleButton = isCollapsible && (
38
+ <button
39
+ type="button"
40
+ onClick={(e) => {
41
+ e.preventDefault();
42
+ setOpen((prev) => !prev);
43
+ }}
44
+ >
45
+ <ChevronRightIcon
46
+ size={16}
47
+ className="transition shrink-0 group-data-[state=open]:rotate-90"
48
+ />
49
+ </button>
50
+ );
51
+
52
+ return (
53
+ <Collapsible.Root
54
+ className={cn("flex flex-col", level === 0 && "-mx-[--padding-nav-item]")}
55
+ defaultOpen={isDefaultOpen}
56
+ open={open}
57
+ onOpenChange={() => setOpen(true)}
58
+ >
59
+ <Collapsible.Trigger
60
+ className={cn(
61
+ "group text-start",
62
+ navigationListItem({ isActive: false, isTopLevel: level === 0 }),
63
+ isCollapsible
64
+ ? "cursor-pointer"
65
+ : "cursor-default hover:bg-transparent",
66
+ )}
67
+ asChild
68
+ disabled={!isCollapsible}
69
+ >
70
+ {category.link?.type === "doc" ? (
71
+ <NavLink to={joinPath(topNavItem?.id, category.link.id)}>
72
+ {({ isActive }) => (
73
+ <div
74
+ className={cn(
75
+ "flex items-center gap-2 justify-between w-full",
76
+ isActive ? "text-primary font-medium" : "text-foreground/80",
77
+ )}
78
+ >
79
+ <div className="truncate">{category.label}</div>
80
+ {ToggleButton}
81
+ </div>
82
+ )}
83
+ </NavLink>
84
+ ) : (
85
+ <div className="flex items-center justify-between w-full">
86
+ <div className="flex gap-2 truncate w-full">{category.label}</div>
87
+ {ToggleButton}
88
+ </div>
89
+ )}
90
+ </Collapsible.Trigger>
91
+ <Collapsible.Content className="CollapsibleContent ms-[calc(var(--padding-nav-item)*1.125)]">
92
+ <ul className="mt-1 border-l ps-2">
93
+ {category.items.map((item) => (
94
+ <SidebarItem
95
+ key={item.label}
96
+ level={level + 1}
97
+ item={item}
98
+ // activeAnchor={activeAnchor}
99
+ />
100
+ ))}
101
+ </ul>
102
+ </Collapsible.Content>
103
+ </Collapsible.Root>
104
+ );
105
+ };
@@ -0,0 +1,96 @@
1
+ import { cva } from "class-variance-authority";
2
+ import { ExternalLinkIcon } from "lucide-react";
3
+ import { NavLink } from "react-router-dom";
4
+
5
+ import type { ResolvedSidebarItem } from "../../../config/validators/ResolvedSidebarSchema.js";
6
+ import { cn } from "../../util/cn.js";
7
+ import { joinPath } from "../../util/joinPath.js";
8
+ import { AnchorLink } from "../AnchorLink.js";
9
+ import { useTopNavigationItem } from "../context/DevPortalProvider.js";
10
+ import { useViewportAnchor } from "../context/ViewportAnchorContext.js";
11
+ import { SidebarBadge } from "./SidebarBadge.js";
12
+ import { SidebarCategory } from "./SidebarCategory.js";
13
+
14
+ export const navigationListItem = cva(
15
+ "flex px-[--padding-nav-item] py-1.5 rounded-lg hover:bg-accent transition-colors duration-300",
16
+ {
17
+ variants: {
18
+ isTopLevel: {
19
+ true: "font-semibold",
20
+ },
21
+ isActive: {
22
+ true: "text-primary font-medium",
23
+ false: "text-foreground/80",
24
+ },
25
+ isMuted: {
26
+ true: "text-foreground/30",
27
+ false: "",
28
+ },
29
+ },
30
+ },
31
+ );
32
+
33
+ export const DATA_ANCHOR_ATTR = "data-anchor";
34
+
35
+ export const SidebarItem = ({
36
+ item,
37
+ level = 0,
38
+ }: {
39
+ item: ResolvedSidebarItem;
40
+ basePath?: string;
41
+ level?: number;
42
+ }) => {
43
+ const topNavItem = useTopNavigationItem();
44
+ const { activeAnchor } = useViewportAnchor();
45
+
46
+ switch (item.type) {
47
+ case "category":
48
+ return <SidebarCategory category={item} level={level} />;
49
+ case "doc":
50
+ return (
51
+ <NavLink
52
+ className={({ isActive }) =>
53
+ navigationListItem({ isActive, isTopLevel: level === 0 })
54
+ }
55
+ to={joinPath(topNavItem?.id, item.id)}
56
+ >
57
+ {item.label}
58
+ {item.badge && <SidebarBadge {...item.badge} />}
59
+ </NavLink>
60
+ );
61
+ case "link":
62
+ return item.href.startsWith("#") ? (
63
+ <AnchorLink
64
+ to={item.href}
65
+ {...{ [DATA_ANCHOR_ATTR]: item.href.slice(1) }}
66
+ className={cn(
67
+ "flex gap-2.5 justify-between",
68
+ level === 0 && "-mx-[--padding-nav-item]",
69
+ navigationListItem({
70
+ isActive: item.href.slice(1) === activeAnchor,
71
+ }),
72
+ )}
73
+ >
74
+ {item.label}
75
+ {item.badge && <SidebarBadge {...item.badge} />}
76
+ </AnchorLink>
77
+ ) : (
78
+ <a
79
+ className={cn(
80
+ navigationListItem({ isTopLevel: level === 0 }),
81
+ "block",
82
+ )}
83
+ href={item.href}
84
+ target="_blank"
85
+ rel="noopener noreferrer"
86
+ >
87
+ <span className="whitespace-normal">{item.label}</span>
88
+ {/* This prevents that the icon would be positioned in its own line if the text fills an line entirely */}
89
+ <span className="whitespace-nowrap">
90
+ &nbsp;
91
+ <ExternalLinkIcon className="inline ml-1" size={12} />
92
+ </span>
93
+ </a>
94
+ );
95
+ }
96
+ };
@@ -1,7 +1,7 @@
1
1
  import { forwardRef, type PropsWithChildren } from "react";
2
2
  import { cn } from "../../util/cn.js";
3
3
 
4
- export const SideNavigationWrapper = forwardRef<
4
+ export const SidebarWrapper = forwardRef<
5
5
  HTMLDivElement,
6
6
  PropsWithChildren<{ pushMainContent?: boolean; className?: string }>
7
7
  >(function SideNavigation({ children, className, pushMainContent }, ref) {
@@ -0,0 +1,120 @@
1
+ import { useLocation } from "react-router-dom";
2
+ import type {
3
+ ResolvedSidebarItem,
4
+ ResolvedSidebarItemCategory,
5
+ } from "../../../config/validators/ResolvedSidebarSchema.js";
6
+ import { joinPath } from "../../util/joinPath.js";
7
+ import {
8
+ useDevPortal,
9
+ useTopNavigationItem,
10
+ } from "../context/DevPortalProvider.js";
11
+
12
+ export type TraverseCallback<T> = (
13
+ item: ResolvedSidebarItem,
14
+ parentCategories: ResolvedSidebarItem[],
15
+ ) => T | void;
16
+
17
+ export const traverseSidebar = <T>(
18
+ sidebar: ResolvedSidebarItem[],
19
+ callback: TraverseCallback<T>,
20
+ ): T | undefined => {
21
+ for (const item of sidebar) {
22
+ const result = traverseSidebarItem(item, callback);
23
+ if (result !== undefined) return result;
24
+ }
25
+ };
26
+
27
+ export const traverseSidebarItem = <T>(
28
+ item: ResolvedSidebarItem,
29
+ callback: TraverseCallback<T>,
30
+ parentCategories: ResolvedSidebarItem[] = [],
31
+ ): T | undefined => {
32
+ const result = callback(item, parentCategories);
33
+ if (result !== undefined) return result;
34
+
35
+ if (item.type === "category") {
36
+ for (const child of item.items) {
37
+ const childResult = traverseSidebarItem(child, callback, [
38
+ ...parentCategories,
39
+ item,
40
+ ]);
41
+ if (childResult !== undefined) return childResult;
42
+ }
43
+ }
44
+ };
45
+
46
+ export const useCurrentItem = () => {
47
+ const location = useLocation();
48
+ const topNavItem = useTopNavigationItem();
49
+ const { sidebars } = useDevPortal();
50
+ const currentSidebar = topNavItem?.id ? sidebars[topNavItem.id] : [];
51
+
52
+ return traverseSidebar(currentSidebar, (item) => {
53
+ if (
54
+ item.type === "doc" &&
55
+ joinPath(topNavItem?.id, item.id) === location.pathname
56
+ ) {
57
+ return item;
58
+ }
59
+ });
60
+ };
61
+
62
+ export const useIsCategoryOpen = (category: ResolvedSidebarItemCategory) => {
63
+ const location = useLocation();
64
+ const topNavItem = useTopNavigationItem();
65
+
66
+ return traverseSidebarItem(category, (item) => {
67
+ if (item.type === "category" && item.link) {
68
+ const categoryLinkPath = joinPath(topNavItem?.id, item.link.id);
69
+ if (categoryLinkPath === location.pathname) {
70
+ return true;
71
+ }
72
+ }
73
+
74
+ if (item.type === "doc") {
75
+ const docPath = joinPath(topNavItem?.id, item.id);
76
+ if (docPath === location.pathname) {
77
+ return true;
78
+ }
79
+ }
80
+ });
81
+ };
82
+
83
+ export const usePrevNext = (): {
84
+ prev?: { label: string; id: string };
85
+ next?: { label: string; id: string };
86
+ } => {
87
+ const currentId = useLocation().pathname;
88
+ const { sidebars } = useDevPortal();
89
+ const topNavItem = useTopNavigationItem();
90
+ const currentSidebar = topNavItem?.id ? sidebars[topNavItem.id] : [];
91
+
92
+ let prev;
93
+ let next;
94
+
95
+ let foundCurrent = false;
96
+
97
+ traverseSidebar(currentSidebar, (item) => {
98
+ const itemId =
99
+ item.type === "doc"
100
+ ? joinPath(topNavItem?.id, item.id)
101
+ : item.type === "category" && item.link
102
+ ? joinPath(topNavItem?.id, item.link.id)
103
+ : undefined;
104
+
105
+ if (!itemId) return;
106
+
107
+ if (foundCurrent) {
108
+ next = { label: item.label, id: itemId };
109
+ return true;
110
+ }
111
+
112
+ if (currentId === itemId) {
113
+ foundCurrent = true;
114
+ } else {
115
+ prev = { label: item.label, id: itemId };
116
+ }
117
+ });
118
+
119
+ return { prev, next };
120
+ };
@@ -1,10 +1,10 @@
1
1
  import { QueryClient } from "@tanstack/react-query";
2
- import { type ReactNode } from "react";
3
2
  import { NavigateFunction } from "react-router-dom";
3
+ import type { ResolvedSidebarConfig } from "../../config/validators/ResolvedSidebarSchema.js";
4
4
  import { type AuthenticationProvider } from "../authentication/authentication.js";
5
5
  import type { ComponentsContextType } from "../components/context/ComponentsContext.js";
6
- import { type DevPortalPath } from "../components/DevPortal.js";
7
6
  import { Slotlets } from "../components/SlotletProvider.js";
7
+ import { joinPath } from "../util/joinPath.js";
8
8
  import type { MdxComponentsType } from "../util/MdxComponents.js";
9
9
  import {
10
10
  type DevPortalPlugin,
@@ -20,41 +20,6 @@ export interface ApiIdentity {
20
20
  id: string;
21
21
  }
22
22
 
23
- type BaseNavigationCategoryItem = {
24
- label: ReactNode;
25
- title?: string;
26
- muted?: boolean;
27
- icon?: ReactNode;
28
- };
29
-
30
- export type PathNavigationCategoryItem = BaseNavigationCategoryItem & {
31
- path: string;
32
- children?: NavigationCategoryItem[];
33
- };
34
-
35
- export type HrefNavigationCategoryItem = BaseNavigationCategoryItem & {
36
- href: string;
37
- };
38
-
39
- export type NavigationCategoryItem =
40
- | PathNavigationCategoryItem
41
- | HrefNavigationCategoryItem;
42
-
43
- export type NavigationCategory = {
44
- label: string;
45
- path?: string;
46
- expanded?: boolean;
47
- collapsible?: boolean;
48
- icon?: ReactNode;
49
- children: NavigationCategoryItem[];
50
- };
51
-
52
- export type NavigationItem = {
53
- label: string;
54
- path: DevPortalPath;
55
- categories?: NavigationCategory[];
56
- };
57
-
58
23
  export const queryClient = new QueryClient();
59
24
 
60
25
  export type ApiKeyCache = "api-keys";
@@ -90,7 +55,8 @@ export type ZudokuContextOptions = {
90
55
  metadata?: Metadata;
91
56
  page?: Page;
92
57
  authentication?: AuthenticationProvider;
93
- navigation: NavigationItem[];
58
+ topNavigation?: Array<{ id: string; label: string }>;
59
+ sidebars?: ResolvedSidebarConfig;
94
60
  plugins?: DevPortalPlugin[];
95
61
  slotlets?: Slotlets;
96
62
  mdx?: {
@@ -100,8 +66,9 @@ export type ZudokuContextOptions = {
100
66
  };
101
67
 
102
68
  export class DevPortalContext {
103
- public plugins: NonNullable<ZudokuContextOptions["plugins"]> = [];
104
- public navigation: ZudokuContextOptions["navigation"];
69
+ public plugins: NonNullable<ZudokuContextOptions["plugins"]>;
70
+ public sidebars: NonNullable<ZudokuContextOptions["sidebars"]>;
71
+ public topNavigation: NonNullable<ZudokuContextOptions["topNavigation"]>;
105
72
  public meta: ZudokuContextOptions["metadata"];
106
73
  public page: ZudokuContextOptions["page"];
107
74
  public authentication?: ZudokuContextOptions["authentication"];
@@ -109,7 +76,8 @@ export class DevPortalContext {
109
76
 
110
77
  constructor(config: ZudokuContextOptions) {
111
78
  this.plugins = config.plugins ?? [];
112
- this.navigation = config.navigation;
79
+ this.topNavigation = config.topNavigation ?? [];
80
+ this.sidebars = config.sidebars ?? {};
113
81
  this.navigationPlugins = this.plugins.filter(isNavigationPlugin);
114
82
  this.authentication = config.authentication;
115
83
  this.meta = config.metadata;
@@ -142,10 +110,10 @@ export class DevPortalContext {
142
110
  return keys.flat();
143
111
  };
144
112
 
145
- getNavigation = async (path: string) => {
113
+ getPluginNavigation = async (path: string) => {
146
114
  const navigations = await Promise.all(
147
- this.navigationPlugins.map(async (plugin) =>
148
- plugin.getNavigation?.(path),
115
+ this.navigationPlugins.map((plugin) =>
116
+ plugin.getNavigation?.(joinPath(path)),
149
117
  ),
150
118
  );
151
119
 
@@ -1,15 +1,8 @@
1
1
  import { type ReactElement } from "react";
2
2
  import { NavigateFunction, type RouteObject } from "react-router-dom";
3
+ import type { ResolvedSidebar } from "../../config/validators/ResolvedSidebarSchema.js";
3
4
  import { MdxComponentsType } from "../util/MdxComponents.js";
4
- import {
5
- DevPortalContext,
6
- type ApiIdentity,
7
- type NavigationCategory,
8
- } from "./DevPortalContext.js";
9
-
10
- export type PluginNavigationCategory = {
11
- path: string;
12
- } & NavigationCategory;
5
+ import { DevPortalContext, type ApiIdentity } from "./DevPortalContext.js";
13
6
 
14
7
  export type DevPortalPlugin =
15
8
  | CommonPlugin
@@ -19,7 +12,7 @@ export type DevPortalPlugin =
19
12
 
20
13
  export interface NavigationPlugin {
21
14
  getRoutes: () => RouteObject[];
22
- getNavigation?: (path: string) => Promise<PluginNavigationCategory[]>;
15
+ getNavigation?: (path: string) => Promise<ResolvedSidebar>;
23
16
  }
24
17
 
25
18
  export interface ApiIdentityPlugin {
@@ -27,13 +20,13 @@ export interface ApiIdentityPlugin {
27
20
  }
28
21
 
29
22
  export interface ProfileMenuPlugin {
30
- getProfileMenuItems: (context: DevPortalContext) => NavigationItem[];
23
+ getProfileMenuItems: (context: DevPortalContext) => ProfileNavigationItem[];
31
24
  }
32
25
 
33
- export type NavigationItem = {
26
+ export type ProfileNavigationItem = {
34
27
  label: string;
35
28
  path?: string;
36
- children?: NavigationItem[];
29
+ children?: ProfileNavigationItem[];
37
30
  };
38
31
 
39
32
  export interface CommonPlugin {
@@ -1,16 +1,17 @@
1
1
  import { useMDXComponents } from "@mdx-js/react";
2
2
  import { Helmet } from "@zudoku/react-helmet-async";
3
- import { useMemo, type PropsWithChildren, type ReactNode } from "react";
4
- import { Link, useLocation } from "react-router-dom";
3
+ import { type PropsWithChildren } from "react";
4
+ import { Link } from "react-router-dom";
5
5
  import { CategoryHeading } from "../../components/CategoryHeading.js";
6
6
  import { Heading } from "../../components/Heading.js";
7
7
  import { ProseClasses } from "../../components/Markdown.js";
8
- import { useTopNavigationItem } from "../../components/context/DevPortalProvider.js";
9
- import { isPathItem } from "../../components/navigation/util.js";
8
+ import {
9
+ useCurrentItem,
10
+ usePrevNext,
11
+ } from "../../components/navigation/utils.js";
10
12
  import type { MdxComponentsType } from "../../util/MdxComponents.js";
11
13
  import { cn } from "../../util/cn.js";
12
14
  import slugify from "../../util/slugify.js";
13
- import { traverseNavigation } from "../../util/traverseNavigation.js";
14
15
  import { Toc } from "./Toc.js";
15
16
  import { MarkdownPluginDefaultOptions, MDXImport } from "./index.js";
16
17
 
@@ -39,17 +40,7 @@ export const MdxPage = ({
39
40
  defaultOptions?: MarkdownPluginDefaultOptions;
40
41
  }
41
42
  >) => {
42
- const navItem = useTopNavigationItem();
43
- const location = useLocation();
44
-
45
- const categoryTitle = navItem
46
- ? traverseNavigation(navItem, (_node, fullPath, parentNodes) => {
47
- if (fullPath === location.pathname) {
48
- return parentNodes.at(0)?.label;
49
- }
50
- })
51
- : undefined;
52
-
43
+ const categoryTitle = useCurrentItem()?.categoryLabel;
53
44
  const title = frontmatter.title;
54
45
  const category = frontmatter.category ?? categoryTitle;
55
46
  const hideToc = frontmatter.toc === false || defaultOptions?.toc === false;
@@ -65,30 +56,7 @@ export const MdxPage = ({
65
56
 
66
57
  const showToc = !hideToc && tocEntries.length > 0;
67
58
 
68
- const { prev, next } = useMemo(() => {
69
- let prev = { path: "", label: "" as ReactNode };
70
- let next = { path: "", label: "" as ReactNode };
71
- let shouldStop = false;
72
-
73
- if (!navItem) return { prev, next };
74
-
75
- traverseNavigation(navItem, (node, fullPath) => {
76
- const item = { path: fullPath, label: node.label };
77
-
78
- if (shouldStop && isPathItem(node) && !node.children?.length) {
79
- next = item;
80
- return true;
81
- }
82
- if (fullPath === location.pathname) {
83
- shouldStop = true;
84
- }
85
- if (!shouldStop && isPathItem(node) && !node.children?.length) {
86
- prev = item;
87
- }
88
- });
89
-
90
- return { prev, next } as const;
91
- }, [navItem, location.pathname]);
59
+ const { prev, next } = usePrevNext();
92
60
 
93
61
  return (
94
62
  <div className="xl:grid grid-cols-[--sidecar-grid-cols] gap-8 justify-between">
@@ -119,13 +87,11 @@ export const MdxPage = ({
119
87
  <>
120
88
  <hr />
121
89
  <div className="not-prose flex items-center justify-between gap-8">
122
- {prev.path ? (
90
+ {prev ? (
123
91
  <Link
124
- to={prev.path}
92
+ to={prev.id}
125
93
  className="flex flex-col items-stretch gap-2 flex-1 truncate border rounded px-6 py-4 text-start hover:border-primary/85 transition shadow-sm hover:shadow-md"
126
- title={
127
- typeof prev.label === "string" ? prev.label : undefined
128
- }
94
+ title={prev.label}
129
95
  >
130
96
  <div className="text-sm text-muted-foreground">
131
97
  ← Previous page
@@ -137,13 +103,11 @@ export const MdxPage = ({
137
103
  ) : (
138
104
  <div className="flex-1" />
139
105
  )}
140
- {next.path ? (
106
+ {next ? (
141
107
  <Link
142
- to={next.path}
108
+ to={next.id}
143
109
  className="flex flex-col items-stretch gap-2 flex-1 truncate border rounded px-6 py-4 text-end hover:border-primary/85 transition shadow-sm hover:shadow-md"
144
- title={
145
- typeof next.label === "string" ? next.label : undefined
146
- }
110
+ title={next.label}
147
111
  >
148
112
  <div className="text-sm text-muted-foreground">
149
113
  Next page →
@@ -1,7 +1,4 @@
1
- import { Navigate, type RouteObject } from "react-router-dom";
2
- import { useTopNavigationItem } from "../../components/context/DevPortalProvider.js";
3
- import { isPathItem } from "../../components/navigation/util.js";
4
- import { traverseNavigation } from "../../util/traverseNavigation.js";
1
+ import { type RouteObject } from "react-router-dom";
5
2
 
6
3
  import {
7
4
  MarkdownPluginDefaultOptions,
@@ -11,57 +8,32 @@ import {
11
8
  export const generateRoutes = (
12
9
  markdownFiles: MarkdownPluginOptions["markdownFiles"],
13
10
  defaultOptions?: MarkdownPluginDefaultOptions,
14
- ): RouteObject[] => {
15
- const routes = Object.entries(markdownFiles).flatMap(
16
- ([file, importPromise]) => {
17
- // @todo we can pass in the folder name and then filter the markdown files based on that path
18
- const match = file.match(/pages\/(.*).mdx?$/);
19
- const path = match?.at(1);
20
-
21
- if (!path) return [];
22
-
23
- const pathSegments = path.split("/");
24
- const isIndexFile = pathSegments.at(-1) === "index";
25
- const routePath = isIndexFile
26
- ? pathSegments.slice(0, -1).join("/")
27
- : path;
28
-
29
- return {
30
- path: routePath,
31
- lazy: async () => {
32
- const { MdxPage } = await import("./MdxPage.js");
33
- const { default: Component, ...props } = await importPromise();
34
- return {
35
- element: (
36
- <MdxPage
37
- mdxComponent={Component}
38
- {...props}
39
- defaultOptions={defaultOptions}
40
- />
41
- ),
42
- };
43
- },
44
- } satisfies RouteObject;
45
- },
46
- );
47
-
48
- const rootRoutes: RouteObject[] = Array.from(
49
- new Set(routes.map((route) => route.path.split("/").at(0))),
50
- ).map((dir) => ({
51
- path: `/${dir}`,
52
- element: <Redirect />,
53
- }));
54
-
55
- return [...routes, ...rootRoutes];
56
- };
57
-
58
- const Redirect = () => {
59
- const navItem = useTopNavigationItem();
60
-
61
- if (!navItem) return null;
62
-
63
- return traverseNavigation(navItem, (node, fullPath) => {
64
- if ("children" in node || !isPathItem(node)) return;
65
- return <Navigate to={fullPath} replace />;
11
+ ): RouteObject[] =>
12
+ Object.entries(markdownFiles).flatMap(([file, importPromise]) => {
13
+ // @todo we can pass in the folder name and then filter the markdown files based on that path
14
+ const match = file.match(/pages\/(.*).mdx?$/);
15
+ const path = match?.at(1);
16
+
17
+ if (!path) return [];
18
+
19
+ const pathSegments = path.split("/");
20
+ const isIndexFile = pathSegments.at(-1) === "index";
21
+ const routePath = isIndexFile ? pathSegments.slice(0, -1).join("/") : path;
22
+
23
+ return {
24
+ path: routePath,
25
+ lazy: async () => {
26
+ const { MdxPage } = await import("./MdxPage.js");
27
+ const { default: Component, ...props } = await importPromise();
28
+ return {
29
+ element: (
30
+ <MdxPage
31
+ mdxComponent={Component}
32
+ {...props}
33
+ defaultOptions={defaultOptions}
34
+ />
35
+ ),
36
+ };
37
+ },
38
+ } satisfies RouteObject;
66
39
  });
67
- };