zudoku 0.0.0-zce59fc03 → 0.0.0-zd57c18df

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 (232) hide show
  1. package/dist/app/main.js +1 -1
  2. package/dist/app/main.js.map +1 -1
  3. package/dist/config/create-plugin.d.ts +2 -0
  4. package/dist/config/create-plugin.js +55 -0
  5. package/dist/config/create-plugin.js.map +1 -0
  6. package/dist/config/loader.js +2 -2
  7. package/dist/config/loader.js.map +1 -1
  8. package/dist/config/validators/InputNavigationSchema.d.ts +118 -68
  9. package/dist/config/validators/InputNavigationSchema.js +17 -0
  10. package/dist/config/validators/InputNavigationSchema.js.map +1 -1
  11. package/dist/config/validators/NavigationSchema.d.ts +10 -2
  12. package/dist/config/validators/NavigationSchema.js +7 -0
  13. package/dist/config/validators/NavigationSchema.js.map +1 -1
  14. package/dist/config/validators/validate.d.ts +5 -4
  15. package/dist/config/validators/validate.js +2 -0
  16. package/dist/config/validators/validate.js.map +1 -1
  17. package/dist/flat-config.d.ts +12 -0
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/lib/components/Bootstrap.js +1 -2
  22. package/dist/lib/components/Bootstrap.js.map +1 -1
  23. package/dist/lib/components/MobileTopNavigation.js +2 -1
  24. package/dist/lib/components/MobileTopNavigation.js.map +1 -1
  25. package/dist/lib/components/Slot.test.js +1 -1
  26. package/dist/lib/components/Slot.test.js.map +1 -1
  27. package/dist/lib/components/TopNavigation.d.ts +7 -1
  28. package/dist/lib/components/TopNavigation.js +7 -2
  29. package/dist/lib/components/TopNavigation.js.map +1 -1
  30. package/dist/lib/components/Zudoku.d.ts +4 -1
  31. package/dist/lib/components/Zudoku.js +4 -7
  32. package/dist/lib/components/Zudoku.js.map +1 -1
  33. package/dist/lib/components/context/ZudokuContext.d.ts +9 -4
  34. package/dist/lib/components/context/ZudokuContext.js +4 -2
  35. package/dist/lib/components/context/ZudokuContext.js.map +1 -1
  36. package/dist/lib/components/context/ZudokuProvider.js +1 -1
  37. package/dist/lib/components/context/ZudokuProvider.js.map +1 -1
  38. package/dist/lib/components/context/ZudokuReactContext.d.ts +11 -0
  39. package/dist/lib/components/context/ZudokuReactContext.js +4 -0
  40. package/dist/lib/components/context/ZudokuReactContext.js.map +1 -0
  41. package/dist/lib/components/navigation/Navigation.js +4 -3
  42. package/dist/lib/components/navigation/Navigation.js.map +1 -1
  43. package/dist/lib/components/navigation/NavigationCategory.js +8 -0
  44. package/dist/lib/components/navigation/NavigationCategory.js.map +1 -1
  45. package/dist/lib/components/navigation/NavigationFilterContext.d.ts +8 -0
  46. package/dist/lib/components/navigation/NavigationFilterContext.js +12 -0
  47. package/dist/lib/components/navigation/NavigationFilterContext.js.map +1 -0
  48. package/dist/lib/components/navigation/NavigationFilterInput.d.ts +3 -0
  49. package/dist/lib/components/navigation/NavigationFilterInput.js +9 -0
  50. package/dist/lib/components/navigation/NavigationFilterInput.js.map +1 -0
  51. package/dist/lib/components/navigation/NavigationItem.js +11 -1
  52. package/dist/lib/components/navigation/NavigationItem.js.map +1 -1
  53. package/dist/lib/components/navigation/utils.d.ts +2 -1
  54. package/dist/lib/components/navigation/utils.js +22 -1
  55. package/dist/lib/components/navigation/utils.js.map +1 -1
  56. package/dist/lib/core/ZudokuContext.d.ts +2 -1
  57. package/dist/lib/core/ZudokuContext.js +3 -1
  58. package/dist/lib/core/ZudokuContext.js.map +1 -1
  59. package/dist/lib/core/__internal.d.ts +1 -0
  60. package/dist/lib/core/__internal.js +2 -0
  61. package/dist/lib/core/__internal.js.map +1 -1
  62. package/dist/lib/core/plugins.d.ts +5 -1
  63. package/dist/lib/core/plugins.js.map +1 -1
  64. package/dist/lib/core/transform-config.d.ts +4 -2
  65. package/dist/lib/core/transform-config.js +33 -13
  66. package/dist/lib/core/transform-config.js.map +1 -1
  67. package/dist/lib/core/transform-config.test.d.ts +1 -0
  68. package/dist/lib/core/transform-config.test.js +83 -0
  69. package/dist/lib/core/transform-config.test.js.map +1 -0
  70. package/dist/lib/errors/ErrorAlert.js +1 -2
  71. package/dist/lib/errors/ErrorAlert.js.map +1 -1
  72. package/dist/lib/hooks/useEvent.test.js +1 -1
  73. package/dist/lib/hooks/useEvent.test.js.map +1 -1
  74. package/dist/lib/plugins/openapi/playground/fileUtils.d.ts +1 -0
  75. package/dist/lib/plugins/openapi/playground/fileUtils.js +3 -0
  76. package/dist/lib/plugins/openapi/playground/fileUtils.js.map +1 -1
  77. package/dist/lib/plugins/openapi/playground/result-panel/AudioPlayer.d.ts +6 -0
  78. package/dist/lib/plugins/openapi/playground/result-panel/AudioPlayer.js +20 -0
  79. package/dist/lib/plugins/openapi/playground/result-panel/AudioPlayer.js.map +1 -0
  80. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js +7 -2
  81. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js.map +1 -1
  82. package/dist/lib/ui/Alert.d.ts +3 -2
  83. package/dist/lib/ui/Alert.js +9 -5
  84. package/dist/lib/ui/Alert.js.map +1 -1
  85. package/dist/lib/ui/InputGroup.d.ts +16 -0
  86. package/dist/lib/ui/InputGroup.js +65 -0
  87. package/dist/lib/ui/InputGroup.js.map +1 -0
  88. package/dist/lib/ui/Secret.js +2 -2
  89. package/dist/lib/ui/Secret.js.map +1 -1
  90. package/dist/vite/config.js +5 -2
  91. package/dist/vite/config.js.map +1 -1
  92. package/dist/vite/plugin-config.js +16 -4
  93. package/dist/vite/plugin-config.js.map +1 -1
  94. package/dist/vite/plugin-theme.js +2 -1
  95. package/dist/vite/plugin-theme.js.map +1 -1
  96. package/dist/vite/prerender/prerender.js +3 -1
  97. package/dist/vite/prerender/prerender.js.map +1 -1
  98. package/dist/vite/prerender/worker.js +3 -1
  99. package/dist/vite/prerender/worker.js.map +1 -1
  100. package/lib/{ClaudeLogo-C6q-Xn_l.js → ClaudeLogo-Br8C_vTq.js} +3 -3
  101. package/lib/{ClaudeLogo-C6q-Xn_l.js.map → ClaudeLogo-Br8C_vTq.js.map} +1 -1
  102. package/lib/{HydrationBoundary-CNF2ZV3E.js → HydrationBoundary-CJu4vUlG.js} +6 -6
  103. package/lib/{HydrationBoundary-CNF2ZV3E.js.map → HydrationBoundary-CJu4vUlG.js.map} +1 -1
  104. package/lib/{MdxPage-B1G4W1TK.js → MdxPage-C0QFAsgv.js} +6 -6
  105. package/lib/{MdxPage-B1G4W1TK.js.map → MdxPage-C0QFAsgv.js.map} +1 -1
  106. package/lib/Mermaid-Chx5BPHn.js +104 -0
  107. package/lib/Mermaid-Chx5BPHn.js.map +1 -0
  108. package/lib/{OAuthErrorPage-01Ke086W.js → OAuthErrorPage-CFz_gBFx.js} +11 -10
  109. package/lib/{OAuthErrorPage-01Ke086W.js.map → OAuthErrorPage-CFz_gBFx.js.map} +1 -1
  110. package/lib/{OasProvider-oHPiMJZg.js → OasProvider-BQ60YgAd.js} +3 -3
  111. package/lib/{OasProvider-oHPiMJZg.js.map → OasProvider-BQ60YgAd.js.map} +1 -1
  112. package/lib/{OperationList-CZ4OK8Pm.js → OperationList-D31urxqy.js} +40 -39
  113. package/lib/{OperationList-CZ4OK8Pm.js.map → OperationList-D31urxqy.js.map} +1 -1
  114. package/lib/{RouteGuard-B1lCR0C_.js → RouteGuard-CVs3yvEs.js} +3 -3
  115. package/lib/{RouteGuard-B1lCR0C_.js.map → RouteGuard-CVs3yvEs.js.map} +1 -1
  116. package/lib/{SchemaList-DoQFkJgM.js → SchemaList-CSVFH585.js} +7 -7
  117. package/lib/{SchemaList-DoQFkJgM.js.map → SchemaList-CSVFH585.js.map} +1 -1
  118. package/lib/{SchemaView-D2k6ZJck.js → SchemaView-D4marpgk.js} +3 -3
  119. package/lib/{SchemaView-D2k6ZJck.js.map → SchemaView-D4marpgk.js.map} +1 -1
  120. package/lib/{Secret-BDBqq4p3.js → Secret-DUpgv4V3.js} +92 -72
  121. package/lib/Secret-DUpgv4V3.js.map +1 -0
  122. package/lib/{SignUp-8kDBaLbO.js → SignUp-Dug1jAGC.js} +4 -4
  123. package/lib/{SignUp-8kDBaLbO.js.map → SignUp-Dug1jAGC.js.map} +1 -1
  124. package/lib/{SyntaxHighlight-hZOFnYl0.js → SyntaxHighlight-BMu0b_hF.js} +8 -8
  125. package/lib/{SyntaxHighlight-hZOFnYl0.js.map → SyntaxHighlight-BMu0b_hF.js.map} +1 -1
  126. package/lib/{Toc-qEIii_-W.js → Toc-BiJ2YL0O.js} +2 -2
  127. package/lib/{Toc-qEIii_-W.js.map → Toc-BiJ2YL0O.js.map} +1 -1
  128. package/lib/{Zudoku-DUsdmPME.js → Zudoku-iyiXgWFY.js} +2777 -2622
  129. package/lib/Zudoku-iyiXgWFY.js.map +1 -0
  130. package/lib/ZudokuContext-CYyb_PB_.js +175 -0
  131. package/lib/ZudokuContext-CYyb_PB_.js.map +1 -0
  132. package/lib/ZudokuReactContext-DGJAP1sN.js +222 -0
  133. package/lib/ZudokuReactContext-DGJAP1sN.js.map +1 -0
  134. package/lib/{circular-D9tSKG2c.js → circular-cPOX8BVJ.js} +2 -2
  135. package/lib/{circular-D9tSKG2c.js.map → circular-cPOX8BVJ.js.map} +1 -1
  136. package/lib/{createServer-BprC4n85.js → createServer-SJT25uZH.js} +4 -4
  137. package/lib/{createServer-BprC4n85.js.map → createServer-SJT25uZH.js.map} +1 -1
  138. package/lib/{errors-7hgPDs1h.js → errors-B77S9iOc.js} +2 -2
  139. package/lib/{errors-7hgPDs1h.js.map → errors-B77S9iOc.js.map} +1 -1
  140. package/lib/{firebase-Dwn-2ju-.js → firebase-C7XKRGLf.js} +25 -24
  141. package/lib/{firebase-Dwn-2ju-.js.map → firebase-C7XKRGLf.js.map} +1 -1
  142. package/lib/{hook-ZEd1Es7D.js → hook-Dz_n9SoE.js} +16 -15
  143. package/lib/{hook-ZEd1Es7D.js.map → hook-Dz_n9SoE.js.map} +1 -1
  144. package/lib/{index-Dxdhrp-I.js → index-BDp2MTiq.js} +2 -2
  145. package/lib/{index-Dxdhrp-I.js.map → index-BDp2MTiq.js.map} +1 -1
  146. package/lib/{index-CyIW9rHv.js → index-Bc2mE-53.js} +642 -606
  147. package/lib/index-Bc2mE-53.js.map +1 -0
  148. package/lib/{index.esm-DG4KaDKR.js → index.esm-Cth49JBv.js} +2 -2
  149. package/lib/index.esm-Cth49JBv.js.map +1 -0
  150. package/lib/{mutation-BISOc7OM.js → mutation-B7eFBLZY.js} +2 -2
  151. package/lib/{mutation-BISOc7OM.js.map → mutation-B7eFBLZY.js.map} +1 -1
  152. package/lib/ui/Alert.js +32 -20
  153. package/lib/ui/Alert.js.map +1 -1
  154. package/lib/ui/InputGroup.js +155 -0
  155. package/lib/ui/InputGroup.js.map +1 -0
  156. package/lib/ui/Secret.js +2 -2
  157. package/lib/ui/Secret.js.map +1 -1
  158. package/lib/ui/SyntaxHighlight.js +2 -2
  159. package/lib/{useMutation-CFMGlAMW.js → useMutation-CErliDZ9.js} +5 -5
  160. package/lib/{useMutation-CFMGlAMW.js.map → useMutation-CErliDZ9.js.map} +1 -1
  161. package/lib/{useSuspenseQuery-CSB_rVek.js → useQuery-ht7aWJ3S.js} +432 -446
  162. package/lib/useQuery-ht7aWJ3S.js.map +1 -0
  163. package/lib/useSuspenseQuery-DQH4Bmc2.js +18 -0
  164. package/lib/useSuspenseQuery-DQH4Bmc2.js.map +1 -0
  165. package/lib/zudoku.__internal.js +524 -500
  166. package/lib/zudoku.__internal.js.map +1 -1
  167. package/lib/zudoku.auth-auth0.js +6 -5
  168. package/lib/zudoku.auth-auth0.js.map +1 -1
  169. package/lib/zudoku.auth-azureb2c.js +14 -13
  170. package/lib/zudoku.auth-azureb2c.js.map +1 -1
  171. package/lib/zudoku.auth-clerk.js +2 -2
  172. package/lib/zudoku.auth-firebase.js +4 -4
  173. package/lib/zudoku.auth-openid.js +7 -6
  174. package/lib/zudoku.auth-openid.js.map +1 -1
  175. package/lib/zudoku.auth-supabase.js +4 -4
  176. package/lib/zudoku.components.js +3 -3
  177. package/lib/zudoku.hooks.js +3 -3
  178. package/lib/zudoku.mermaid.js +3 -3
  179. package/lib/zudoku.plugin-api-catalog.js +28 -27
  180. package/lib/zudoku.plugin-api-catalog.js.map +1 -1
  181. package/lib/zudoku.plugin-api-keys.js +98 -96
  182. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  183. package/lib/zudoku.plugin-custom-pages.js +1 -1
  184. package/lib/zudoku.plugin-markdown.js +1 -1
  185. package/lib/zudoku.plugin-openapi.js +2 -2
  186. package/lib/zudoku.plugin-search-pagefind.js +19 -18
  187. package/lib/zudoku.plugin-search-pagefind.js.map +1 -1
  188. package/lib/zudoku.plugins.js.map +1 -1
  189. package/lib/zudoku.react-query.js +26 -25
  190. package/lib/zudoku.react-query.js.map +1 -1
  191. package/package.json +10 -4
  192. package/src/app/defaultTheme.css +4 -0
  193. package/src/app/main.css +2 -0
  194. package/src/app/main.tsx +1 -1
  195. package/src/lib/components/Bootstrap.tsx +1 -4
  196. package/src/lib/components/MobileTopNavigation.tsx +13 -8
  197. package/src/lib/components/Slot.test.tsx +1 -1
  198. package/src/lib/components/TopNavigation.tsx +25 -7
  199. package/src/lib/components/Zudoku.tsx +18 -14
  200. package/src/lib/components/context/ZudokuContext.ts +3 -6
  201. package/src/lib/components/context/ZudokuProvider.tsx +1 -1
  202. package/src/lib/components/context/ZudokuReactContext.tsx +17 -0
  203. package/src/lib/components/navigation/Navigation.tsx +4 -3
  204. package/src/lib/components/navigation/NavigationCategory.tsx +9 -0
  205. package/src/lib/components/navigation/NavigationFilterContext.tsx +28 -0
  206. package/src/lib/components/navigation/NavigationFilterInput.tsx +35 -0
  207. package/src/lib/components/navigation/NavigationItem.tsx +17 -1
  208. package/src/lib/components/navigation/utils.ts +32 -1
  209. package/src/lib/core/ZudokuContext.ts +7 -1
  210. package/src/lib/core/__internal.tsx +2 -0
  211. package/src/lib/core/plugins.ts +7 -3
  212. package/src/lib/core/transform-config.test.tsx +99 -0
  213. package/src/lib/core/transform-config.ts +57 -19
  214. package/src/lib/errors/ErrorAlert.tsx +1 -6
  215. package/src/lib/hooks/useEvent.test.tsx +1 -1
  216. package/src/lib/plugins/openapi/playground/fileUtils.ts +4 -0
  217. package/src/lib/plugins/openapi/playground/result-panel/AudioPlayer.tsx +50 -0
  218. package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +33 -17
  219. package/src/lib/ui/Alert.tsx +17 -5
  220. package/src/lib/ui/InputGroup.tsx +168 -0
  221. package/src/lib/ui/Secret.tsx +2 -2
  222. package/lib/Mermaid-B1xNo-pf.js +0 -103
  223. package/lib/Mermaid-B1xNo-pf.js.map +0 -1
  224. package/lib/Secret-BDBqq4p3.js.map +0 -1
  225. package/lib/Separator-BXt1LYnm.js +0 -27
  226. package/lib/Separator-BXt1LYnm.js.map +0 -1
  227. package/lib/Zudoku-DUsdmPME.js.map +0 -1
  228. package/lib/ZudokuContext-BBI06sOx.js +0 -387
  229. package/lib/ZudokuContext-BBI06sOx.js.map +0 -1
  230. package/lib/index-CyIW9rHv.js.map +0 -1
  231. package/lib/index.esm-DG4KaDKR.js.map +0 -1
  232. package/lib/useSuspenseQuery-CSB_rVek.js.map +0 -1
