zudoku 0.0.0-f9d5b02 → 0.0.0-fa903e7

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 (183) hide show
  1. package/dist/app/entry.client.js +14 -0
  2. package/dist/app/entry.client.js.map +1 -1
  3. package/dist/cli/common/logger.js +9 -0
  4. package/dist/cli/common/logger.js.map +1 -1
  5. package/dist/config/validators/validate.d.ts +31 -11
  6. package/dist/config/validators/validate.js +7 -1
  7. package/dist/config/validators/validate.js.map +1 -1
  8. package/dist/lib/authentication/AuthenticationPlugin.d.ts +4 -2
  9. package/dist/lib/authentication/AuthenticationPlugin.js +3 -0
  10. package/dist/lib/authentication/AuthenticationPlugin.js.map +1 -1
  11. package/dist/lib/authentication/authentication.d.ts +1 -1
  12. package/dist/lib/authentication/hook.d.ts +5 -4
  13. package/dist/lib/authentication/hook.js +1 -3
  14. package/dist/lib/authentication/hook.js.map +1 -1
  15. package/dist/lib/authentication/providers/auth0.js +2 -2
  16. package/dist/lib/authentication/providers/auth0.js.map +1 -1
  17. package/dist/lib/authentication/providers/openid.d.ts +0 -1
  18. package/dist/lib/authentication/providers/openid.js +11 -26
  19. package/dist/lib/authentication/providers/openid.js.map +1 -1
  20. package/dist/lib/authentication/state.d.ts +25 -4
  21. package/dist/lib/authentication/state.js +28 -5
  22. package/dist/lib/authentication/state.js.map +1 -1
  23. package/dist/lib/components/Bootstrap.js +9 -5
  24. package/dist/lib/components/Bootstrap.js.map +1 -1
  25. package/dist/lib/components/Header.js +12 -4
  26. package/dist/lib/components/Header.js.map +1 -1
  27. package/dist/lib/components/Layout.js +12 -4
  28. package/dist/lib/components/Layout.js.map +1 -1
  29. package/dist/lib/components/MobileTopNavigation.js +5 -7
  30. package/dist/lib/components/MobileTopNavigation.js.map +1 -1
  31. package/dist/lib/components/SyntaxHighlight.js +2 -2
  32. package/dist/lib/components/SyntaxHighlight.js.map +1 -1
  33. package/dist/lib/components/ThemeSwitch.js +5 -3
  34. package/dist/lib/components/ThemeSwitch.js.map +1 -1
  35. package/dist/lib/components/TopNavigation.d.ts +2 -0
  36. package/dist/lib/components/TopNavigation.js +13 -7
  37. package/dist/lib/components/TopNavigation.js.map +1 -1
  38. package/dist/lib/components/index.d.ts +11 -1
  39. package/dist/lib/core/plugins.d.ts +6 -0
  40. package/dist/lib/core/plugins.js.map +1 -1
  41. package/dist/lib/oas/parser/upgrade/index.d.ts +2 -2
  42. package/dist/lib/oas/parser/upgrade/index.js +3 -20
  43. package/dist/lib/oas/parser/upgrade/index.js.map +1 -1
  44. package/dist/lib/plugins/api-keys/index.js +3 -0
  45. package/dist/lib/plugins/api-keys/index.js.map +1 -1
  46. package/dist/lib/plugins/markdown/MdxPage.d.ts +9 -1
  47. package/dist/lib/plugins/markdown/MdxPage.js +14 -1
  48. package/dist/lib/plugins/markdown/MdxPage.js.map +1 -1
  49. package/dist/lib/plugins/markdown/index.js +1 -1
  50. package/dist/lib/plugins/markdown/index.js.map +1 -1
  51. package/dist/lib/plugins/openapi/ColorizedParam.js +2 -2
  52. package/dist/lib/plugins/openapi/ColorizedParam.js.map +1 -1
  53. package/dist/lib/plugins/openapi/client/GraphQLClient.js +12 -0
  54. package/dist/lib/plugins/openapi/client/GraphQLClient.js.map +1 -1
  55. package/dist/lib/plugins/openapi/client/useCreateQuery.d.ts +1 -1
  56. package/dist/lib/plugins/openapi/client/useCreateQuery.js +4 -2
  57. package/dist/lib/plugins/openapi/client/useCreateQuery.js.map +1 -1
  58. package/dist/lib/plugins/openapi/interfaces.d.ts +1 -1
  59. package/dist/lib/plugins/openapi/post-processors/removeExtensions.d.ts +6 -0
  60. package/dist/lib/plugins/openapi/post-processors/removeExtensions.js +14 -0
  61. package/dist/lib/plugins/openapi/post-processors/removeExtensions.js.map +1 -0
  62. package/dist/lib/plugins/openapi/post-processors/removeExtensions.test.d.ts +1 -0
  63. package/dist/lib/plugins/openapi/post-processors/removeExtensions.test.js +125 -0
  64. package/dist/lib/plugins/openapi/post-processors/removeExtensions.test.js.map +1 -0
  65. package/dist/lib/plugins/openapi/post-processors/removePaths.d.ts +11 -0
  66. package/dist/lib/plugins/openapi/post-processors/removePaths.js +33 -0
  67. package/dist/lib/plugins/openapi/post-processors/removePaths.js.map +1 -0
  68. package/dist/lib/plugins/openapi/post-processors/removePaths.test.d.ts +1 -0
  69. package/dist/lib/plugins/openapi/post-processors/removePaths.test.js +104 -0
  70. package/dist/lib/plugins/openapi/post-processors/removePaths.test.js.map +1 -0
  71. package/dist/lib/plugins/openapi/post-processors/traverse.d.ts +1 -0
  72. package/dist/lib/plugins/openapi/post-processors/traverse.js +2 -0
  73. package/dist/lib/plugins/openapi/post-processors/traverse.js.map +1 -0
  74. package/dist/lib/plugins/openapi/schema/SchemaView.js.map +1 -1
  75. package/dist/lib/util/traverse.d.ts +2 -0
  76. package/dist/lib/util/traverse.js +18 -0
  77. package/dist/lib/util/traverse.js.map +1 -0
  78. package/dist/vite/config.js +12 -0
  79. package/dist/vite/config.js.map +1 -1
  80. package/dist/vite/config.test.js +3 -4
  81. package/dist/vite/config.test.js.map +1 -1
  82. package/dist/vite/output.js +20 -0
  83. package/dist/vite/output.js.map +1 -1
  84. package/dist/vite/plugin-api.js +23 -19
  85. package/dist/vite/plugin-api.js.map +1 -1
  86. package/dist/vite/plugin-component.js +14 -19
  87. package/dist/vite/plugin-component.js.map +1 -1
  88. package/dist/vite/plugin-docs.test.js +15 -23
  89. package/dist/vite/plugin-docs.test.js.map +1 -1
  90. package/dist/vite/plugin-mdx.js +10 -3
  91. package/dist/vite/plugin-mdx.js.map +1 -1
  92. package/dist/vite/remarkStaticGeneration.js +5 -5
  93. package/dist/vite/remarkStaticGeneration.js.map +1 -1
  94. package/dist/zuplo/with-zuplo.d.ts +3 -0
  95. package/dist/zuplo/with-zuplo.js +28 -0
  96. package/dist/zuplo/with-zuplo.js.map +1 -0
  97. package/lib/AuthenticationPlugin-D0Em0SwR.js +59 -0
  98. package/lib/AuthenticationPlugin-D0Em0SwR.js.map +1 -0
  99. package/lib/{Markdown-BorQdbxW.js → Markdown-ievDDhFT.js} +2 -2
  100. package/lib/{Markdown-BorQdbxW.js.map → Markdown-ievDDhFT.js.map} +1 -1
  101. package/lib/MdxPage-B2FpJ9KC.js +183 -0
  102. package/lib/MdxPage-B2FpJ9KC.js.map +1 -0
  103. package/lib/{OperationList-KshJrrLL.js → OperationList-BkNQEsNs.js} +460 -458
  104. package/lib/OperationList-BkNQEsNs.js.map +1 -0
  105. package/lib/{Select-DP74t8Yy.js → Select-O9ZM3ZgX.js} +2 -2
  106. package/lib/{Select-DP74t8Yy.js.map → Select-O9ZM3ZgX.js.map} +1 -1
  107. package/lib/{SlotletProvider-D2v6rJy1.js → SlotletProvider-DyomlzGx.js} +2 -2
  108. package/lib/{SlotletProvider-D2v6rJy1.js.map → SlotletProvider-DyomlzGx.js.map} +1 -1
  109. package/lib/{SyntaxHighlight-CBmwwKoM.js → SyntaxHighlight-DkLOsjHS.js} +2 -2
  110. package/lib/{SyntaxHighlight-CBmwwKoM.js.map → SyntaxHighlight-DkLOsjHS.js.map} +1 -1
  111. package/lib/assets/{worker-CPsGZsve.js → worker-BHClFO3A.js} +434 -438
  112. package/lib/assets/worker-BHClFO3A.js.map +1 -0
  113. package/lib/{createServer-DK-g7kbB.js → createServer-CpJlUPtn.js} +4457 -5247
  114. package/lib/createServer-CpJlUPtn.js.map +1 -0
  115. package/lib/{hook-Diu0rqp-.js → hook-hEqe7fPB.js} +12 -14
  116. package/lib/{hook-Diu0rqp-.js.map → hook-hEqe7fPB.js.map} +1 -1
  117. package/lib/{index-BcesIHH4.js → index-C7SaIME0.js} +54 -50
  118. package/lib/index-C7SaIME0.js.map +1 -0
  119. package/lib/object_hash-CvlLgU-M.js +785 -0
  120. package/lib/object_hash-CvlLgU-M.js.map +1 -0
  121. package/lib/post-processors/removeExtensions.js +11 -0
  122. package/lib/post-processors/removeExtensions.js.map +1 -0
  123. package/lib/post-processors/removePaths.js +28 -0
  124. package/lib/post-processors/removePaths.js.map +1 -0
  125. package/lib/post-processors/traverse.js +12 -0
  126. package/lib/post-processors/traverse.js.map +1 -0
  127. package/lib/state-tsXBLONe.js +203 -0
  128. package/lib/state-tsXBLONe.js.map +1 -0
  129. package/lib/zudoku.auth-auth0.js +9 -8
  130. package/lib/zudoku.auth-auth0.js.map +1 -1
  131. package/lib/zudoku.auth-clerk.js +2 -2
  132. package/lib/zudoku.auth-openid.js +119 -133
  133. package/lib/zudoku.auth-openid.js.map +1 -1
  134. package/lib/zudoku.components.js +596 -530
  135. package/lib/zudoku.components.js.map +1 -1
  136. package/lib/zudoku.openapi-worker.js +1 -1
  137. package/lib/zudoku.plugin-api-keys.js +40 -38
  138. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  139. package/lib/zudoku.plugin-custom-pages.js +1 -1
  140. package/lib/zudoku.plugin-markdown.js +15 -14
  141. package/lib/zudoku.plugin-markdown.js.map +1 -1
  142. package/lib/zudoku.plugin-openapi.js +2 -2
  143. package/package.json +17 -6
  144. package/src/app/entry.client.tsx +14 -0
  145. package/src/lib/authentication/AuthenticationPlugin.tsx +4 -1
  146. package/src/lib/authentication/authentication.ts +1 -1
  147. package/src/lib/authentication/hook.ts +1 -3
  148. package/src/lib/authentication/providers/auth0.tsx +3 -2
  149. package/src/lib/authentication/providers/openid.tsx +12 -30
  150. package/src/lib/authentication/state.ts +44 -10
  151. package/src/lib/components/Bootstrap.tsx +25 -18
  152. package/src/lib/components/Header.tsx +42 -9
  153. package/src/lib/components/Layout.tsx +49 -37
  154. package/src/lib/components/MobileTopNavigation.tsx +11 -18
  155. package/src/lib/components/SyntaxHighlight.tsx +3 -2
  156. package/src/lib/components/ThemeSwitch.tsx +6 -4
  157. package/src/lib/components/TopNavigation.tsx +25 -17
  158. package/src/lib/core/plugins.ts +8 -0
  159. package/src/lib/oas/parser/upgrade/index.ts +4 -27
  160. package/src/lib/plugins/api-keys/index.tsx +3 -0
  161. package/src/lib/plugins/markdown/MdxPage.tsx +25 -1
  162. package/src/lib/plugins/markdown/index.tsx +1 -0
  163. package/src/lib/plugins/openapi/ColorizedParam.tsx +2 -2
  164. package/src/lib/plugins/openapi/client/GraphQLClient.tsx +17 -0
  165. package/src/lib/plugins/openapi/client/useCreateQuery.ts +5 -2
  166. package/src/lib/plugins/openapi/interfaces.ts +1 -1
  167. package/src/lib/plugins/openapi/post-processors/removeExtensions.test.ts +144 -0
  168. package/src/lib/plugins/openapi/post-processors/removeExtensions.ts +24 -0
  169. package/src/lib/plugins/openapi/post-processors/removePaths.test.ts +126 -0
  170. package/src/lib/plugins/openapi/post-processors/removePaths.ts +55 -0
  171. package/src/lib/plugins/openapi/post-processors/traverse.ts +1 -0
  172. package/src/lib/plugins/openapi/schema/SchemaView.tsx +1 -1
  173. package/src/lib/util/traverse.ts +25 -0
  174. package/lib/AuthenticationPlugin-DeGDVa1r.js +0 -56
  175. package/lib/AuthenticationPlugin-DeGDVa1r.js.map +0 -1
  176. package/lib/MdxPage-DFlbtJWi.js +0 -174
  177. package/lib/MdxPage-DFlbtJWi.js.map +0 -1
  178. package/lib/OperationList-KshJrrLL.js.map +0 -1
  179. package/lib/assets/worker-CPsGZsve.js.map +0 -1
  180. package/lib/createServer-DK-g7kbB.js.map +0 -1
  181. package/lib/index-BcesIHH4.js.map +0 -1
  182. package/lib/state-BsPrOUAh.js +0 -252
  183. package/lib/state-BsPrOUAh.js.map +0 -1
