zudoku 0.17.0 → 0.18.0

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 (275) hide show
  1. package/dist/app/demo.js +0 -2
  2. package/dist/app/demo.js.map +1 -1
  3. package/dist/app/entry.client.js +14 -0
  4. package/dist/app/entry.client.js.map +1 -1
  5. package/dist/app/entry.server.js +5 -4
  6. package/dist/app/entry.server.js.map +1 -1
  7. package/dist/app/standalone.js +0 -2
  8. package/dist/app/standalone.js.map +1 -1
  9. package/dist/codegen.d.ts +3 -0
  10. package/dist/codegen.js +45 -0
  11. package/dist/codegen.js.map +1 -0
  12. package/dist/config/validators/InputSidebarSchema.d.ts +10 -10
  13. package/dist/config/validators/validate.d.ts +74 -74
  14. package/dist/lib/authentication/hook.d.ts +5 -4
  15. package/dist/lib/authentication/hook.js +1 -3
  16. package/dist/lib/authentication/hook.js.map +1 -1
  17. package/dist/lib/authentication/providers/auth0.js +11 -11
  18. package/dist/lib/authentication/providers/auth0.js.map +1 -1
  19. package/dist/lib/authentication/providers/openid.d.ts +0 -1
  20. package/dist/lib/authentication/providers/openid.js +11 -26
  21. package/dist/lib/authentication/providers/openid.js.map +1 -1
  22. package/dist/lib/authentication/state.d.ts +25 -4
  23. package/dist/lib/authentication/state.js +28 -5
  24. package/dist/lib/authentication/state.js.map +1 -1
  25. package/dist/lib/components/Bootstrap.d.ts +3 -1
  26. package/dist/lib/components/Bootstrap.js +11 -3
  27. package/dist/lib/components/Bootstrap.js.map +1 -1
  28. package/dist/lib/components/DeveloperHint.js +2 -1
  29. package/dist/lib/components/DeveloperHint.js.map +1 -1
  30. package/dist/lib/components/Header.js +3 -7
  31. package/dist/lib/components/Header.js.map +1 -1
  32. package/dist/lib/components/Heading.d.ts +1 -1
  33. package/dist/lib/components/Layout.js +11 -3
  34. package/dist/lib/components/Layout.js.map +1 -1
  35. package/dist/lib/components/MobileTopNavigation.js +6 -7
  36. package/dist/lib/components/MobileTopNavigation.js.map +1 -1
  37. package/dist/lib/components/SyntaxHighlight.js +16 -12
  38. package/dist/lib/components/SyntaxHighlight.js.map +1 -1
  39. package/dist/lib/components/ThemeSwitch.d.ts +1 -0
  40. package/dist/lib/components/ThemeSwitch.js +13 -0
  41. package/dist/lib/components/ThemeSwitch.js.map +1 -0
  42. package/dist/lib/components/TopNavigation.d.ts +2 -0
  43. package/dist/lib/components/TopNavigation.js +13 -7
  44. package/dist/lib/components/TopNavigation.js.map +1 -1
  45. package/dist/lib/components/Zudoku.js +4 -5
  46. package/dist/lib/components/Zudoku.js.map +1 -1
  47. package/dist/lib/components/context/ZudokuContext.d.ts +3 -3
  48. package/dist/lib/components/context/ZudokuContext.js +7 -12
  49. package/dist/lib/components/context/ZudokuContext.js.map +1 -1
  50. package/dist/lib/components/index.d.ts +14 -3
  51. package/dist/lib/components/navigation/Sidebar.js +1 -1
  52. package/dist/lib/components/navigation/Sidebar.js.map +1 -1
  53. package/dist/lib/components/navigation/utils.js +2 -2
  54. package/dist/lib/components/navigation/utils.js.map +1 -1
  55. package/dist/lib/core/ZudokuContext.d.ts +0 -4
  56. package/dist/lib/core/ZudokuContext.js +0 -5
  57. package/dist/lib/core/ZudokuContext.js.map +1 -1
  58. package/dist/lib/errors/ErrorAlert.js +1 -1
  59. package/dist/lib/errors/ErrorAlert.js.map +1 -1
  60. package/dist/lib/plugins/openapi/ColorizedParam.js +13 -9
  61. package/dist/lib/plugins/openapi/ColorizedParam.js.map +1 -1
  62. package/dist/lib/plugins/openapi/Endpoint.d.ts +1 -1
  63. package/dist/lib/plugins/openapi/Endpoint.js +5 -9
  64. package/dist/lib/plugins/openapi/Endpoint.js.map +1 -1
  65. package/dist/lib/plugins/openapi/OperationList.d.ts +2 -2
  66. package/dist/lib/plugins/openapi/OperationList.js +6 -21
  67. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  68. package/dist/lib/plugins/openapi/Route.d.ts +4 -4
  69. package/dist/lib/plugins/openapi/Route.js +2 -4
  70. package/dist/lib/plugins/openapi/Route.js.map +1 -1
  71. package/dist/lib/plugins/openapi/Sidecar.d.ts +1 -1
  72. package/dist/lib/plugins/openapi/Sidecar.js +8 -11
  73. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  74. package/dist/lib/plugins/openapi/client/GraphQLClient.d.ts +8 -0
  75. package/dist/lib/plugins/openapi/client/GraphQLClient.js +102 -0
  76. package/dist/lib/plugins/openapi/client/GraphQLClient.js.map +1 -0
  77. package/dist/lib/plugins/openapi/client/GraphQLContext.d.ts +7 -0
  78. package/dist/lib/plugins/openapi/client/GraphQLContext.js +5 -0
  79. package/dist/lib/plugins/openapi/client/GraphQLContext.js.map +1 -0
  80. package/dist/lib/plugins/openapi/client/createServer.d.ts +1 -0
  81. package/dist/lib/plugins/openapi/client/useCreateQuery.d.ts +5 -0
  82. package/dist/lib/plugins/openapi/client/useCreateQuery.js +13 -0
  83. package/dist/lib/plugins/openapi/client/useCreateQuery.js.map +1 -0
  84. package/dist/lib/plugins/openapi/client/worker.d.ts +4 -1
  85. package/dist/lib/plugins/openapi/client/worker.js +23 -14
  86. package/dist/lib/plugins/openapi/client/worker.js.map +1 -1
  87. package/dist/lib/plugins/openapi/graphql/fragment-masking.d.ts +3 -3
  88. package/dist/lib/plugins/openapi/graphql/fragment-masking.js +3 -4
  89. package/dist/lib/plugins/openapi/graphql/fragment-masking.js.map +1 -1
  90. package/dist/lib/plugins/openapi/graphql/gql.d.ts +5 -52
  91. package/dist/lib/plugins/openapi/graphql/gql.js +2 -1
  92. package/dist/lib/plugins/openapi/graphql/gql.js.map +1 -1
  93. package/dist/lib/plugins/openapi/graphql/graphql.d.ts +134 -9
  94. package/dist/lib/plugins/openapi/graphql/graphql.js +194 -778
  95. package/dist/lib/plugins/openapi/graphql/graphql.js.map +1 -1
  96. package/dist/lib/plugins/openapi/index.js +40 -53
  97. package/dist/lib/plugins/openapi/index.js.map +1 -1
  98. package/dist/lib/plugins/openapi/schema/SchemaView.js.map +1 -1
  99. package/dist/lib/plugins/openapi-worker.d.ts +1 -1
  100. package/dist/lib/plugins/openapi-worker.js +7 -1
  101. package/dist/lib/plugins/openapi-worker.js.map +1 -1
  102. package/dist/lib/util/MdxComponents.d.ts +1 -1
  103. package/dist/vite/config.js +0 -1
  104. package/dist/vite/config.js.map +1 -1
  105. package/dist/vite/html.js +0 -2
  106. package/dist/vite/html.js.map +1 -1
  107. package/dist/vite/plugin-component.js +1 -1
  108. package/dist/vite/plugin-component.js.map +1 -1
  109. package/dist/vite/plugin-mdx.js +3 -2
  110. package/dist/vite/plugin-mdx.js.map +1 -1
  111. package/dist/vite/plugin.js +0 -2
  112. package/dist/vite/plugin.js.map +1 -1
  113. package/dist/vite/remarkStaticGeneration.d.ts +3 -0
  114. package/dist/vite/remarkStaticGeneration.js +125 -0
  115. package/dist/vite/remarkStaticGeneration.js.map +1 -0
  116. package/lib/{AnchorLink-DYbUOP9U.js → AnchorLink-CDlhr8gL.js} +11 -10
  117. package/lib/{AnchorLink-DYbUOP9U.js.map → AnchorLink-CDlhr8gL.js.map} +1 -1
  118. package/lib/{AuthenticationPlugin-bqGAKfot.js → AuthenticationPlugin-DeGDVa1r.js} +6 -5
  119. package/lib/{AuthenticationPlugin-bqGAKfot.js.map → AuthenticationPlugin-DeGDVa1r.js.map} +1 -1
  120. package/lib/{Spinner-ChOGyPls.js → Button-jK0EsymC.js} +12 -15
  121. package/lib/Button-jK0EsymC.js.map +1 -0
  122. package/lib/Markdown-ievDDhFT.js +15192 -0
  123. package/lib/Markdown-ievDDhFT.js.map +1 -0
  124. package/lib/{MdxPage-DRKqyn2b.js → MdxPage-Bwn-VSsH.js} +5 -5
  125. package/lib/{MdxPage-DRKqyn2b.js.map → MdxPage-Bwn-VSsH.js.map} +1 -1
  126. package/lib/OperationList-BwBl1xrD.js +4691 -0
  127. package/lib/OperationList-BwBl1xrD.js.map +1 -0
  128. package/lib/Route-DlG_HTMu.js +11 -0
  129. package/lib/Route-DlG_HTMu.js.map +1 -0
  130. package/lib/{Select-DYKDahHt.js → Select-O9ZM3ZgX.js} +7 -7
  131. package/lib/Select-O9ZM3ZgX.js.map +1 -0
  132. package/lib/SidebarBadge-DxFJcJ6V.js +51 -0
  133. package/lib/SidebarBadge-DxFJcJ6V.js.map +1 -0
  134. package/lib/SlotletProvider-DyomlzGx.js +252 -0
  135. package/lib/SlotletProvider-DyomlzGx.js.map +1 -0
  136. package/lib/Spinner-3cQDBVGr.js +7 -0
  137. package/lib/Spinner-3cQDBVGr.js.map +1 -0
  138. package/lib/SyntaxHighlight-DkLOsjHS.js +2983 -0
  139. package/lib/SyntaxHighlight-DkLOsjHS.js.map +1 -0
  140. package/lib/assets/{worker-YA-aCP3P.js → worker-CPsGZsve.js} +24 -22
  141. package/lib/assets/{worker-YA-aCP3P.js.map → worker-CPsGZsve.js.map} +1 -1
  142. package/lib/context-D1nXWxm7.js +22 -0
  143. package/lib/context-D1nXWxm7.js.map +1 -0
  144. package/lib/createServer-DK-g7kbB.js +16089 -0
  145. package/lib/createServer-DK-g7kbB.js.map +1 -0
  146. package/lib/{hook-CjQERPa7.js → hook-hEqe7fPB.js} +12 -14
  147. package/lib/hook-hEqe7fPB.js.map +1 -0
  148. package/lib/index-Czzd9rjU.js +899 -0
  149. package/lib/index-Czzd9rjU.js.map +1 -0
  150. package/lib/index-DNxQ_rCt.js +1273 -0
  151. package/lib/index-DNxQ_rCt.js.map +1 -0
  152. package/lib/index-Yn8c3UWE.js +921 -0
  153. package/lib/index-Yn8c3UWE.js.map +1 -0
  154. package/lib/{router-BsfSoK2j.js → router-lfyopgBI.js} +23 -23
  155. package/lib/{router-BsfSoK2j.js.map → router-lfyopgBI.js.map} +1 -1
  156. package/lib/state-tsXBLONe.js +203 -0
  157. package/lib/state-tsXBLONe.js.map +1 -0
  158. package/lib/ui/ActionButton.js +11 -10
  159. package/lib/ui/ActionButton.js.map +1 -1
  160. package/lib/useExposedProps-CTPtylCV.js +10 -0
  161. package/lib/{useExposedProps-BxyHjPNN.js.map → useExposedProps-CTPtylCV.js.map} +1 -1
  162. package/lib/{utils-DNAltzXc.js → utils-DcpDOncX.js} +197 -202
  163. package/lib/utils-DcpDOncX.js.map +1 -0
  164. package/lib/zudoku.auth-auth0.js +24 -18
  165. package/lib/zudoku.auth-auth0.js.map +1 -1
  166. package/lib/zudoku.auth-clerk.js +2 -2
  167. package/lib/zudoku.auth-openid.js +124 -138
  168. package/lib/zudoku.auth-openid.js.map +1 -1
  169. package/lib/zudoku.components.js +1133 -992
  170. package/lib/zudoku.components.js.map +1 -1
  171. package/lib/zudoku.openapi-worker.js +10 -16346
  172. package/lib/zudoku.openapi-worker.js.map +1 -1
  173. package/lib/zudoku.plugin-api-keys.js +18 -18
  174. package/lib/zudoku.plugin-custom-pages.js +2 -2
  175. package/lib/zudoku.plugin-markdown.js +1 -1
  176. package/lib/zudoku.plugin-openapi.js +5 -9
  177. package/lib/zudoku.plugin-openapi.js.map +1 -1
  178. package/lib/zudoku.plugin-redirect.js +1 -1
  179. package/package.json +14 -4
  180. package/src/app/demo.tsx +0 -3
  181. package/src/app/entry.client.tsx +14 -0
  182. package/src/app/entry.server.tsx +59 -57
  183. package/src/app/standalone.tsx +0 -3
  184. package/src/lib/authentication/hook.ts +1 -3
  185. package/src/lib/authentication/providers/auth0.tsx +16 -11
  186. package/src/lib/authentication/providers/openid.tsx +12 -30
  187. package/src/lib/authentication/state.ts +44 -10
  188. package/src/lib/components/Bootstrap.tsx +36 -9
  189. package/src/lib/components/DeveloperHint.tsx +6 -1
  190. package/src/lib/components/Header.tsx +31 -42
  191. package/src/lib/components/Layout.tsx +48 -36
  192. package/src/lib/components/MobileTopNavigation.tsx +15 -18
  193. package/src/lib/components/SyntaxHighlight.tsx +81 -46
  194. package/src/lib/components/ThemeSwitch.tsx +26 -0
  195. package/src/lib/components/TopNavigation.tsx +27 -19
  196. package/src/lib/components/Zudoku.tsx +5 -10
  197. package/src/lib/components/context/ZudokuContext.ts +8 -13
  198. package/src/lib/components/navigation/Sidebar.tsx +3 -3
  199. package/src/lib/components/navigation/utils.ts +2 -2
  200. package/src/lib/core/ZudokuContext.ts +0 -8
  201. package/src/lib/errors/ErrorAlert.tsx +2 -1
  202. package/src/lib/plugins/openapi/ColorizedParam.tsx +23 -14
  203. package/src/lib/plugins/openapi/Endpoint.tsx +5 -10
  204. package/src/lib/plugins/openapi/OperationList.tsx +5 -40
  205. package/src/lib/plugins/openapi/Route.tsx +11 -12
  206. package/src/lib/plugins/openapi/Sidecar.tsx +10 -13
  207. package/src/lib/plugins/openapi/client/GraphQLClient.tsx +140 -0
  208. package/src/lib/plugins/openapi/client/GraphQLContext.tsx +16 -0
  209. package/src/lib/plugins/openapi/client/createServer.ts +2 -0
  210. package/src/lib/plugins/openapi/client/useCreateQuery.ts +18 -0
  211. package/src/lib/plugins/openapi/client/worker.ts +38 -24
  212. package/src/lib/plugins/openapi/graphql/fragment-masking.ts +11 -18
  213. package/src/lib/plugins/openapi/graphql/gql.ts +7 -25
  214. package/src/lib/plugins/openapi/graphql/graphql.ts +351 -782
  215. package/src/lib/plugins/openapi/index.tsx +40 -63
  216. package/src/lib/plugins/openapi/schema/SchemaView.tsx +1 -1
  217. package/src/lib/plugins/openapi-worker.ts +11 -1
  218. package/dist/lib/components/context/ThemeContext.d.ts +0 -2
  219. package/dist/lib/components/context/ThemeContext.js +0 -7
  220. package/dist/lib/components/context/ThemeContext.js.map +0 -1
  221. package/dist/lib/components/context/ThemeProvider.d.ts +0 -4
  222. package/dist/lib/components/context/ThemeProvider.js +0 -23
  223. package/dist/lib/components/context/ThemeProvider.js.map +0 -1
  224. package/dist/lib/plugins/openapi/client/createMemoryClient.d.ts +0 -9
  225. package/dist/lib/plugins/openapi/client/createMemoryClient.js +0 -54
  226. package/dist/lib/plugins/openapi/client/createMemoryClient.js.map +0 -1
  227. package/dist/lib/plugins/openapi/client/createWorkerClient.d.ts +0 -10
  228. package/dist/lib/plugins/openapi/client/createWorkerClient.js +0 -62
  229. package/dist/lib/plugins/openapi/client/createWorkerClient.js.map +0 -1
  230. package/dist/lib/plugins/openapi/client/interfaces.d.ts +0 -4
  231. package/dist/lib/plugins/openapi/client/interfaces.js +0 -2
  232. package/dist/lib/plugins/openapi/client/interfaces.js.map +0 -1
  233. package/dist/lib/themeToggle.d.ts +0 -1
  234. package/dist/lib/themeToggle.js +0 -7
  235. package/dist/lib/themeToggle.js.map +0 -1
  236. package/dist/lib/util/createWaitForNotify.d.ts +0 -1
  237. package/dist/lib/util/createWaitForNotify.js +0 -15
  238. package/dist/lib/util/createWaitForNotify.js.map +0 -1
  239. package/dist/vite/plugin-html-transform.d.ts +0 -2
  240. package/dist/vite/plugin-html-transform.js +0 -15
  241. package/dist/vite/plugin-html-transform.js.map +0 -1
  242. package/lib/DeveloperHint-DHdLXGHA.js +0 -16
  243. package/lib/DeveloperHint-DHdLXGHA.js.map +0 -1
  244. package/lib/Markdown-D6UxMbZm.js +0 -18059
  245. package/lib/Markdown-D6UxMbZm.js.map +0 -1
  246. package/lib/OperationList-BHUBGM0c.js +0 -621
  247. package/lib/OperationList-BHUBGM0c.js.map +0 -1
  248. package/lib/Route-B0XuN1oC.js +0 -13
  249. package/lib/Route-B0XuN1oC.js.map +0 -1
  250. package/lib/Select-DYKDahHt.js.map +0 -1
  251. package/lib/SidebarBadge-Bbt92M5K.js +0 -38
  252. package/lib/SidebarBadge-Bbt92M5K.js.map +0 -1
  253. package/lib/SlotletProvider-mhjLPG44.js +0 -241
  254. package/lib/SlotletProvider-mhjLPG44.js.map +0 -1
  255. package/lib/Spinner-ChOGyPls.js.map +0 -1
  256. package/lib/StaggeredRender-DDHSzQKE.js +0 -17
  257. package/lib/StaggeredRender-DDHSzQKE.js.map +0 -1
  258. package/lib/hook-CjQERPa7.js.map +0 -1
  259. package/lib/index-BRg5pi5D.js +0 -5902
  260. package/lib/index-BRg5pi5D.js.map +0 -1
  261. package/lib/index-DM9hrcCG.js +0 -1783
  262. package/lib/index-DM9hrcCG.js.map +0 -1
  263. package/lib/state-BsPrOUAh.js +0 -252
  264. package/lib/state-BsPrOUAh.js.map +0 -1
  265. package/lib/urql-core-35Qt_U4i.js +0 -1511
  266. package/lib/urql-core-35Qt_U4i.js.map +0 -1
  267. package/lib/useExposedProps-BxyHjPNN.js +0 -9
  268. package/lib/utils-DNAltzXc.js.map +0 -1
  269. package/src/lib/components/context/ThemeContext.tsx +0 -8
  270. package/src/lib/components/context/ThemeProvider.tsx +0 -27
  271. package/src/lib/plugins/openapi/client/createMemoryClient.ts +0 -65
  272. package/src/lib/plugins/openapi/client/createWorkerClient.ts +0 -79
  273. package/src/lib/plugins/openapi/client/interfaces.ts +0 -5
  274. package/src/lib/themeToggle.ts +0 -7
  275. package/src/lib/util/createWaitForNotify.ts +0 -18