@@ -20,7 +20,7 @@ import { Slot } from "./Slot.js";
20
20
 
21
21
  const createWrapper = (slots: Record<string, ReactNode> = {}) => {
22
22
  const queryClient = new QueryClient();
23
- const context = new ZudokuContext({}, queryClient);
23
+ const context = new ZudokuContext({}, queryClient, {});
24
24
 
25
25
  const wrapper = ({ children }: PropsWithChildren) => (
26
26
  <MemoryRouter initialEntries={["/", "/page"]}>
@@ -2,6 +2,7 @@ import { cx } from "class-variance-authority";
2
2
  import { deepEqual } from "fast-equals";
3
3
  import { Suspense } from "react";
4
4
  import { NavLink, type NavLinkProps } from "react-router";
5
+ import { Separator } from "zudoku/ui/Separator.js";
5
6
  import type { NavigationItem } from "../../config/validators/NavigationSchema.js";
6
7
  import { useAuth } from "../authentication/hook.js";
7
8
  import { joinUrl } from "../util/joinUrl.js";
@@ -24,11 +25,17 @@ export const TopNavigation = () => {
24
25
  <div className="items-center justify-between px-8 h-(--top-nav-height) hidden lg:flex text-sm relative">
25
26
  <nav className="text-sm">
26
27
  <ul className="flex flex-row items-center gap-8">
27
- {filteredItems.map((item) => (
28
- <li key={item.label + item.type}>
29
- <TopNavItem {...item} />
30
- </li>
31
- ))}
28
+ {filteredItems.map((item) =>
29
+ item.type === "separator" ? (
30
+ <li key={item.label} className="-mx-4 h-7">
31
+ <Separator orientation="vertical" />
32
+ </li>
33
+ ) : item.type !== "section" && item.type !== "filter" ? (
34
+ <li key={item.label + item.type}>
35
+ <TopNavItem {...item} />
36
+ </li>
37
+ ) : null,
38
+ )}
32
39
  </ul>
33
40
  </nav>
34
41
  <Slot.Target name="top-navigation-side" />
@@ -51,7 +58,11 @@ const getPathForItem = (item: NavigationItem): string => {
51
58
 
52
59
  return (
53
60
  traverseNavigationItem(item, (child) => {
54
- if (child.type !== "category") {
61
+ if (
62
+ child.type !== "category" &&
63
+ child.type !== "separator" &&
64
+ child.type !== "section"
65
+ ) {
55
66
  return getPathForItem(child);
56
67
  }
57
68
  }) ?? ""
@@ -59,6 +70,8 @@ const getPathForItem = (item: NavigationItem): string => {
59
70
  }
60
71
  case "custom-page":
61
72
  return item.path;
73
+ default:
74
+ return "";
62
75
  }
63
76
  };
64
77
 
@@ -97,7 +110,12 @@ export const TopNavLink = ({
97
110
  );
98
111
  };
99
112
 
100
- export const TopNavItem = (item: NavigationItem) => {
113
+ export const TopNavItem = (
114
+ item: Exclude<
115
+ NavigationItem,
116
+ { type: "separator" } | { type: "section" } | { type: "filter" }
117
+ >,
118
+ ) => {
101
119
  const currentNav = useCurrentNavigation();
102
120
  const isActiveTopNavItem = deepEqual(currentNav.topNavItem, item);
103
121
 
@@ -5,7 +5,7 @@ import { ThemeProvider } from "next-themes";
5
5
  import {
6
6
  memo,
7
7
  type PropsWithChildren,
8
- useContext,
8
+ Suspense,
9
9
  useEffect,
10
10
  useMemo,
11
11
  useState,
@@ -18,7 +18,6 @@ import {
18
18
  type ZudokuContextOptions,
19
19
  } from "../core/ZudokuContext.js";
20
20
  import { TopLevelError } from "../errors/TopLevelError.js";
21
- import { StaggeredRenderContext } from "../plugins/openapi/StaggeredRender.js";
22
21
  import { MdxComponents } from "../util/MdxComponents.js";
23
22
  import "../util/requestIdleCallbackPolyfill.js";
24
23
  import {
@@ -33,7 +32,13 @@ import { ZudokuProvider } from "./context/ZudokuProvider.js";
33
32
  let zudokuContext: ZudokuContext | undefined;
34
33
 
35
34
  const ZudokuInner = memo(
36
- ({ children, ...props }: PropsWithChildren<ZudokuContextOptions>) => {
35
+ ({
36
+ children,
37
+ env,
38
+ ...props
39
+ }: PropsWithChildren<
40
+ ZudokuContextOptions & { env: Record<string, string> }
41
+ >) => {
37
42
  const components = useMemo(
38
43
  () => ({ ...DEFAULT_COMPONENTS, ...props.overrides }),
39
44
  [props.overrides],
@@ -56,12 +61,7 @@ const ZudokuInner = memo(
56
61
  ...props.mdx?.components,
57
62
  };
58
63
  }, [props.mdx?.components, props.plugins]);
59
- const { stagger } = useContext(StaggeredRenderContext);
60
64
  const [didNavigate, setDidNavigate] = useState(false);
61
- const staggeredValue = useMemo(
62
- () => (didNavigate ? { stagger: true } : { stagger }),
63
- [stagger, didNavigate],
64
- );
65
65
  const navigation = useNavigation();
66
66
  const queryClient = useQueryClient();
67
67
 
@@ -72,7 +72,7 @@ const ZudokuInner = memo(
72
72
  setDidNavigate(true);
73
73
  }, [didNavigate, navigation.location]);
74
74
 
75
- zudokuContext ??= new ZudokuContext(props, queryClient);
75
+ zudokuContext ??= new ZudokuContext(props, queryClient, env);
76
76
 
77
77
  const heads = props.plugins?.flatMap((plugin) =>
78
78
  hasHead(plugin) ? (plugin.getHead?.({ location }) ?? []) : [],
@@ -81,8 +81,8 @@ const ZudokuInner = memo(
81
81
  return (
82
82
  <>
83
83
  <Helmet>{heads}</Helmet>
84
- <StaggeredRenderContext.Provider value={staggeredValue}>
85
- <ZudokuProvider context={zudokuContext}>
84
+ <ZudokuProvider context={zudokuContext}>
85
+ <Suspense fallback={<div>Zudoku Loading...</div>}>
86
86
  <RouterEventsEmitter />
87
87
  <SlotProvider slots={props.slots ?? props.UNSAFE_slotlets}>
88
88
  <MDXProvider components={mdxComponents}>
@@ -95,8 +95,8 @@ const ZudokuInner = memo(
95
95
  </ThemeProvider>
96
96
  </MDXProvider>
97
97
  </SlotProvider>
98
- </ZudokuProvider>
99
- </StaggeredRenderContext.Provider>
98
+ </Suspense>
99
+ </ZudokuProvider>
100
100
  </>
101
101
  );
102
102
  },
@@ -104,7 +104,11 @@ const ZudokuInner = memo(
104
104
 
105
105
  ZudokuInner.displayName = "ZudokuInner";
106
106
 
107
- const Zudoku = (props: ZudokuContextOptions) => {
107
+ const Zudoku = (
108
+ props: PropsWithChildren<
109
+ ZudokuContextOptions & { env: Record<string, string> }
110
+ >,
111
+ ) => {
108
112
  return (
109
113
  <ErrorBoundary FallbackComponent={TopLevelError}>
110
114
  <ZudokuInner {...props} />
@@ -1,16 +1,12 @@
1
1
  import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
2
- import { createContext, useContext, useEffect } from "react";
2
+ import { useContext, useEffect } from "react";
3
3
  import { matchPath, useLocation } from "react-router";
4
4
  import type { NavigationItem } from "../../../config/validators/NavigationSchema.js";
5
5
  import { useAuthState } from "../../authentication/state.js";
6
- import type { ZudokuContext } from "../../core/ZudokuContext.js";
7
6
  import { joinUrl } from "../../util/joinUrl.js";
8
7
  import { CACHE_KEYS, useCache } from "../cache.js";
9
8
  import { traverseNavigation } from "../navigation/utils.js";
10
-
11
- export const ZudokuReactContext = createContext<ZudokuContext | undefined>(
12
- undefined,
13
- );
9
+ import { ZudokuReactContext } from "./ZudokuReactContext.js";
14
10
 
15
11
  export const useZudoku = () => {
16
12
  const context = useContext(ZudokuReactContext);
@@ -77,6 +73,7 @@ export const useCurrentNavigation = () => {
77
73
  const location = useLocation();
78
74
 
79
75
  const navItem = traverseNavigation(navigation, (item, parentCategories) => {
76
+ if (item.type === "link") return;
80
77
  if (getItemPath(item) === location.pathname) {
81
78
  return parentCategories.at(0) ?? item;
82
79
  }
@@ -2,7 +2,7 @@ import { useSuspenseQuery } from "@tanstack/react-query";
2
2
  import type { PropsWithChildren } from "react";
3
3
  import type { ZudokuContext } from "../../core/ZudokuContext.js";
4
4
  import { NO_DEHYDRATE } from "../cache.js";
5
- import { ZudokuReactContext } from "./ZudokuContext.js";
5
+ import { ZudokuReactContext } from "./ZudokuReactContext.js";
6
6
 
7
7
  export const ZudokuProvider = ({
8
8
  children,
@@ -0,0 +1,17 @@
1
+ import { type Context, createContext } from "react";
2
+ import type { ZudokuContext } from "../../core/ZudokuContext.js";
3
+
4
+ /**
5
+ * During SSR, Vite's module runner can load the same module multiple times
6
+ * (once for the main app, once for external plugins), creating duplicate
7
+ * React contexts that don't share state.
8
+ */
9
+ declare global {
10
+ var __ZUDOKU_CONTEXT: Context<ZudokuContext | undefined>;
11
+ }
12
+
13
+ globalThis.__ZUDOKU_CONTEXT ??= createContext<ZudokuContext | undefined>(
14
+ undefined,
15
+ );
16
+
17
+ export const ZudokuReactContext = globalThis.__ZUDOKU_CONTEXT;
@@ -2,6 +2,7 @@ import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
2
2
  import type { NavigationItem as NavigationItemType } from "../../../config/validators/NavigationSchema.js";
3
3
  import { DrawerContent, DrawerTitle } from "../../ui/Drawer.js";
4
4
  import { Slot } from "../Slot.js";
5
+ import { NavigationFilterProvider } from "./NavigationFilterContext.js";
5
6
  import { NavigationItem } from "./NavigationItem.js";
6
7
  import { NavigationWrapper } from "./NavigationWrapper.js";
7
8
 
@@ -12,7 +13,7 @@ export const Navigation = ({
12
13
  onRequestClose?: () => void;
13
14
  navigation: NavigationItemType[];
14
15
  }) => (
15
- <>
16
+ <NavigationFilterProvider>
16
17
  <NavigationWrapper>
17
18
  <Slot.Target name="navigation-before" />
18
19
  {navigation.map((item) => (
@@ -30,7 +31,7 @@ export const Navigation = ({
30
31
  <Slot.Target name="navigation-after" />
31
32
  </NavigationWrapper>
32
33
  <DrawerContent
33
- className="lg:hidden h-[100dvh] start-0 w-[320px] rounded-none"
34
+ className="lg:hidden h-dvh start-0 w-[320px] rounded-none"
34
35
  aria-describedby={undefined}
35
36
  >
36
37
  <div className="p-4 overflow-y-auto overscroll-none">
@@ -46,5 +47,5 @@ export const Navigation = ({
46
47
  ))}
47
48
  </div>
48
49
  </DrawerContent>
49
- </>
50
+ </NavigationFilterProvider>
50
51
  );
@@ -7,6 +7,7 @@ import { Button } from "zudoku/ui/Button.js";
7
7
  import type { NavigationCategory as NavigationCategoryType } from "../../../config/validators/NavigationSchema.js";
8
8
  import { cn } from "../../util/cn.js";
9
9
  import { joinUrl } from "../../util/joinUrl.js";
10
+ import { useNavigationFilter } from "./NavigationFilterContext.js";
10
11
  import { NavigationItem } from "./NavigationItem.js";
11
12
  import { navigationListItem, useIsCategoryOpen } from "./utils.js";
12
13
 
@@ -20,6 +21,7 @@ const NavigationCategoryInner = ({
20
21
  const isCategoryOpen = useIsCategoryOpen(category);
21
22
  const [hasInteracted, setHasInteracted] = useState(false);
22
23
  const location = useLocation();
24
+ const { query: filterQuery } = useNavigationFilter();
23
25
 
24
26
  const isCollapsible = category.collapsible ?? true;
25
27
  const isCollapsed = category.collapsed ?? true;
@@ -37,6 +39,13 @@ const NavigationCategoryInner = ({
37
39
  }
38
40
  }, [isCategoryOpen]);
39
41
 
42
+ // Auto-expand when there's an active filter query
43
+ useEffect(() => {
44
+ if (filterQuery.trim()) {
45
+ setOpen(true);
46
+ }
47
+ }, [filterQuery]);
48
+
40
49
  const ToggleButton = isCollapsible && (
41
50
  <Button
42
51
  onClick={(e) => {
@@ -0,0 +1,28 @@
1
+ import {
2
+ createContext,
3
+ type PropsWithChildren,
4
+ useContext,
5
+ useState,
6
+ } from "react";
7
+
8
+ type NavigationFilterContextType = {
9
+ query: string;
10
+ setQuery: (query: string) => void;
11
+ };
12
+
13
+ const NavigationFilterContext = createContext<NavigationFilterContextType>({
14
+ query: "",
15
+ setQuery: () => {},
16
+ });
17
+
18
+ export const NavigationFilterProvider = ({ children }: PropsWithChildren) => {
19
+ const [query, setQuery] = useState("");
20
+
21
+ return (
22
+ <NavigationFilterContext.Provider value={{ query, setQuery }}>
23
+ {children}
24
+ </NavigationFilterContext.Provider>
25
+ );
26
+ };
27
+
28
+ export const useNavigationFilter = () => useContext(NavigationFilterContext);
@@ -0,0 +1,35 @@
1
+ import { SearchIcon, XIcon } from "lucide-react";
2
+ import {
3
+ InputGroup,
4
+ InputGroupAddon,
5
+ InputGroupButton,
6
+ InputGroupInput,
7
+ } from "zudoku/ui/InputGroup.js";
8
+ import { useNavigationFilter } from "./NavigationFilterContext.js";
9
+
10
+ export const NavigationFilterInput = ({
11
+ placeholder,
12
+ }: {
13
+ placeholder?: string;
14
+ }) => {
15
+ const { query, setQuery } = useNavigationFilter();
16
+
17
+ return (
18
+ <InputGroup className="my-2">
19
+ <InputGroupAddon>
20
+ <SearchIcon className="size-3.5" />
21
+ </InputGroupAddon>
22
+ <InputGroupInput
23
+ type="text"
24
+ placeholder={placeholder}
25
+ value={query}
26
+ onChange={(e) => setQuery(e.target.value)}
27
+ />
28
+ {query && (
29
+ <InputGroupButton onClick={() => setQuery("")}>
30
+ <XIcon className="size-3" />
31
+ </InputGroupButton>
32
+ )}
33
+ </InputGroup>
34
+ );
35
+ };
@@ -1,6 +1,7 @@
1
1
  import { ExternalLinkIcon } from "lucide-react";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { NavLink, useLocation } from "react-router";
4
+ import { Separator } from "zudoku/ui/Separator.js";
4
5
  import { Tooltip, TooltipContent, TooltipTrigger } from "zudoku/ui/Tooltip.js";
5
6
  import type { NavigationItem as NavigationItemType } from "../../../config/validators/NavigationSchema.js";
6
7
  import { useAuth } from "../../authentication/hook.js";
@@ -11,6 +12,8 @@ import { useViewportAnchor } from "../context/ViewportAnchorContext.js";
11
12
  import { useZudoku } from "../context/ZudokuContext.js";
12
13
  import { NavigationBadge } from "./NavigationBadge.js";
13
14
  import { NavigationCategory } from "./NavigationCategory.js";
15
+ import { useNavigationFilter } from "./NavigationFilterContext.js";
16
+ import { NavigationFilterInput } from "./NavigationFilterInput.js";
14
17
  import { navigationListItem, shouldShowItem } from "./utils.js";
15
18
 
16
19
  const TruncatedLabel = ({
@@ -65,8 +68,9 @@ export const NavigationItem = ({
65
68
  const { activeAnchor } = useViewportAnchor();
66
69
  const auth = useAuth();
67
70
  const context = useZudoku();
71
+ const { query } = useNavigationFilter();
68
72
 
69
- if (!shouldShowItem(auth, context)(item)) {
73
+ if (!shouldShowItem(auth, context, query)(item)) {
70
74
  return null;
71
75
  }
72
76
 
@@ -75,6 +79,18 @@ export const NavigationItem = ({
75
79
  return (
76
80
  <NavigationCategory category={item} onRequestClose={onRequestClose} />
77
81
  );
82
+ case "separator":
83
+ return (
84
+ <Separator className="my-1 mx-auto w-[calc(100%-var(--padding-nav-item)*2)]!" />
85
+ );
86
+ case "section":
87
+ return (
88
+ <div className="mt-4 px-(--padding-nav-item) text-xs font-semibold text-muted-foreground uppercase tracking-wider">
89
+ {item.label}
90
+ </div>
91
+ );
92
+ case "filter":
93
+ return <NavigationFilterInput placeholder={item.placeholder} />;
78
94
  case "doc":
79
95
  return (
80
96
  <NavLink
@@ -86,6 +86,13 @@ export const usePrevNext = (): {
86
86
  let foundCurrent = false;
87
87
 
88
88
  traverseNavigation(navigation, (item) => {
89
+ if (
90
+ item.type === "separator" ||
91
+ item.type === "section" ||
92
+ item.type === "filter"
93
+ )
94
+ return;
95
+
89
96
  const itemId =
90
97
  item.type === "doc"
91
98
  ? joinUrl(item.path)
@@ -133,9 +140,33 @@ export const navigationListItem = cva(
133
140
  },
134
141
  );
135
142
 
143
+ export const itemMatchesFilter = (
144
+ item: NavigationItem,
145
+ query: string,
146
+ ): boolean => {
147
+ if (["separator", "section", "filter"].includes(item.type)) {
148
+ return true;
149
+ }
150
+ if (item.label?.toLowerCase().includes(query.toLowerCase())) {
151
+ return true;
152
+ }
153
+
154
+ if (item.type === "category") {
155
+ return item.items.some((child) => itemMatchesFilter(child, query));
156
+ }
157
+
158
+ return false;
159
+ };
160
+
136
161
  export const shouldShowItem =
137
- (auth: UseAuthReturn, context: ZudokuContext) =>
162
+ (auth: UseAuthReturn, context: ZudokuContext, filterQuery?: string) =>
138
163
  (item: NavigationItem): boolean => {
164
+ if (item.type === "filter") return true;
165
+
166
+ if (filterQuery?.trim() && !itemMatchesFilter(item, filterQuery)) {
167
+ return false;
168
+ }
169
+
139
170
  if (typeof item.display === "function") {
140
171
  return item.display({ context, auth });
141
172
  }
@@ -132,10 +132,15 @@ export class ZudokuContext {
132
132
  public readonly getAuthState: () => AuthState;
133
133
  public readonly queryClient: QueryClient;
134
134
  public readonly options: ZudokuContextOptions;
135
+ public readonly env: Record<string, string | undefined>;
135
136
  private readonly navigationPlugins: NavigationPlugin[];
136
137
  private emitter = createNanoEvents<ZudokuEvents>();
137
138
 
138
- constructor(options: ZudokuContextOptions, queryClient: QueryClient) {
139
+ constructor(
140
+ options: ZudokuContextOptions,
141
+ queryClient: QueryClient,
142
+ env: Record<string, string | undefined>,
143
+ ) {
139
144
  const pluginProtectedRoutes = Object.fromEntries(
140
145
  (options.plugins ?? []).flatMap((plugin) => {
141
146
  if (!isNavigationPlugin(plugin)) return [];
@@ -152,6 +157,7 @@ export class ZudokuContext {
152
157
  };
153
158
 
154
159
  this.queryClient = queryClient;
160
+ this.env = env;
155
161
  this.options = { ...options, protectedRoutes };
156
162
  this.plugins = options.plugins ?? [];
157
163
  this.navigation = options.navigation ?? [];
@@ -17,6 +17,7 @@ import { StatusPage as StatusPageImport } from "../components/StatusPage.js";
17
17
  import { RouterError as RouterErrorImport } from "../errors/RouterError.js";
18
18
  import { ServerError as ServerErrorImport } from "../errors/ServerError.js";
19
19
  import { RouteGuard as RouteGuardImport } from "./RouteGuard.js";
20
+ import { runPluginTransformConfig as runPluginTransformConfigImport } from "./transform-config.js";
20
21
 
21
22
  export const Layout = LayoutImport;
22
23
  export const RouterError = RouterErrorImport;
@@ -28,3 +29,4 @@ export const Head = Helmet;
28
29
  export const StatusPage = StatusPageImport;
29
30
  export const BuildCheck = BuildCheckImport;
30
31
  export const Meta = MetaImport;
32
+ export const runPluginTransformConfig = runPluginTransformConfigImport;
@@ -68,11 +68,15 @@ export interface ConfigHookContext {
68
68
  configPath: string;
69
69
  }
70
70
 
71
+ export interface TransformConfigContext {
72
+ config: ZudokuConfig;
73
+ merge: <T extends Partial<ZudokuConfig>>(partial: T) => ZudokuConfig & T;
74
+ }
75
+
71
76
  export interface TransformConfigPlugin {
72
77
  transformConfig?: (
73
- config: ZudokuConfig,
74
- ctx: ConfigHookContext,
75
- ) => Partial<ZudokuConfig> | void | Promise<Partial<ZudokuConfig> | void>;
78
+ context: TransformConfigContext,
79
+ ) => ZudokuConfig | void | Promise<ZudokuConfig | void>;
76
80
  }
77
81
 
78
82
  export interface CommonPlugin {
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { isPlainObject, mergeConfig } from "./transform-config.js";
3
+
4
+ describe("isPlainObject", () => {
5
+ test("returns true for plain objects", () => {
6
+ expect(isPlainObject({})).toBe(true);
7
+ expect(isPlainObject({ a: 1 })).toBe(true);
8
+ });
9
+
10
+ test("returns false for arrays", () => {
11
+ expect(isPlainObject([])).toBe(false);
12
+ expect(isPlainObject([1, 2, 3])).toBe(false);
13
+ });
14
+
15
+ test("returns false for null and undefined", () => {
16
+ expect(isPlainObject(null)).toBe(false);
17
+ expect(isPlainObject(undefined)).toBe(false);
18
+ });
19
+
20
+ test("returns false for class instances", () => {
21
+ expect(isPlainObject(new Date())).toBe(false);
22
+ expect(isPlainObject(new Map())).toBe(false);
23
+ expect(isPlainObject(/regex/)).toBe(false);
24
+ });
25
+ });
26
+
27
+ describe("mergeConfig", () => {
28
+ test("merges flat objects", () => {
29
+ const target = { a: 1, b: 2 };
30
+ const source = { b: 3, c: 4 };
31
+ expect(mergeConfig(target, source)).toEqual({ a: 1, b: 3, c: 4 });
32
+ });
33
+
34
+ test("merges nested objects", () => {
35
+ const target = { nested: { a: 1, b: 2 } } as Record<string, unknown>;
36
+ const source = { nested: { b: 3, c: 4 } };
37
+ expect(mergeConfig(target, source)).toEqual({
38
+ nested: { a: 1, b: 3, c: 4 },
39
+ });
40
+ });
41
+
42
+ test("replaces arrays instead of merging", () => {
43
+ const target = { arr: [1, 2, 3] };
44
+ const source = { arr: [4, 5] };
45
+ expect(mergeConfig(target, source)).toEqual({ arr: [4, 5] });
46
+ });
47
+
48
+ test("preserves React elements without deep cloning", () => {
49
+ const element = <div className="test">Hello</div>;
50
+ const target = { banner: { message: "old" } };
51
+ const source = { banner: { message: element } };
52
+
53
+ const result = mergeConfig(target, source);
54
+
55
+ // Should be the exact same reference, not a clone
56
+ expect(result.banner.message).toBe(element);
57
+ });
58
+
59
+ test("does not clone React element children", () => {
60
+ const child = <strong>Bold</strong>;
61
+ const element = <div>{child} text</div>;
62
+ const target = { site: { banner: {} } };
63
+ const source = { site: { banner: { message: element } } };
64
+
65
+ const result = mergeConfig(target, source);
66
+
67
+ // The element should be identical (same reference)
68
+ expect(result.site.banner.message).toBe(element);
69
+ // Children should be preserved exactly
70
+ expect(result.site.banner.message.props.children).toBe(
71
+ element.props.children,
72
+ );
73
+ });
74
+
75
+ test("handles null and undefined values", () => {
76
+ const target = { a: 1, b: 2 };
77
+ const source = { a: null, c: undefined };
78
+ expect(mergeConfig(target, source)).toEqual({
79
+ a: null,
80
+ b: 2,
81
+ c: undefined,
82
+ });
83
+ });
84
+
85
+ test("replaces non-plain objects", () => {
86
+ const date = new Date("2024-01-01");
87
+ const target = { date: new Date("2020-01-01") };
88
+ const source = { date };
89
+ const result = mergeConfig(target, source);
90
+ expect(result.date).toBe(date);
91
+ });
92
+
93
+ test("does not mutate target", () => {
94
+ const target = { a: 1, nested: { b: 2 } };
95
+ const source = { a: 2, nested: { c: 3 } };
96
+ mergeConfig(target, source);
97
+ expect(target).toEqual({ a: 1, nested: { b: 2 } });
98
+ });
99
+ });