@@ -107,7 +107,10 @@ export class OpenIDAuthenticationProvider implements AuthenticationProvider {
107
107
  expiresOn: new Date(Date.now() + response.expires_in * 1000),
108
108
  tokenType: response.token_type,
109
109
  };
110
- sessionStorage.setItem("token-state", JSON.stringify(tokens));
110
+
111
+ useAuthState.setState({
112
+ providerData: tokens,
113
+ });
111
114
  }
112
115
 
113
116
  async signUp({ redirectTo }: { redirectTo?: string } = {}) {
@@ -194,14 +197,14 @@ export class OpenIDAuthenticationProvider implements AuthenticationProvider {
194
197
 
195
198
  async getAccessToken(): Promise<string> {
196
199
  const as = await this.getAuthServer();
197
- const tokenState = sessionStorage.getItem("token-state");
198
- if (!tokenState) {
200
+ const { providerData } = useAuthState.getState();
201
+ if (!providerData) {
199
202
  throw new AuthorizationError("User is not authenticated");
200
203
  }
204
+ const tokenState = providerData as TokenState;
201
205
 
202
- const state = JSON.parse(tokenState) as TokenState;
203
- if (state.expiresOn < new Date()) {
204
- if (!state.refreshToken) {
206
+ if (tokenState.expiresOn < new Date()) {
207
+ if (!tokenState.refreshToken) {
205
208
  await this.signIn();
206
209
  return "";
207
210
  }
@@ -209,7 +212,7 @@ export class OpenIDAuthenticationProvider implements AuthenticationProvider {
209
212
  const request = await oauth.refreshTokenGrantRequest(
210
213
  as,
211
214
  this.client,
212
- state.refreshToken,
215
+ tokenState.refreshToken,
213
216
  );
214
217
  const response = await oauth.processRefreshTokenResponse(
215
218
  as,
@@ -225,7 +228,7 @@ export class OpenIDAuthenticationProvider implements AuthenticationProvider {
225
228
 
226
229
  return response.access_token.toString();
227
230
  } else {
228
- return state.accessToken;
231
+ return tokenState.accessToken;
229
232
  }
230
233
  }
231
234
 
@@ -234,8 +237,8 @@ export class OpenIDAuthenticationProvider implements AuthenticationProvider {
234
237
  isAuthenticated: false,
235
238
  isPending: false,
236
239
  profile: undefined,
240
+ providerData: undefined,
237
241
  });
238
- sessionStorage.clear();
239
242
 
240
243
  const as = await this.getAuthServer();
241
244
 
@@ -342,32 +345,11 @@ export class OpenIDAuthenticationProvider implements AuthenticationProvider {
342
345
  profile,
343
346
  });
344
347
 
345
- sessionStorage.setItem(
346
- "profile-state",
347
- JSON.stringify(useAuthState.getState().profile),
348
- );
349
-
350
348
  const redirectTo = sessionStorage.getItem("redirect-to") ?? "/";
351
349
  sessionStorage.removeItem("redirect-to");
352
350
  return redirectTo;
353
351
  };
354
352
 
355
- pageLoad(): void {
356
- const profileState = sessionStorage.getItem("profile-state");
357
- if (profileState) {
358
- try {
359
- const profile = JSON.parse(profileState);
360
- useAuthState.setState({
361
- isAuthenticated: true,
362
- isPending: false,
363
- profile,
364
- });
365
- } catch (err) {
366
- logger.error("Error parsing auth state", err);
367
- }
368
- }
369
- }
370
-
371
353
  getAuthenticationPlugin() {
372
354
  // TODO: This API is a bit messy, we need to refactor auth plugins/providers
373
355
  // to remove the extra layers of abstraction.
@@ -1,24 +1,58 @@
1
- import { create } from "zustand";
2
- import { persist } from "zustand/middleware";
3
- import { shared } from "./use-broadcast/shared.js";
1
+ import { create, type Mutate, type StoreApi } from "zustand";
2
+ import { createJSONStorage, persist } from "zustand/middleware";
4
3
 
5
- export interface AuthState {
4
+ export interface AuthState<ProviderData = unknown> {
6
5
  isAuthenticated: boolean;
7
6
  isPending: boolean;
8
- profile?: UserProfile;
7
+ profile: UserProfile | null;
8
+ providerData: ProviderData | null;
9
9
  }
10
10
 
11
- export const useAuthState = create<AuthState>(
12
- shared(
13
- (_) => ({
11
+ export class Authentication {
12
+ async setLoggedIn(isLoggedIn: boolean) {}
13
+ async setProfile() {}
14
+ async setPersistentProviderData() {}
15
+ }
16
+
17
+ export type StoreWithPersist<T> = Mutate<
18
+ StoreApi<T>,
19
+ [["zustand/persist", unknown]]
20
+ >;
21
+
22
+ export const withStorageDOMEvents = <T>(store: StoreWithPersist<T>) => {
23
+ const storageEventCallback = (e: StorageEvent) => {
24
+ if (e.key === store.persist.getOptions().name && e.newValue) {
25
+ void store.persist.rehydrate();
26
+ }
27
+ };
28
+
29
+ window.addEventListener("storage", storageEventCallback);
30
+
31
+ return () => {
32
+ window.removeEventListener("storage", storageEventCallback);
33
+ };
34
+ };
35
+
36
+ export const useAuthState = create<AuthState>()(
37
+ persist(
38
+ (state) => ({
14
39
  isAuthenticated: false,
15
40
  isPending: false,
16
- profile: undefined,
41
+ profile: null,
42
+ providerData: null,
17
43
  }),
18
- { name: "auth-state" },
44
+ {
45
+ name: "auth-state",
46
+ storage: createJSONStorage(() => localStorage),
47
+ // partialize: (s) => ({ state: s }),
48
+ },
19
49
  ),
20
50
  );
21
51
 
52
+ if (typeof window !== "undefined") {
53
+ withStorageDOMEvents(useAuthState);
54
+ }
55
+
22
56
  export interface UserProfile {
23
57
  sub: string;
24
58
  email: string | undefined;
@@ -4,7 +4,7 @@ import {
4
4
  QueryClientProvider,
5
5
  } from "@tanstack/react-query";
6
6
  import { type HelmetData, HelmetProvider } from "@zudoku/react-helmet-async";
7
- import { StrictMode, useMemo } from "react";
7
+ import { StrictMode } from "react";
8
8
  import { type createBrowserRouter, RouterProvider } from "react-router-dom";
9
9
  import {
10
10
  type createStaticRouter,
@@ -13,29 +13,36 @@ import {
13
13
  } from "react-router-dom/server.js";
14
14
  import { StaggeredRenderContext } from "../plugins/openapi/StaggeredRender.js";
15
15
 
16
+ const queryClient = new QueryClient({
17
+ defaultOptions: {
18
+ queries: {
19
+ staleTime: 1000 * 60 * 5,
20
+ },
21
+ },
22
+ });
23
+
16
24
  const Bootstrap = ({
17
25
  router,
18
26
  hydrate = false,
19
27
  }: {
20
28
  hydrate?: boolean;
21
29
  router: ReturnType<typeof createBrowserRouter>;
22
- }) => {
23
- const queryClient = useMemo(() => new QueryClient(), []);
24
-
25
- return (
26
- <StrictMode>
27
- <QueryClientProvider client={queryClient}>
28
- <HydrationBoundary state={hydrate ? (window as any).DATA : undefined}>
29
- <HelmetProvider>
30
- <StaggeredRenderContext.Provider value={{ stagger: !hydrate }}>
31
- <RouterProvider router={router} />
32
- </StaggeredRenderContext.Provider>
33
- </HelmetProvider>
34
- </HydrationBoundary>
35
- </QueryClientProvider>
36
- </StrictMode>
37
- );
38
- };
30
+ }) => (
31
+ <StrictMode>
32
+ <QueryClientProvider client={queryClient}>
33
+ <HydrationBoundary state={hydrate ? (window as any).DATA : undefined}>
34
+ <HelmetProvider>
35
+ <StaggeredRenderContext.Provider value={{ stagger: !hydrate }}>
36
+ <RouterProvider
37
+ router={router}
38
+ future={{ v7_startTransition: true }}
39
+ />
40
+ </StaggeredRenderContext.Provider>
41
+ </HelmetProvider>
42
+ </HydrationBoundary>
43
+ </QueryClientProvider>
44
+ </StrictMode>
45
+ );
39
46
 
40
47
  const BootstrapStatic = ({
41
48
  router,
@@ -41,7 +41,12 @@ const RecursiveMenu = ({ item }: { item: ProfileNavigationItem }) => {
41
41
  </DropdownMenuSub>
42
42
  ) : (
43
43
  <Link to={item.path ?? ""}>
44
- <DropdownMenuItem key={item.label}>{item.label}</DropdownMenuItem>
44
+ <DropdownMenuItem key={item.label} className="flex gap-2">
45
+ {item.icon && (
46
+ <item.icon size={16} strokeWidth={1} absoluteStrokeWidth />
47
+ )}
48
+ {item.label}
49
+ </DropdownMenuItem>
45
50
  </Link>
46
51
  );
47
52
  };
@@ -55,7 +60,7 @@ export const Header = memo(function HeaderInner() {
55
60
  const accountItems = plugins
56
61
  .filter((p) => isProfileMenuPlugin(p))
57
62
  .flatMap((p) => p.getProfileMenuItems(context))
58
- .map((i) => <RecursiveMenu key={i.label} item={i} />);
63
+ .sort((i) => i.weight ?? 0);
59
64
 
60
65
  return (
61
66
  <header className="sticky lg:top-0 z-10 bg-background/80 backdrop-blur w-full">
@@ -82,7 +87,6 @@ export const Header = memo(function HeaderInner() {
82
87
  loading="lazy"
83
88
  />
84
89
  <img
85
- data-hide-on-theme="light"
86
90
  src={
87
91
  /https?:\/\//.test(page.logo.src.dark)
88
92
  ? page.logo.src.dark
@@ -93,7 +97,7 @@ export const Header = memo(function HeaderInner() {
93
97
  }
94
98
  alt={page.logo.alt ?? page.pageTitle}
95
99
  style={{ width: page.logo.width }}
96
- className="h-10"
100
+ className="h-10 hidden dark:block"
97
101
  loading="lazy"
98
102
  />
99
103
  </>
@@ -121,17 +125,46 @@ export const Header = memo(function HeaderInner() {
121
125
  Login
122
126
  </Button>
123
127
  ) : (
124
- accountItems.length > 0 && (
128
+ Object.values(accountItems).length > 0 && (
125
129
  <DropdownMenu modal={false}>
126
130
  <DropdownMenuTrigger asChild>
127
131
  <Button variant="ghost">
128
- {profile?.email ? `${profile.email}` : "My Account"}
132
+ {profile?.name ? `${profile.name}` : "My Account"}
129
133
  </Button>
130
134
  </DropdownMenuTrigger>
131
135
  <DropdownMenuContent className="w-56">
132
- <DropdownMenuLabel>My Account</DropdownMenuLabel>
133
- <DropdownMenuSeparator />
134
- {accountItems}
136
+ <DropdownMenuLabel>
137
+ {profile?.name ? `${profile.name}` : "My Account"}
138
+ {profile?.email && (
139
+ <div className="font-normal text-muted-foreground">
140
+ {profile.email}
141
+ </div>
142
+ )}
143
+ </DropdownMenuLabel>
144
+ {accountItems.filter((i) => i.category === "top")
145
+ .length > 0 && <DropdownMenuSeparator />}
146
+ {accountItems
147
+ .filter((i) => i.category === "top")
148
+ .map((i) => (
149
+ <RecursiveMenu key={i.label} item={i} />
150
+ ))}
151
+ {accountItems.filter(
152
+ (i) => !i.category || i.category === "middle",
153
+ ).length > 0 && <DropdownMenuSeparator />}
154
+ {accountItems
155
+ .filter(
156
+ (i) => !i.category || i.category === "middle",
157
+ )
158
+ .map((i) => (
159
+ <RecursiveMenu key={i.label} item={i} />
160
+ ))}
161
+ {accountItems.filter((i) => i.category === "bottom")
162
+ .length > 0 && <DropdownMenuSeparator />}
163
+ {accountItems
164
+ .filter((i) => i.category === "bottom")
165
+ .map((i) => (
166
+ <RecursiveMenu key={i.label} item={i} />
167
+ ))}
135
168
  </DropdownMenuContent>
136
169
  </DropdownMenu>
137
170
  )
@@ -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();
@@ -25,7 +32,7 @@ export const Layout = ({ children }: { children?: ReactNode }) => {
25
32
 
26
33
  useEffect(() => {
27
34
  // Initialize the authentication plugin
28
- authentication?.pageLoad?.();
35
+ authentication?.onPageLoad?.();
29
36
  }, [authentication]);
30
37
 
31
38
  useEffect(() => {
@@ -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,11 +1,9 @@
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,
@@ -13,14 +11,19 @@ import {
13
11
  import { useZudoku } from "./context/ZudokuContext.js";
14
12
  import { Search } from "./Search.js";
15
13
  import { ThemeSwitch } from "./ThemeSwitch.js";
16
- import { isHiddenItem } from "./TopNavigation.js";
14
+ import { isHiddenItem, TopNavItem } from "./TopNavigation.js";
17
15
 
18
16
  export const MobileTopNavigation = () => {
19
17
  const { topNavigation } = useZudoku();
20
18
  const { isAuthenticated } = useAuth();
19
+ const [drawerOpen, setDrawerOpen] = useState(false);
21
20
 
22
21
  return (
23
- <Drawer direction="right">
22
+ <Drawer
23
+ direction="right"
24
+ open={drawerOpen}
25
+ onOpenChange={(open) => setDrawerOpen(open)}
26
+ >
24
27
  <div className="flex lg:hidden justify-self-end">
25
28
  <DrawerTrigger className="lg:hidden">
26
29
  <MenuIcon size={22} />
@@ -42,19 +45,9 @@ export const MobileTopNavigation = () => {
42
45
  </li>
43
46
  {topNavigation.filter(isHiddenItem(isAuthenticated)).map((item) => (
44
47
  <li key={item.label}>
45
- <NavLink
46
- className={({ isActive }) =>
47
- cx(
48
- "block font-medium border-b-2",
49
- isActive
50
- ? "border-primary text-foreground"
51
- : "border-transparent text-foreground/75 hover:text-foreground hover:border-accent-foreground/25",
52
- )
53
- }
54
- to={item.id}
55
- >
56
- <DrawerClose>{item.label}</DrawerClose>
57
- </NavLink>
48
+ <button onClick={() => setDrawerOpen(false)}>
49
+ <TopNavItem {...item} />
50
+ </button>
58
51
  </li>
59
52
  ))}
60
53
  </ul>
@@ -56,14 +56,15 @@ export const SyntaxHighlight = ({
56
56
  language = "plain",
57
57
  ...props
58
58
  }: SyntaxHighlightProps) => {
59
- const { theme } = useTheme();
59
+ const { resolvedTheme } = useTheme();
60
60
  const [isCopied, setIsCopied] = useState(false);
61
61
 
62
62
  if (!props.code) {
63
63
  return null;
64
64
  }
65
65
 
66
- const highlightTheme = theme === "dark" ? themes.vsDark : themes.github;
66
+ const highlightTheme =
67
+ resolvedTheme === "dark" ? themes.vsDark : themes.github;
67
68
 
68
69
  // hardcoded values from the themes to avoid color flash in SSR
69
70
  const themeColorClasses =
@@ -4,18 +4,20 @@ import { Button } from "zudoku/ui/Button.js";
4
4
  import { ClientOnly } from "./ClientOnly.js";
5
5
 
6
6
  export const ThemeSwitch = () => {
7
- const { theme, setTheme } = useTheme();
8
- const ThemeIcon = theme === "dark" ? MoonStarIcon : SunIcon;
7
+ const { resolvedTheme, setTheme } = useTheme();
8
+ const ThemeIcon = resolvedTheme === "dark" ? MoonStarIcon : SunIcon;
9
9
 
10
10
  return (
11
11
  <ClientOnly>
12
12
  <Button
13
13
  variant="ghost"
14
14
  aria-label={
15
- theme === "dark" ? "Switch to light mode" : "Switch to dark mode"
15
+ resolvedTheme === "dark"
16
+ ? "Switch to light mode"
17
+ : "Switch to dark mode"
16
18
  }
17
19
  className="p-2.5 -m-2.5 rounded-full"
18
- onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
20
+ onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
19
21
  >
20
22
  <ThemeIcon size={18} />
21
23
  </Button>
@@ -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";
3
+ import { NavLink, useNavigation } from "react-router-dom";
4
4
  import { TopNavigationItem } from "../../config/validators/validate.js";
5
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";
@@ -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.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
  };
@@ -1,3 +1,4 @@
1
+ import type { LucideProps } from "lucide-react";
1
2
  import { type ReactElement } from "react";
2
3
  import { type RouteObject } from "react-router-dom";
3
4
  import type { Sidebar } from "../../config/validators/SidebarSchema.js";
@@ -36,7 +37,14 @@ export interface ProfileMenuPlugin {
36
37
  export type ProfileNavigationItem = {
37
38
  label: string;
38
39
  path?: string;
40
+ weight?: number;
41
+ category?: "top" | "middle" | "bottom";
39
42
  children?: ProfileNavigationItem[];
43
+ icon?: React.ComponentType<
44
+ LucideProps & {
45
+ [key: string]: any;
46
+ }
47
+ >;
40
48
  };
41
49
 
42
50
  export interface CommonPlugin {