@@ -1,5 +1,10 @@
1
+ import {
2
+ HydrationBoundary,
3
+ QueryClient,
4
+ QueryClientProvider,
5
+ } from "@tanstack/react-query";
1
6
  import { type HelmetData, HelmetProvider } from "@zudoku/react-helmet-async";
2
- import { StrictMode } from "react";
7
+ import { StrictMode, useState } from "react";
3
8
  import { type createBrowserRouter, RouterProvider } from "react-router-dom";
4
9
  import {
5
10
  type createStaticRouter,
@@ -15,13 +20,31 @@ const Bootstrap = ({
15
20
  hydrate?: boolean;
16
21
  router: ReturnType<typeof createBrowserRouter>;
17
22
  }) => {
23
+ const [queryClient] = useState(
24
+ () =>
25
+ new QueryClient({
26
+ defaultOptions: {
27
+ queries: {
28
+ staleTime: 1000 * 60 * 5,
29
+ },
30
+ },
31
+ }),
32
+ );
33
+
18
34
  return (
19
35
  <StrictMode>
20
- <HelmetProvider>
21
- <StaggeredRenderContext.Provider value={{ stagger: !hydrate }}>
22
- <RouterProvider router={router} />
23
- </StaggeredRenderContext.Provider>
24
- </HelmetProvider>
36
+ <QueryClientProvider client={queryClient}>
37
+ <HydrationBoundary state={hydrate ? (window as any).DATA : undefined}>
38
+ <HelmetProvider>
39
+ <StaggeredRenderContext.Provider value={{ stagger: !hydrate }}>
40
+ <RouterProvider
41
+ router={router}
42
+ future={{ v7_startTransition: true }}
43
+ />
44
+ </StaggeredRenderContext.Provider>
45
+ </HelmetProvider>
46
+ </HydrationBoundary>
47
+ </QueryClientProvider>
25
48
  </StrictMode>
26
49
  );
27
50
  };
@@ -29,16 +52,20 @@ const Bootstrap = ({
29
52
  const BootstrapStatic = ({
30
53
  router,
31
54
  context,
55
+ queryClient,
32
56
  helmetContext,
33
57
  }: {
34
58
  helmetContext: HelmetData["context"];
35
59
  context: StaticHandlerContext;
60
+ queryClient: QueryClient;
36
61
  router: ReturnType<typeof createStaticRouter>;
37
62
  }) => (
38
63
  <StrictMode>
39
- <HelmetProvider context={helmetContext}>
40
- <StaticRouterProvider router={router} context={context} />
41
- </HelmetProvider>
64
+ <QueryClientProvider client={queryClient}>
65
+ <HelmetProvider context={helmetContext}>
66
+ <StaticRouterProvider router={router} context={context} />
67
+ </HelmetProvider>
68
+ </QueryClientProvider>
42
69
  </StrictMode>
43
70
  );
44
71
 
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { Callout } from "../ui/Callout.js";
3
+ import { Markdown } from "./Markdown.js";
3
4
 
4
5
  export const DeveloperHint = ({
5
6
  children,
@@ -13,7 +14,11 @@ export const DeveloperHint = ({
13
14
  return (
14
15
  <Callout type="caution" title="Developer hint" className={className}>
15
16
  <div className="flex flex-col gap-2">
16
- <div>{children}</div>
17
+ {typeof children === "string" ? (
18
+ <Markdown content={children} />
19
+ ) : (
20
+ <div>{children}</div>
21
+ )}
17
22
  <small className="italic">
18
23
  Note: This hint is only shown in development mode.
19
24
  </small>
@@ -1,4 +1,3 @@
1
- import { MoonStarIcon, SunIcon } from "lucide-react";
2
1
  import { memo } from "react";
3
2
  import { Link } from "react-router-dom";
4
3
  import { Button } from "zudoku/ui/Button.js";
@@ -17,15 +16,14 @@ import {
17
16
  DropdownMenuSubTrigger,
18
17
  DropdownMenuTrigger,
19
18
  } from "../ui/DropdownMenu.js";
20
- import { cn } from "../util/cn.js";
21
19
  import { joinPath } from "../util/joinPath.js";
22
20
  import { Banner } from "./Banner.js";
23
21
  import { ClientOnly } from "./ClientOnly.js";
24
- import { useTheme } from "./context/ThemeContext.js";
25
22
  import { useZudoku } from "./context/ZudokuContext.js";
26
23
  import { MobileTopNavigation } from "./MobileTopNavigation.js";
27
24
  import { Search } from "./Search.js";
28
25
  import { Slotlet } from "./SlotletProvider.js";
26
+ import { ThemeSwitch } from "./ThemeSwitch.js";
29
27
  import { TopNavigation } from "./TopNavigation.js";
30
28
 
31
29
  const RecursiveMenu = ({ item }: { item: ProfileNavigationItem }) => {
@@ -50,7 +48,6 @@ const RecursiveMenu = ({ item }: { item: ProfileNavigationItem }) => {
50
48
 
51
49
  export const Header = memo(function HeaderInner() {
52
50
  const auth = useAuth();
53
- const [isDark, toggleTheme] = useTheme();
54
51
  const { isAuthenticated, profile, isAuthEnabled } = useAuth();
55
52
  const context = useZudoku();
56
53
  const { page, plugins } = context;
@@ -60,8 +57,6 @@ export const Header = memo(function HeaderInner() {
60
57
  .flatMap((p) => p.getProfileMenuItems(context))
61
58
  .map((i) => <RecursiveMenu key={i.label} item={i} />);
62
59
 
63
- const ThemeIcon = isDark ? MoonStarIcon : SunIcon;
64
-
65
60
  return (
66
61
  <header className="sticky lg:top-0 z-10 bg-background/80 backdrop-blur w-full">
67
62
  <Banner />
@@ -83,10 +78,11 @@ export const Header = memo(function HeaderInner() {
83
78
  }
84
79
  alt={page.logo.alt ?? page.pageTitle}
85
80
  style={{ width: page.logo.width }}
86
- className={cn("h-10", isDark && "hidden")}
81
+ className="h-10 dark:hidden"
87
82
  loading="lazy"
88
83
  />
89
84
  <img
85
+ data-hide-on-theme="light"
90
86
  src={
91
87
  /https?:\/\//.test(page.logo.src.dark)
92
88
  ? page.logo.src.dark
@@ -97,7 +93,7 @@ export const Header = memo(function HeaderInner() {
97
93
  }
98
94
  alt={page.logo.alt ?? page.pageTitle}
99
95
  style={{ width: page.logo.width }}
100
- className={cn("h-10", !isDark && "hidden")}
96
+ className="h-10"
101
97
  loading="lazy"
102
98
  />
103
99
  </>
@@ -116,40 +112,33 @@ export const Header = memo(function HeaderInner() {
116
112
  <MobileTopNavigation />
117
113
  <div className="hidden lg:flex items-center justify-self-end text-sm gap-2">
118
114
  <Slotlet name="head-navigation-start" />
119
- <ClientOnly
120
- fallback={<Skeleton className="rounded h-5 w-24 mr-4" />}
121
- >
122
- {isAuthEnabled && !isAuthenticated ? (
123
- <Button variant="ghost" onClick={() => auth.login()}>
124
- Login
125
- </Button>
126
- ) : (
127
- accountItems.length > 0 && (
128
- <DropdownMenu modal={false}>
129
- <DropdownMenuTrigger asChild>
130
- <Button variant="ghost">
131
- {profile?.email ? `${profile.email}` : "My Account"}
132
- </Button>
133
- </DropdownMenuTrigger>
134
- <DropdownMenuContent className="w-56">
135
- <DropdownMenuLabel>My Account</DropdownMenuLabel>
136
- <DropdownMenuSeparator />
137
- {accountItems}
138
- </DropdownMenuContent>
139
- </DropdownMenu>
140
- )
141
- )}
142
- </ClientOnly>
143
- <Button
144
- variant="ghost"
145
- aria-label={
146
- isDark ? "Switch to light mode" : "Switch to dark mode"
147
- }
148
- className="p-2.5 -m-2.5 rounded-full"
149
- onClick={toggleTheme}
150
- >
151
- <ThemeIcon size={18} />
152
- </Button>
115
+ {isAuthEnabled && (
116
+ <ClientOnly
117
+ fallback={<Skeleton className="rounded h-5 w-24 mr-4" />}
118
+ >
119
+ {!isAuthenticated ? (
120
+ <Button variant="ghost" onClick={() => auth.login()}>
121
+ Login
122
+ </Button>
123
+ ) : (
124
+ accountItems.length > 0 && (
125
+ <DropdownMenu modal={false}>
126
+ <DropdownMenuTrigger asChild>
127
+ <Button variant="ghost">
128
+ {profile?.email ? `${profile.email}` : "My Account"}
129
+ </Button>
130
+ </DropdownMenuTrigger>
131
+ <DropdownMenuContent className="w-56">
132
+ <DropdownMenuLabel>My Account</DropdownMenuLabel>
133
+ <DropdownMenuSeparator />
134
+ {accountItems}
135
+ </DropdownMenuContent>
136
+ </DropdownMenu>
137
+ )
138
+ )}
139
+ </ClientOnly>
140
+ )}
141
+ <ThemeSwitch />
153
142
  <Slotlet name="head-navigation-end" />
154
143
  </div>
155
144
  </div>
@@ -1,7 +1,8 @@
1
1
  import { Helmet } from "@zudoku/react-helmet-async";
2
2
  import { PanelLeftIcon } from "lucide-react";
3
3
  import { Suspense, useEffect, useRef, type ReactNode } from "react";
4
- import { Outlet, useLocation } from "react-router-dom";
4
+ import { Outlet, useLocation, useNavigation } from "react-router-dom";
5
+ import { useSpinDelay } from "spin-delay";
5
6
  import { Drawer, DrawerTrigger } from "../ui/Drawer.js";
6
7
  import { cn } from "../util/cn.js";
7
8
  import { useScrollToAnchor } from "../util/useScrollToAnchor.js";
@@ -13,6 +14,12 @@ import { Sidebar } from "./navigation/Sidebar.js";
13
14
  import { Slotlet } from "./SlotletProvider.js";
14
15
  import { Spinner } from "./Spinner.js";
15
16
 
17
+ const LoadingFallback = () => (
18
+ <main className="grid h-[calc(100vh-var(--header-height))] place-items-center">
19
+ <Spinner />
20
+ </main>
21
+ );
22
+
16
23
  export const Layout = ({ children }: { children?: ReactNode }) => {
17
24
  const location = useLocation();
18
25
  const { setActiveAnchor } = useViewportAnchor();
@@ -36,6 +43,13 @@ export const Layout = ({ children }: { children?: ReactNode }) => {
36
43
  previousLocationPath.current = location.pathname;
37
44
  }, [location.pathname, setActiveAnchor]);
38
45
 
46
+ // Page transition is happening: https://reactrouter.com/start/framework/pending-ui#global-pending-navigation
47
+ const isNavigating = Boolean(useNavigation().location);
48
+ const showSpinner = useSpinDelay(isNavigating, {
49
+ delay: 300,
50
+ minDuration: 500,
51
+ });
52
+
39
53
  return (
40
54
  <>
41
55
  {import.meta.env.MODE === "standalone" && (
@@ -52,41 +66,39 @@ export const Layout = ({ children }: { children?: ReactNode }) => {
52
66
  <Slotlet name="layout-after-head" />
53
67
 
54
68
  <div className="w-full max-w-screen-2xl mx-auto px-10 lg:px-12">
55
- <Suspense
56
- fallback={
57
- <main className="grid h-[calc(100vh-var(--header-height))] place-items-center">
58
- <Spinner />
59
- </main>
60
- }
61
- >
62
- <Drawer direction="left">
63
- <Sidebar />
64
- <div
65
- className={cn(
66
- "lg:hidden -mx-10 px-10 py-2 sticky bg-background/80 backdrop-blur z-10 top-0 left-0 right-0 border-b",
67
- "peer-data-[navigation=false]:hidden",
68
- )}
69
- >
70
- <DrawerTrigger className="flex items-center gap-2">
71
- <PanelLeftIcon size={16} strokeWidth={1.5} />
72
- <span className="text-sm">Menu</span>
73
- </DrawerTrigger>
74
- </div>
75
- <main
76
- className={cn(
77
- "h-full dark:border-white/10 translate-x-0",
78
- "lg:overflow-visible",
79
- // This works in tandem with the `SidebarWrapper` component
80
- "lg:peer-data-[navigation=true]:w-[calc(100%-var(--side-nav-width))]",
81
- "lg:peer-data-[navigation=true]:translate-x-[--side-nav-width] lg:peer-data-[navigation=true]:pl-12",
82
- )}
83
- >
84
- <Slotlet name="zudoku-before-content" />
85
- {children ?? <Outlet />}
86
- <Slotlet name="zudoku-after-content" />
87
- </main>
88
- </Drawer>
89
- </Suspense>
69
+ {showSpinner ? (
70
+ <LoadingFallback />
71
+ ) : (
72
+ <Suspense fallback={<LoadingFallback />}>
73
+ <Drawer direction="left">
74
+ <Sidebar />
75
+ <div
76
+ className={cn(
77
+ "lg:hidden -mx-10 px-10 py-2 sticky bg-background/80 backdrop-blur z-10 top-0 left-0 right-0 border-b",
78
+ "peer-data-[navigation=false]:hidden",
79
+ )}
80
+ >
81
+ <DrawerTrigger className="flex items-center gap-2">
82
+ <PanelLeftIcon size={16} strokeWidth={1.5} />
83
+ <span className="text-sm">Menu</span>
84
+ </DrawerTrigger>
85
+ </div>
86
+ <main
87
+ className={cn(
88
+ "h-full dark:border-white/10 translate-x-0",
89
+ "lg:overflow-visible",
90
+ // This works in tandem with the `SidebarWrapper` component
91
+ "lg:peer-data-[navigation=true]:w-[calc(100%-var(--side-nav-width))]",
92
+ "lg:peer-data-[navigation=true]:translate-x-[--side-nav-width] lg:peer-data-[navigation=true]:pl-12",
93
+ )}
94
+ >
95
+ <Slotlet name="zudoku-before-content" />
96
+ {children ?? <Outlet />}
97
+ <Slotlet name="zudoku-after-content" />
98
+ </main>
99
+ </Drawer>
100
+ </Suspense>
101
+ )}
90
102
  </div>
91
103
  </>
92
104
  );
@@ -1,25 +1,29 @@
1
1
  import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
2
- import { cx } from "class-variance-authority";
3
2
  import { MenuIcon } from "lucide-react";
4
- import { NavLink } from "react-router-dom";
3
+ import { useState } from "react";
5
4
  import { useAuth } from "../authentication/hook.js";
6
5
  import {
7
6
  Drawer,
8
- DrawerClose,
9
7
  DrawerContent,
10
8
  DrawerTitle,
11
9
  DrawerTrigger,
12
10
  } from "../ui/Drawer.js";
13
11
  import { useZudoku } from "./context/ZudokuContext.js";
14
12
  import { Search } from "./Search.js";
15
- import { isHiddenItem } from "./TopNavigation.js";
13
+ import { ThemeSwitch } from "./ThemeSwitch.js";
14
+ import { isHiddenItem, TopNavItem } from "./TopNavigation.js";
16
15
 
17
16
  export const MobileTopNavigation = () => {
18
17
  const { topNavigation } = useZudoku();
19
18
  const { isAuthenticated } = useAuth();
19
+ const [drawerOpen, setDrawerOpen] = useState(false);
20
20
 
21
21
  return (
22
- <Drawer direction="right">
22
+ <Drawer
23
+ direction="right"
24
+ open={drawerOpen}
25
+ onOpenChange={(open) => setDrawerOpen(open)}
26
+ >
23
27
  <div className="flex lg:hidden justify-self-end">
24
28
  <DrawerTrigger className="lg:hidden">
25
29
  <MenuIcon size={22} />
@@ -36,21 +40,14 @@ export const MobileTopNavigation = () => {
36
40
  <Search />
37
41
  </div>
38
42
  <ul className="flex flex-col items-center gap-4 p-4">
43
+ <li>
44
+ <ThemeSwitch />
45
+ </li>
39
46
  {topNavigation.filter(isHiddenItem(isAuthenticated)).map((item) => (
40
47
  <li key={item.label}>
41
- <NavLink
42
- className={({ isActive }) =>
43
- cx(
44
- "block font-medium border-b-2",
45
- isActive
46
- ? "border-primary text-foreground"
47
- : "border-transparent text-foreground/75 hover:text-foreground hover:border-accent-foreground/25",
48
- )
49
- }
50
- to={item.id}
51
- >
52
- <DrawerClose>{item.label}</DrawerClose>
53
- </NavLink>
48
+ <button onClick={() => setDrawerOpen(false)}>
49
+ <TopNavItem {...item} />
50
+ </button>
54
51
  </li>
55
52
  ))}
56
53
  </ul>
@@ -33,9 +33,10 @@ void import("prismjs/components/prism-javascript.min.js");
33
33
  // @ts-expect-error This is untyped
34
34
  void import("prismjs/components/prism-typescript.min.js");
35
35
 
36
+ import { useTheme } from "next-themes";
36
37
  import { useState } from "react";
37
38
  import { cn } from "../util/cn.js";
38
- import { useTheme } from "./context/ThemeContext.js";
39
+ import { ClientOnly } from "./ClientOnly.js";
39
40
 
40
41
  type SyntaxHighlightProps = {
41
42
  className?: string;
@@ -55,72 +56,106 @@ export const SyntaxHighlight = ({
55
56
  language = "plain",
56
57
  ...props
57
58
  }: SyntaxHighlightProps) => {
58
- const [isDark] = useTheme();
59
+ const { resolvedTheme } = useTheme();
59
60
  const [isCopied, setIsCopied] = useState(false);
60
61
 
61
62
  if (!props.code) {
62
63
  return null;
63
64
  }
64
65
 
66
+ const highlightTheme =
67
+ resolvedTheme === "dark" ? themes.vsDark : themes.github;
68
+
69
+ // hardcoded values from the themes to avoid color flash in SSR
70
+ const themeColorClasses =
71
+ "bg-[#f6f8fa] text-[#393a34] dark:bg-[#1e1e1e] dark:text-[#9cdcfe]";
72
+
65
73
  return (
66
- <Highlight
67
- theme={isDark ? themes.vsDark : themes.github}
68
- language={remapLang[language] ?? language}
69
- {...props}
70
- >
71
- {({ className, style, tokens, getLineProps, getTokenProps }) => (
74
+ <ClientOnly
75
+ fallback={
72
76
  <div className="relative group">
73
77
  <pre
74
78
  className={cn(
75
79
  "relative scrollbar overflow-x-auto",
76
- className,
77
80
  props.className,
78
- props.noBackground && "!bg-transparent",
81
+ props.noBackground ? "!bg-transparent" : themeColorClasses,
79
82
  props.wrapLines && "whitespace-pre-wrap break-words",
80
83
  )}
81
- style={style}
82
84
  >
83
- {tokens.map((line, i) => (
84
- // eslint-disable-next-line react/no-array-index-key
85
- <div key={i} {...getLineProps({ line })}>
86
- {line.map((token, key) => (
87
- // eslint-disable-next-line react/no-array-index-key
88
- <span key={key} {...getTokenProps({ token })} />
89
- ))}
90
- </div>
91
- ))}
85
+ {props.code}
92
86
  </pre>
93
87
  {props.showLanguageIndicator && (
94
88
  <span className="absolute top-1.5 right-3 text-[11px] font-mono text-muted-foreground transition group-hover:opacity-0">
95
89
  {language}
96
90
  </span>
97
91
  )}
98
- {copyable && (
99
- <button
100
- type="button"
101
- aria-label="Copy code"
102
- title="Copy code"
103
- className="absolute top-2 right-2 p-2 opacity-0 group-hover:opacity-100 group-hover:bg-zinc-100 group-hover:dark:bg-zinc-700 hover:outline hover:outline-border/75 dark:hover:outline-border rounded-md text-sm text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400 transition"
104
- disabled={isCopied}
105
- onClick={() => {
106
- setIsCopied(true);
107
- void navigator.clipboard.writeText(
108
- tokens
109
- .map((line) => line.map(({ content }) => content).join(""))
110
- .join("\n"),
111
- );
112
- setTimeout(() => setIsCopied(false), 2000);
113
- }}
114
- >
115
- {isCopied ? (
116
- <CheckIcon className="text-emerald-600" size={16} />
117
- ) : (
118
- <CopyIcon size={16} />
119
- )}
120
- </button>
121
- )}
122
92
  </div>
123
- )}
124
- </Highlight>
93
+ }
94
+ >
95
+ <Highlight
96
+ theme={highlightTheme}
97
+ language={remapLang[language] ?? language}
98
+ {...props}
99
+ >
100
+ {({ className, style, tokens, getLineProps, getTokenProps }) => (
101
+ <div className="relative group">
102
+ <pre
103
+ className={cn(
104
+ "relative scrollbar overflow-x-auto",
105
+ className,
106
+ props.className,
107
+ props.noBackground && "!bg-transparent",
108
+ props.wrapLines && "whitespace-pre-wrap break-words",
109
+ )}
110
+ style={style}
111
+ >
112
+ {tokens.map((line, i) => (
113
+ // eslint-disable-next-line react/no-array-index-key
114
+ <div key={i} {...getLineProps({ line })}>
115
+ {line.map((token, key) => (
116
+ // eslint-disable-next-line react/no-array-index-key
117
+ <span key={key} {...getTokenProps({ token })} />
118
+ ))}
119
+ </div>
120
+ ))}
121
+ </pre>
122
+ {props.showLanguageIndicator && (
123
+ <span className="absolute top-1.5 right-3 text-[11px] font-mono text-muted-foreground transition group-hover:opacity-0">
124
+ {language}
125
+ </span>
126
+ )}
127
+ {copyable && (
128
+ <button
129
+ type="button"
130
+ aria-label="Copy code"
131
+ title="Copy code"
132
+ className="absolute top-2 right-2 p-2 opacity-0 group-hover:opacity-100 group-hover:bg-zinc-100 group-hover:dark:bg-zinc-700 hover:outline hover:outline-border/75 dark:hover:outline-border rounded-md text-sm text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400 transition"
133
+ disabled={isCopied}
134
+ onClick={() => {
135
+ setIsCopied(true);
136
+ void navigator.clipboard.writeText(
137
+ tokens
138
+ .map((l) => l.map(({ content }) => content).join(""))
139
+ .join("\n"),
140
+ );
141
+ setTimeout(() => setIsCopied(false), 2000);
142
+ }}
143
+ >
144
+ {isCopied ? (
145
+ <CheckIcon
146
+ className="text-emerald-600"
147
+ size={16}
148
+ strokeWidth={2.5}
149
+ absoluteStrokeWidth
150
+ />
151
+ ) : (
152
+ <CopyIcon size={16} />
153
+ )}
154
+ </button>
155
+ )}
156
+ </div>
157
+ )}
158
+ </Highlight>
159
+ </ClientOnly>
125
160
  );
126
161
  };
@@ -0,0 +1,26 @@
1
+ import { MoonStarIcon, SunIcon } from "lucide-react";
2
+ import { useTheme } from "next-themes";
3
+ import { Button } from "zudoku/ui/Button.js";
4
+ import { ClientOnly } from "./ClientOnly.js";
5
+
6
+ export const ThemeSwitch = () => {
7
+ const { resolvedTheme, setTheme } = useTheme();
8
+ const ThemeIcon = resolvedTheme === "dark" ? MoonStarIcon : SunIcon;
9
+
10
+ return (
11
+ <ClientOnly>
12
+ <Button
13
+ variant="ghost"
14
+ aria-label={
15
+ resolvedTheme === "dark"
16
+ ? "Switch to light mode"
17
+ : "Switch to dark mode"
18
+ }
19
+ className="p-2.5 -m-2.5 rounded-full"
20
+ onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
21
+ >
22
+ <ThemeIcon size={18} />
23
+ </Button>
24
+ </ClientOnly>
25
+ );
26
+ };
@@ -1,8 +1,9 @@
1
1
  import { cx } from "class-variance-authority";
2
2
  import { Suspense } from "react";
3
- import { Link } from "react-router-dom";
4
- import { useAuth } from "../authentication/hook.js";
3
+ import { NavLink, useNavigation } from "react-router-dom";
5
4
  import { TopNavigationItem } from "../../config/validators/validate.js";
5
+ import { useAuth } from "../authentication/hook.js";
6
+ import { ZudokuError } from "../util/invariant.js";
6
7
  import { joinPath } from "../util/joinPath.js";
7
8
  import { useCurrentNavigation, useZudoku } from "./context/ZudokuContext.js";
8
9
  import { traverseSidebar } from "./navigation/utils.js";
@@ -32,7 +33,7 @@ export const TopNavigation = () => {
32
33
  <nav className="hidden lg:block border-b text-sm px-12 h-[--top-nav-height]">
33
34
  <ul className="flex flex-row items-center gap-8">
34
35
  {topNavigation.filter(isHiddenItem(isAuthenticated)).map((item) => (
35
- <li key={item.id}>
36
+ <li key={item.id}>
36
37
  <TopNavItem {...item} />
37
38
  </li>
38
39
  ))}
@@ -42,10 +43,16 @@ export const TopNavigation = () => {
42
43
  );
43
44
  };
44
45
 
45
- const TopNavItem = ({ id, label, default: defaultLink }: TopNavigationItem) => {
46
+ export const TopNavItem = ({
47
+ id,
48
+ label,
49
+ default: defaultLink,
50
+ }: TopNavigationItem) => {
46
51
  const { sidebars } = useZudoku();
47
- const nav = useCurrentNavigation();
48
52
  const currentSidebar = sidebars[id];
53
+ const currentNav = useCurrentNavigation();
54
+ const isNavigating = Boolean(useNavigation().location);
55
+ const isActive = currentNav.topNavItem?.id === id && !isNavigating;
49
56
 
50
57
  // TODO: This is a bit of a hack to get the first link in the sidebar
51
58
  // We should really process this when we load the config so we can validate
@@ -60,25 +67,26 @@ const TopNavItem = ({ id, label, default: defaultLink }: TopNavigationItem) => {
60
67
  : joinPath(id));
61
68
 
62
69
  if (!first) {
63
- throw new Error(
64
- `No links found in top navigation for top navigation '${id}'. Check that the sidebar isn't empty or that a default link set.`,
65
- );
70
+ throw new ZudokuError("Page not found.", {
71
+ developerHint: `No links found in top navigation for '${id}'. Check that the sidebar isn't empty or that a default link is set.`,
72
+ });
66
73
  }
67
74
 
68
- // Manually set the active sidebar based on our logic of what is active
69
- const isActive = nav.data.topNavItem?.id === id;
70
-
71
75
  return (
72
- <Link
73
- className={cx(
74
- "block py-3.5 font-medium -mb-px border-b-2",
75
- isActive
76
- ? "border-primary text-foreground"
77
- : "border-transparent text-foreground/75 hover:text-foreground hover:border-accent-foreground/25",
78
- )}
76
+ // We don't use isActive here because it has to be inside the sidebar,
77
+ // the top nav id doesn't necessarily start with the sidebar id
78
+ <NavLink
79
+ className={({ isPending }) =>
80
+ cx(
81
+ "block lg:py-3.5 font-medium -mb-px border-b-2",
82
+ isActive || isPending
83
+ ? "border-primary text-foreground"
84
+ : "border-transparent text-foreground/75 hover:text-foreground hover:border-accent-foreground/25",
85
+ )
86
+ }
79
87
  to={first}
80
88
  >
81
89
  {label}
82
- </Link>
90
+ </NavLink>
83
91
  );
84
92
  };