zudoku 0.3.0-dev.57 → 0.3.0-dev.59

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 (199) hide show
  1. package/dist/app/entry.client.js +1 -1
  2. package/dist/app/entry.client.js.map +1 -1
  3. package/dist/app/entry.server.d.ts +2 -0
  4. package/dist/app/entry.server.js +1 -0
  5. package/dist/app/entry.server.js.map +1 -1
  6. package/dist/app/main.js +8 -2
  7. package/dist/app/main.js.map +1 -1
  8. package/dist/config/validators/validate.d.ts +121 -102
  9. package/dist/config/validators/validate.js +4 -0
  10. package/dist/config/validators/validate.js.map +1 -1
  11. package/dist/lib/authentication/AuthenticationPlugin.d.ts +16 -0
  12. package/dist/lib/authentication/AuthenticationPlugin.js +31 -0
  13. package/dist/lib/authentication/AuthenticationPlugin.js.map +1 -0
  14. package/dist/lib/authentication/authentication.d.ts +3 -4
  15. package/dist/lib/authentication/components/Login.d.ts +1 -0
  16. package/dist/lib/authentication/components/Login.js +10 -0
  17. package/dist/lib/authentication/components/Login.js.map +1 -0
  18. package/dist/lib/authentication/components/Logout.d.ts +1 -0
  19. package/dist/lib/authentication/components/Logout.js +10 -0
  20. package/dist/lib/authentication/components/Logout.js.map +1 -0
  21. package/dist/lib/authentication/providers/clerk.js +43 -27
  22. package/dist/lib/authentication/providers/clerk.js.map +1 -1
  23. package/dist/lib/authentication/providers/openid.d.ts +11 -3
  24. package/dist/lib/authentication/providers/openid.js +22 -11
  25. package/dist/lib/authentication/providers/openid.js.map +1 -1
  26. package/dist/lib/authentication/routes.d.ts +5 -0
  27. package/dist/lib/authentication/routes.js +12 -0
  28. package/dist/lib/authentication/routes.js.map +1 -0
  29. package/dist/lib/components/Bootstrap.d.ts +2 -1
  30. package/dist/lib/components/Bootstrap.js +4 -1
  31. package/dist/lib/components/Bootstrap.js.map +1 -1
  32. package/dist/lib/components/DevPortal.js +14 -2
  33. package/dist/lib/components/DevPortal.js.map +1 -1
  34. package/dist/lib/components/Header.js +16 -2
  35. package/dist/lib/components/Header.js.map +1 -1
  36. package/dist/lib/components/InlineCode.js +1 -1
  37. package/dist/lib/components/InlineCode.js.map +1 -1
  38. package/dist/lib/components/TopNavigation.js +1 -1
  39. package/dist/lib/components/TopNavigation.js.map +1 -1
  40. package/dist/lib/components/context/DevPortalProvider.js +18 -1
  41. package/dist/lib/components/context/DevPortalProvider.js.map +1 -1
  42. package/dist/lib/components/index.d.ts +5 -1
  43. package/dist/lib/components/index.js +4 -0
  44. package/dist/lib/components/index.js.map +1 -1
  45. package/dist/lib/components/navigation/SideNavigationItem.js +1 -1
  46. package/dist/lib/components/navigation/SideNavigationItem.js.map +1 -1
  47. package/dist/lib/core/DevPortalContext.d.ts +1 -1
  48. package/dist/lib/core/DevPortalContext.js.map +1 -1
  49. package/dist/lib/core/plugins.d.ts +10 -1
  50. package/dist/lib/core/plugins.js +1 -0
  51. package/dist/lib/core/plugins.js.map +1 -1
  52. package/dist/lib/plugins/api-keys/CreateApiKey.js +1 -1
  53. package/dist/lib/plugins/api-keys/CreateApiKey.js.map +1 -1
  54. package/dist/lib/plugins/api-keys/SettingsApiKeys.js +2 -2
  55. package/dist/lib/plugins/api-keys/SettingsApiKeys.js.map +1 -1
  56. package/dist/lib/plugins/api-keys/index.d.ts +2 -2
  57. package/dist/lib/plugins/api-keys/index.js +6 -0
  58. package/dist/lib/plugins/api-keys/index.js.map +1 -1
  59. package/dist/lib/plugins/custom-page/index.d.ts +8 -0
  60. package/dist/lib/plugins/custom-page/index.js +12 -0
  61. package/dist/lib/plugins/custom-page/index.js.map +1 -0
  62. package/dist/lib/plugins/markdown/MdxPage.js +1 -1
  63. package/dist/lib/plugins/markdown/MdxPage.js.map +1 -1
  64. package/dist/lib/plugins/openapi/OperationListItem.js +1 -1
  65. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  66. package/dist/lib/plugins/openapi/ParameterList.js +1 -1
  67. package/dist/lib/plugins/openapi/ParameterList.js.map +1 -1
  68. package/dist/lib/plugins/openapi/SchemaListViewItem.js +3 -3
  69. package/dist/lib/plugins/openapi/SchemaListViewItem.js.map +1 -1
  70. package/dist/lib/plugins/openapi/SidecarBox.js +1 -1
  71. package/dist/lib/plugins/openapi/SidecarBox.js.map +1 -1
  72. package/dist/lib/plugins/openapi/StaggeredRender.d.ts +3 -0
  73. package/dist/lib/plugins/openapi/StaggeredRender.js +10 -5
  74. package/dist/lib/plugins/openapi/StaggeredRender.js.map +1 -1
  75. package/dist/lib/plugins/openapi/playground/Playground.js +2 -2
  76. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  77. package/dist/lib/ui/Card.js +1 -1
  78. package/dist/lib/ui/Card.js.map +1 -1
  79. package/dist/lib/ui/DropdownMenu.d.ts +27 -0
  80. package/dist/lib/ui/DropdownMenu.js +36 -0
  81. package/dist/lib/ui/DropdownMenu.js.map +1 -0
  82. package/dist/lib/ui/button-variants.d.ts +2 -2
  83. package/dist/lib/ui/button-variants.js +1 -0
  84. package/dist/lib/ui/button-variants.js.map +1 -1
  85. package/dist/lib/util/MdxComponents.js +1 -1
  86. package/dist/lib/util/MdxComponents.js.map +1 -1
  87. package/dist/lib/util/joinPath.js +2 -1
  88. package/dist/lib/util/joinPath.js.map +1 -1
  89. package/dist/vite/build.js +5 -2
  90. package/dist/vite/build.js.map +1 -1
  91. package/dist/vite/config.d.ts +8 -1
  92. package/dist/vite/config.js +13 -6
  93. package/dist/vite/config.js.map +1 -1
  94. package/dist/vite/plugin-component.js +1 -0
  95. package/dist/vite/plugin-component.js.map +1 -1
  96. package/dist/vite/prerender.d.ts +1 -1
  97. package/dist/vite/prerender.js +23 -3
  98. package/dist/vite/prerender.js.map +1 -1
  99. package/lib/{AnchorLink-GNsUeGSX.js → AnchorLink-Bj1hwDuD.js} +3 -3
  100. package/lib/{AnchorLink-GNsUeGSX.js.map → AnchorLink-Bj1hwDuD.js.map} +1 -1
  101. package/lib/AuthenticationPlugin-CG6Bw32B.js +46 -0
  102. package/lib/AuthenticationPlugin-CG6Bw32B.js.map +1 -0
  103. package/lib/CategoryHeading-DMkTmmBh.js +10 -0
  104. package/lib/CategoryHeading-DMkTmmBh.js.map +1 -0
  105. package/lib/Combination-lAFQBd6U.js +2774 -0
  106. package/lib/Combination-lAFQBd6U.js.map +1 -0
  107. package/lib/DevPortalProvider-BBhQ8kgI.js +1125 -0
  108. package/lib/DevPortalProvider-BBhQ8kgI.js.map +1 -0
  109. package/lib/{Markdown-DtLFdxD1.js → Markdown-BjRJKl_E.js} +1376 -1379
  110. package/lib/Markdown-BjRJKl_E.js.map +1 -0
  111. package/lib/{MdxPage-CbwYRKf5.js → MdxPage-DJTFOCbZ.js} +17 -17
  112. package/lib/{MdxPage-CbwYRKf5.js.map → MdxPage-DJTFOCbZ.js.map} +1 -1
  113. package/lib/OperationList-DDTtK3I7.js +5403 -0
  114. package/lib/OperationList-DDTtK3I7.js.map +1 -0
  115. package/lib/{Route-C1LyvITr.js → Route-Bsrd0acQ.js} +2 -2
  116. package/lib/{Route-C1LyvITr.js.map → Route-Bsrd0acQ.js.map} +1 -1
  117. package/lib/Select-CEnkyfyn.js +2223 -0
  118. package/lib/Select-CEnkyfyn.js.map +1 -0
  119. package/lib/Spinner-Ciq_pWU7.js +359 -0
  120. package/lib/Spinner-Ciq_pWU7.js.map +1 -0
  121. package/lib/{hook-Biq3zYel.js → hook-Q_gAL2NZ.js} +20 -19
  122. package/lib/{hook-Biq3zYel.js.map → hook-Q_gAL2NZ.js.map} +1 -1
  123. package/lib/{index-Bg82-bqR.js → index-BE2a6gGC.js} +24 -23
  124. package/lib/{index-Bg82-bqR.js.map → index-BE2a6gGC.js.map} +1 -1
  125. package/lib/{jsx-runtime-CJZJivg2.js → jsx-runtime-BIr0WBt_.js} +119 -119
  126. package/lib/jsx-runtime-BIr0WBt_.js.map +1 -0
  127. package/lib/{router-CBw2vqJE.js → router-BiRCp01d.js} +671 -673
  128. package/lib/router-BiRCp01d.js.map +1 -0
  129. package/lib/zudoku.auth-clerk.js +47 -32
  130. package/lib/zudoku.auth-clerk.js.map +1 -1
  131. package/lib/zudoku.auth-openid.js +170 -159
  132. package/lib/zudoku.auth-openid.js.map +1 -1
  133. package/lib/zudoku.components.js +1514 -598
  134. package/lib/zudoku.components.js.map +1 -1
  135. package/lib/zudoku.plugin-api-keys.js +30 -24
  136. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  137. package/lib/zudoku.plugin-custom-page.js +13 -0
  138. package/lib/zudoku.plugin-custom-page.js.map +1 -0
  139. package/lib/zudoku.plugin-markdown.js +19 -20
  140. package/lib/zudoku.plugin-markdown.js.map +1 -1
  141. package/lib/zudoku.plugin-openapi.js +4 -4
  142. package/lib/zudoku.plugin-redirect.js +3 -3
  143. package/package.json +29 -14
  144. package/src/app/entry.client.tsx +1 -1
  145. package/src/app/entry.server.tsx +2 -0
  146. package/src/app/main.css +6 -0
  147. package/src/app/main.tsx +8 -2
  148. package/src/lib/authentication/AuthenticationPlugin.tsx +36 -0
  149. package/src/lib/authentication/authentication.ts +3 -4
  150. package/src/lib/authentication/components/Login.tsx +11 -0
  151. package/src/lib/authentication/components/Logout.tsx +11 -0
  152. package/src/lib/authentication/providers/clerk.tsx +43 -27
  153. package/src/lib/authentication/providers/openid.tsx +25 -13
  154. package/src/lib/authentication/routes.tsx +10 -0
  155. package/src/lib/components/Bootstrap.tsx +14 -7
  156. package/src/lib/components/DevPortal.tsx +29 -9
  157. package/src/lib/components/Header.tsx +80 -30
  158. package/src/lib/components/InlineCode.tsx +1 -1
  159. package/src/lib/components/TopNavigation.tsx +1 -1
  160. package/src/lib/components/context/DevPortalProvider.ts +22 -2
  161. package/src/lib/components/index.ts +4 -0
  162. package/src/lib/components/navigation/SideNavigationItem.tsx +1 -1
  163. package/src/lib/core/DevPortalContext.ts +1 -1
  164. package/src/lib/core/plugins.ts +16 -0
  165. package/src/lib/plugins/api-keys/CreateApiKey.tsx +1 -1
  166. package/src/lib/plugins/api-keys/SettingsApiKeys.tsx +4 -4
  167. package/src/lib/plugins/api-keys/index.tsx +8 -1
  168. package/src/lib/plugins/custom-page/index.tsx +22 -0
  169. package/src/lib/plugins/markdown/MdxPage.tsx +2 -2
  170. package/src/lib/plugins/openapi/OperationListItem.tsx +1 -4
  171. package/src/lib/plugins/openapi/ParameterList.tsx +1 -1
  172. package/src/lib/plugins/openapi/SchemaListViewItem.tsx +3 -3
  173. package/src/lib/plugins/openapi/SidecarBox.tsx +1 -1
  174. package/src/lib/plugins/openapi/StaggeredRender.tsx +19 -5
  175. package/src/lib/plugins/openapi/playground/Playground.tsx +2 -2
  176. package/src/lib/ui/Card.tsx +1 -1
  177. package/src/lib/ui/DropdownMenu.tsx +199 -0
  178. package/src/lib/ui/button-variants.ts +1 -0
  179. package/src/lib/util/MdxComponents.tsx +1 -1
  180. package/src/lib/util/joinPath.tsx +2 -1
  181. package/dist/app/zudoku-manifest.d.ts +0 -1
  182. package/dist/app/zudoku-manifest.js +0 -20
  183. package/dist/app/zudoku-manifest.js.map +0 -1
  184. package/lib/Button-DpHMZvVs.js +0 -4571
  185. package/lib/Button-DpHMZvVs.js.map +0 -1
  186. package/lib/DevPortalProvider-Do9oJqme.js +0 -1081
  187. package/lib/DevPortalProvider-Do9oJqme.js.map +0 -1
  188. package/lib/Markdown-DtLFdxD1.js.map +0 -1
  189. package/lib/OperationList-DypxLtSC.js +0 -5578
  190. package/lib/OperationList-DypxLtSC.js.map +0 -1
  191. package/lib/Spinner-Bhbs5aPI.js +0 -182
  192. package/lib/Spinner-Bhbs5aPI.js.map +0 -1
  193. package/lib/index-gsAuUwQh.js +0 -418
  194. package/lib/index-gsAuUwQh.js.map +0 -1
  195. package/lib/jsx-runtime-CJZJivg2.js.map +0 -1
  196. package/lib/router-CBw2vqJE.js.map +0 -1
  197. package/lib/util-_jwUlTBU.js +0 -41
  198. package/lib/util-_jwUlTBU.js.map +0 -1
  199. package/src/app/zudoku-manifest.ts +0 -22
@@ -0,0 +1,36 @@
1
+ import type { RouteObject } from "react-router-dom";
2
+ import {
3
+ CommonPlugin,
4
+ NavigationPlugin,
5
+ ProfileMenuPlugin,
6
+ } from "../core/plugins.js";
7
+ import { Login } from "./components/Login.js";
8
+ import { Logout } from "./components/Logout.js";
9
+
10
+ type PluginInterface = NavigationPlugin & CommonPlugin & ProfileMenuPlugin;
11
+
12
+ export class AuthenticationPlugin implements PluginInterface {
13
+ constructor(private additionalRoutes: RouteObject[] = []) {}
14
+ getRoutes() {
15
+ return [
16
+ {
17
+ path: "/logout",
18
+ element: <Logout />,
19
+ },
20
+ {
21
+ path: "/login",
22
+ element: <Login />,
23
+ },
24
+ ...this.additionalRoutes,
25
+ ];
26
+ }
27
+
28
+ getProfileMenuItems() {
29
+ return [
30
+ {
31
+ label: "Logout",
32
+ path: "/logout",
33
+ },
34
+ ];
35
+ }
36
+ }
@@ -1,11 +1,10 @@
1
- import { CommonPlugin, NavigationPlugin } from "../core/plugins.js";
1
+ import { DevPortalPlugin } from "../core/plugins.js";
2
2
 
3
- type AuthenticationPlugin = NavigationPlugin & CommonPlugin;
4
-
5
- export interface AuthenticationProvider extends AuthenticationPlugin {
3
+ export interface AuthenticationProvider {
6
4
  login(): Promise<void>;
7
5
  getAccessToken(): Promise<string>;
8
6
  logout(): Promise<void>;
7
+ getAuthenticationPlugin?(): DevPortalPlugin;
9
8
  }
10
9
 
11
10
  /**
@@ -0,0 +1,11 @@
1
+ import { useEffect } from "react";
2
+ import { useDevPortal } from "../../components/context/DevPortalProvider.js";
3
+
4
+ export const Login = () => {
5
+ const context = useDevPortal();
6
+ useEffect(() => {
7
+ void context.authentication?.login();
8
+ }, [context.authentication]);
9
+
10
+ return null;
11
+ };
@@ -0,0 +1,11 @@
1
+ import { useEffect } from "react";
2
+ import { useDevPortal } from "../../components/context/DevPortalProvider.js";
3
+
4
+ export const Logout = () => {
5
+ const context = useDevPortal();
6
+ useEffect(() => {
7
+ void context.authentication?.logout();
8
+ }, [context.authentication]);
9
+
10
+ return null;
11
+ };
@@ -1,8 +1,42 @@
1
1
  import type { Clerk } from "@clerk/clerk-js";
2
2
  import { ClerkAuthenticationConfig } from "../../../config/config.js";
3
3
  import { AuthenticationProviderInitializer } from "../authentication.js";
4
+ import { AuthenticationPlugin } from "../AuthenticationPlugin.js";
4
5
  import { useAuthState } from "../state.js";
5
6
 
7
+ class ClerkAuthPlugin extends AuthenticationPlugin {
8
+ constructor(private clerk: Promise<Clerk | undefined>) {
9
+ super();
10
+ }
11
+ initialize = async () => {
12
+ const clerk = await this.clerk;
13
+
14
+ if (!clerk) {
15
+ return;
16
+ }
17
+
18
+ if (clerk.session) {
19
+ useAuthState.setState({
20
+ isAuthenticated: true,
21
+ isPending: false,
22
+ profile: {
23
+ sub: clerk.session.user.id,
24
+ name: clerk.session.user.fullName ?? undefined,
25
+ email: clerk.session.user.emailAddresses[0]?.emailAddress,
26
+ emailVerified: false, // TODO: Check this
27
+ pictureUrl: clerk.session.user.imageUrl,
28
+ },
29
+ });
30
+ } else {
31
+ useAuthState.setState({
32
+ isAuthenticated: false,
33
+ isPending: false,
34
+ profile: undefined,
35
+ });
36
+ }
37
+ };
38
+ }
39
+
6
40
  const clerkAuth: AuthenticationProviderInitializer<
7
41
  ClerkAuthenticationConfig
8
42
  > = ({ clerkPubKey }) => {
@@ -13,7 +47,8 @@ const clerkAuth: AuthenticationProviderInitializer<
13
47
  const { Clerk } = await import("@clerk/clerk-js");
14
48
  clerkApi = new Clerk(clerkPubKey);
15
49
 
16
- await clerkApi.load({});
50
+ await clerkApi.load();
51
+ return clerkApi;
17
52
  })();
18
53
 
19
54
  async function getAccessToken() {
@@ -29,38 +64,19 @@ const clerkAuth: AuthenticationProviderInitializer<
29
64
  }
30
65
 
31
66
  return {
32
- initialize: async () => {
33
- await ensureLoaded;
34
-
35
- if (clerkApi.session) {
36
- useAuthState.setState({
37
- isAuthenticated: true,
38
- isPending: false,
39
- profile: {
40
- sub: clerkApi.session.user.id,
41
- name: clerkApi.session.user.fullName ?? undefined,
42
- email: clerkApi.session.user.emailAddresses[0]?.emailAddress,
43
- emailVerified: false, // TODO: Check this
44
- // emailVerified: clerkApi.session.user.hasVerifiedEmailAddress,
45
- pictureUrl: clerkApi.session.user.imageUrl,
46
- },
47
- });
48
- } else {
49
- useAuthState.setState({
50
- isAuthenticated: false,
51
- isPending: false,
52
- profile: undefined,
53
- });
54
- }
55
- },
56
67
  getAccessToken,
57
68
  logout: async () => {
58
69
  await clerkApi.signOut();
59
70
  },
60
71
  login: async () => {
61
- await clerkApi.redirectToSignIn();
72
+ await clerkApi.redirectToSignIn({
73
+ signInForceRedirectUrl: "http://localhost:9000/",
74
+ signUpForceRedirectUrl: "http://localhost:9000/",
75
+ });
76
+ },
77
+ getAuthenticationPlugin() {
78
+ return new ClerkAuthPlugin(ensureLoaded);
62
79
  },
63
- getRoutes: () => [],
64
80
  };
65
81
  };
66
82
 
@@ -1,11 +1,11 @@
1
1
  import logger from "loglevel";
2
2
  import * as oauth from "oauth4webapi";
3
- import type { RouteObject } from "react-router-dom";
4
3
  import { OpenIDAuthenticationConfig } from "../../../config/config.js";
5
4
  import {
6
5
  AuthenticationProvider,
7
6
  AuthenticationProviderInitializer,
8
7
  } from "../authentication.js";
8
+ import { AuthenticationPlugin } from "../AuthenticationPlugin.js";
9
9
  import { Callback } from "../Callback.js";
10
10
  import { AuthorizationError, OAuthAuthorizationError } from "../errors.js";
11
11
  import { useAuthState, UserProfile } from "../state.js";
@@ -19,6 +19,24 @@ interface TokenState {
19
19
  tokenType: string;
20
20
  }
21
21
 
22
+ class OpenIdAuthPlugin extends AuthenticationPlugin {
23
+ constructor(
24
+ private callbackUrlPath: string,
25
+ private handleCallback: () => Promise<void>,
26
+ ) {
27
+ super();
28
+ }
29
+ getRoutes() {
30
+ return [
31
+ ...super.getRoutes(),
32
+ {
33
+ path: this.callbackUrlPath,
34
+ element: <Callback handleCallback={this.handleCallback} />,
35
+ },
36
+ ];
37
+ }
38
+ }
39
+
22
40
  export class OpenIDAuthenticationProvider implements AuthenticationProvider {
23
41
  protected client: oauth.Client;
24
42
  protected issuer: string;
@@ -91,10 +109,6 @@ export class OpenIDAuthenticationProvider implements AuthenticationProvider {
91
109
  };
92
110
  }
93
111
 
94
- async initialize() {
95
- // No init needed
96
- }
97
-
98
112
  async login(): Promise<void> {
99
113
  const code_challenge_method = "S256";
100
114
  const authorizationServer = await this.getAuthServer();
@@ -207,14 +221,6 @@ export class OpenIDAuthenticationProvider implements AuthenticationProvider {
207
221
  logoutUrl = redirectUrl;
208
222
  }
209
223
  }
210
- getRoutes(): RouteObject[] {
211
- return [
212
- {
213
- path: this.callbackUrlPath,
214
- element: <Callback handleCallback={this.handleCallback} />,
215
- },
216
- ];
217
- }
218
224
 
219
225
  handleCallback = async (): Promise<void> => {
220
226
  const url = new URL(window.location.href);
@@ -305,6 +311,12 @@ export class OpenIDAuthenticationProvider implements AuthenticationProvider {
305
311
  // Returning true because we are using react query
306
312
  // return true;
307
313
  };
314
+
315
+ getAuthenticationPlugin() {
316
+ return new OpenIdAuthPlugin(this.callbackUrlPath, () =>
317
+ this.handleCallback(),
318
+ );
319
+ }
308
320
  }
309
321
 
310
322
  const openIDAuth: AuthenticationProviderInitializer<
@@ -0,0 +1,10 @@
1
+ export default [
2
+ {
3
+ path: "/logout",
4
+ element: <div>Logout</div>,
5
+ },
6
+ {
7
+ path: "/login",
8
+ element: <div>Login</div>,
9
+ },
10
+ ];
@@ -5,18 +5,25 @@ import {
5
5
  type StaticHandlerContext,
6
6
  StaticRouterProvider,
7
7
  } from "react-router-dom/server.js";
8
+ import { StaggeredRenderContext } from "../plugins/openapi/StaggeredRender.js";
8
9
 
9
10
  const Bootstrap = ({
10
11
  router,
12
+ hydrate = false,
11
13
  }: {
14
+ hydrate?: boolean;
12
15
  router: ReturnType<typeof createBrowserRouter>;
13
- }) => (
14
- <StrictMode>
15
- <HelmetProvider>
16
- <RouterProvider router={router} />
17
- </HelmetProvider>
18
- </StrictMode>
19
- );
16
+ }) => {
17
+ return (
18
+ <StrictMode>
19
+ <HelmetProvider>
20
+ <StaggeredRenderContext.Provider value={{ stagger: !hydrate }}>
21
+ <RouterProvider router={router} />
22
+ </StaggeredRenderContext.Provider>
23
+ </HelmetProvider>
24
+ </StrictMode>
25
+ );
26
+ };
20
27
 
21
28
  const BootstrapStatic = ({
22
29
  router,
@@ -5,10 +5,13 @@ import {
5
5
  Fragment,
6
6
  memo,
7
7
  type PropsWithChildren,
8
+ useContext,
8
9
  useEffect,
9
10
  useMemo,
11
+ useState,
10
12
  } from "react";
11
13
  import { ErrorBoundary } from "react-error-boundary";
14
+ import { useNavigation } from "react-router-dom";
12
15
  import {
13
16
  DevPortalContext,
14
17
  queryClient,
@@ -16,6 +19,7 @@ import {
16
19
  } from "../core/DevPortalContext.js";
17
20
  import { hasHead } from "../core/plugins.js";
18
21
  import { TopLevelError } from "../errors/TopLevelError.js";
22
+ import { StaggeredRenderContext } from "../plugins/openapi/StaggeredRender.js";
19
23
  import { MdxComponents } from "../util/MdxComponents.js";
20
24
  import {
21
25
  ComponentsProvider,
@@ -46,6 +50,20 @@ const DevPortalInner = ({
46
50
  () => ({ ...MdxComponents, ...props.mdx?.components }),
47
51
  [props.mdx?.components],
48
52
  );
53
+ const { stagger } = useContext(StaggeredRenderContext);
54
+ const [didNavigate, setDidNavigate] = useState(false);
55
+ const staggeredValue = useMemo(
56
+ () => (didNavigate ? { stagger: true } : { stagger }),
57
+ [stagger, didNavigate],
58
+ );
59
+ const navigation = useNavigation();
60
+
61
+ useEffect(() => {
62
+ if (didNavigate) {
63
+ return;
64
+ }
65
+ setDidNavigate(true);
66
+ }, [didNavigate, navigation.location]);
49
67
 
50
68
  const devPortalContext = useMemo(() => new DevPortalContext(props), [props]);
51
69
 
@@ -61,15 +79,17 @@ const DevPortalInner = ({
61
79
  return (
62
80
  <QueryClientProvider client={queryClient}>
63
81
  <Helmet>{heads}</Helmet>
64
- <DevPortalProvider value={devPortalContext}>
65
- <MDXProvider components={mdxComponents}>
66
- <ThemeProvider>
67
- <ComponentsProvider value={components}>
68
- <ViewportAnchorProvider>{children}</ViewportAnchorProvider>
69
- </ComponentsProvider>
70
- </ThemeProvider>
71
- </MDXProvider>
72
- </DevPortalProvider>
82
+ <StaggeredRenderContext.Provider value={staggeredValue}>
83
+ <DevPortalProvider value={devPortalContext}>
84
+ <MDXProvider components={mdxComponents}>
85
+ <ThemeProvider>
86
+ <ComponentsProvider value={components}>
87
+ <ViewportAnchorProvider>{children}</ViewportAnchorProvider>
88
+ </ComponentsProvider>
89
+ </ThemeProvider>
90
+ </MDXProvider>
91
+ </DevPortalProvider>
92
+ </StaggeredRenderContext.Provider>
73
93
  </QueryClientProvider>
74
94
  );
75
95
  };
@@ -1,30 +1,77 @@
1
1
  import { MoonStarIcon, SunIcon } from "lucide-react";
2
2
  import { memo } from "react";
3
3
 
4
+ import { Link } from "react-router-dom";
4
5
  import { useAuth } from "../authentication/hook.js";
6
+ import { isProfileMenuPlugin, NavigationItem } from "../core/plugins.js";
7
+ import { Button } from "../ui/Button.js";
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuLabel,
13
+ DropdownMenuPortal,
14
+ DropdownMenuSeparator,
15
+ DropdownMenuSub,
16
+ DropdownMenuSubContent,
17
+ DropdownMenuSubTrigger,
18
+ DropdownMenuTrigger,
19
+ } from "../ui/DropdownMenu.js";
20
+ import { cn } from "../util/cn.js";
5
21
  import { TopNavigation } from "./TopNavigation.js";
6
22
  import { useDevPortal } from "./context/DevPortalProvider.js";
7
23
  import { useTheme } from "./context/ThemeContext.js";
8
24
 
25
+ const RecursiveMenu = ({ item }: { item: NavigationItem }) => {
26
+ return item.children ? (
27
+ <DropdownMenuSub key={item.label}>
28
+ <DropdownMenuSubTrigger>{item.label}</DropdownMenuSubTrigger>
29
+ <DropdownMenuPortal>
30
+ <DropdownMenuSubContent>
31
+ {item.children.map((item, i) => (
32
+ // eslint-disable-next-line react/no-array-index-key
33
+ <RecursiveMenu key={i} item={item} />
34
+ ))}
35
+ </DropdownMenuSubContent>
36
+ </DropdownMenuPortal>
37
+ </DropdownMenuSub>
38
+ ) : (
39
+ <Link to={item.path ?? ""}>
40
+ <DropdownMenuItem key={item.label}>{item.label}</DropdownMenuItem>
41
+ </Link>
42
+ );
43
+ };
44
+
9
45
  export const Header = memo(function HeaderInner() {
10
46
  const [isDark, toggleTheme] = useTheme();
11
47
  const { isAuthenticated, profile, isAuthEnabled, login, logout } = useAuth();
12
- const { page } = useDevPortal();
48
+ const context = useDevPortal();
49
+ const { page, plugins } = context;
13
50
 
14
51
  const ThemeIcon = isDark ? MoonStarIcon : SunIcon;
15
52
 
16
53
  return (
17
54
  <header className="fixed top-0 w-full z-10 bg-background/80 backdrop-blur">
18
55
  <div className="max-w-screen-2xl mx-auto">
19
- <div className="grid grid-cols-[calc(var(--side-nav-width))_1fr] lg:gap-12 items-center border-b border-border px-12 h-[--top-header-height]">
56
+ <div className="grid grid-cols-[calc(var(--side-nav-width))_1fr] lg:gap-12 items-center border-b px-12 h-[--top-header-height]">
20
57
  <div className="flex items-center gap-3.5">
21
58
  {page?.logo && (
22
- <img
23
- src={isDark ? page.logo.src.dark : page.logo.src.light}
24
- alt={page.logo.alt ?? page.pageTitle}
25
- style={{ width: page.logo.width }}
26
- className="h-10"
27
- />
59
+ <>
60
+ <img
61
+ src={page.logo.src.light}
62
+ alt={page.logo.alt ?? page.pageTitle}
63
+ style={{ width: page.logo.width }}
64
+ className={cn("h-10", isDark && "hidden")}
65
+ loading="lazy"
66
+ />
67
+ <img
68
+ src={page.logo.src.dark}
69
+ alt={page.logo.alt ?? page.pageTitle}
70
+ style={{ width: page.logo.width }}
71
+ className={cn("h-10", !isDark && "hidden")}
72
+ loading="lazy"
73
+ />
74
+ </>
28
75
  )}
29
76
  <span className="font-bold text-2xl text-foreground/85 tracking-wide">
30
77
  {page?.pageTitle}
@@ -37,34 +84,37 @@ export const Header = memo(function HeaderInner() {
37
84
  <SearchIcon size={14} />
38
85
  Search
39
86
  </div>
40
- <kbd className="absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border border-border bg-muted px-1.5 font-mono text-[11px] font-medium opacity-100 sm:flex">
87
+ <kbd className="absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[11px] font-medium opacity-100 sm:flex">
41
88
  ⌘K
42
89
  </kbd>
43
90
  </button>*/}
44
91
  </div>
45
92
 
46
- <div className="items-center justify-self-end text-sm hidden lg:flex">
47
- {isAuthEnabled && (
48
- <>
49
- {isAuthenticated ? (
50
- <button
51
- type="button"
52
- className="cursor-pointer hover:bg-secondary p-1 px-2 mx-2 rounded text-nowrap"
53
- onClick={logout}
54
- >
55
- Logout {profile?.email ? `(${profile.email})` : null}
56
- </button>
57
- ) : (
58
- <button
59
- type="button"
60
- className="cursor-pointer hover:bg-secondary p-1 px-2 mx-2 rounded"
61
- onClick={login}
62
- >
63
- Login
64
- </button>
65
- )}
66
- </>
93
+ <div className="items-center justify-self-end text-sm hidden lg:flex gap-2">
94
+ {isAuthEnabled && !isAuthenticated ? (
95
+ <Button variant="ghost" asChild>
96
+ <Link to="/login">Login</Link>
97
+ </Button>
98
+ ) : (
99
+ <DropdownMenu>
100
+ <DropdownMenuTrigger asChild>
101
+ <Button variant="ghost">
102
+ {profile?.email ? `${profile.email}` : "My Account"}
103
+ </Button>
104
+ </DropdownMenuTrigger>
105
+ <DropdownMenuContent className="w-56">
106
+ <DropdownMenuLabel>My Account</DropdownMenuLabel>
107
+ <DropdownMenuSeparator />
108
+ {plugins
109
+ .filter((p) => isProfileMenuPlugin(p))
110
+ .flatMap((p) => p.getProfileMenuItems(context))
111
+ .map((i) => (
112
+ <RecursiveMenu key={i.label} item={i} />
113
+ ))}
114
+ </DropdownMenuContent>
115
+ </DropdownMenu>
67
116
  )}
117
+
68
118
  <button
69
119
  type="button"
70
120
  aria-label={
@@ -11,7 +11,7 @@ export const InlineCode = ({
11
11
  <code
12
12
  className={cn(
13
13
  className,
14
- "font-mono border border-border p-1 py-0.5 rounded bg-border/50 dark:bg-border/70 whitespace-nowrap",
14
+ "font-mono border p-1 py-0.5 rounded bg-border/50 dark:bg-border/70 whitespace-nowrap",
15
15
  )}
16
16
  >
17
17
  {children}
@@ -12,7 +12,7 @@ export const TopNavigation = () => {
12
12
  }
13
13
 
14
14
  return (
15
- <nav className="border-b border-border text-sm px-12 h-[--top-nav-height]">
15
+ <nav className="border-b text-sm px-12 h-[--top-nav-height]">
16
16
  <ul className="flex flex-row items-center gap-8">
17
17
  {navigation.map((item) => (
18
18
  <li key={item.label}>
@@ -2,6 +2,7 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
2
2
  import { createContext, useContext } from "react";
3
3
  import { matchPath, useLocation } from "react-router-dom";
4
4
  import { DevPortalContext } from "../../core/DevPortalContext.js";
5
+ import { traverseNavigation } from "../../util/traverseNavigation.js";
5
6
 
6
7
  const DevPortalReactContext = createContext<DevPortalContext | undefined>(
7
8
  undefined,
@@ -31,8 +32,27 @@ export const useTopNavigationItem = () => {
31
32
  const { navigation } = useDevPortal();
32
33
  const location = useLocation();
33
34
 
34
- return navigation.find((item) =>
35
- matchPath({ path: item.path, end: false }, location.pathname),
35
+ // The `/` path needs this logic because it would always match:
36
+ // Only if a leaf node actually matches the current path it is active
37
+ for (const item of navigation) {
38
+ const foundNavItem = traverseNavigation(item, (_node, fullPath) => {
39
+ if (location.pathname === fullPath) {
40
+ return item;
41
+ }
42
+ });
43
+
44
+ if (foundNavItem) {
45
+ return foundNavItem;
46
+ }
47
+ }
48
+ if (location.pathname === "/") {
49
+ return navigation.find((item) => item.path === "/");
50
+ }
51
+
52
+ return navigation.find(
53
+ (item) =>
54
+ item.path !== "/" &&
55
+ matchPath({ path: item.path, end: false }, location.pathname),
36
56
  );
37
57
  };
38
58
 
@@ -1,7 +1,9 @@
1
1
  import { useMDXComponents as useMDXComponentsImport } from "@mdx-js/react";
2
+ import { Helmet } from "@zudoku/react-helmet-async";
2
3
  import { Link as LinkImport } from "react-router-dom";
3
4
  import { RouterError as RouterErrorImport } from "../errors/RouterError.js";
4
5
  import { ServerError as ServerErrorImport } from "../errors/ServerError.js";
6
+ import { Button as ButtonImport } from "../ui/Button.js";
5
7
  import { Callout as CalloutImport } from "../ui/Callout.js";
6
8
  import {
7
9
  Bootstrap as BootstrapImport,
@@ -19,3 +21,5 @@ export const RouterError = /*@__PURE__*/ RouterErrorImport;
19
21
  export const ServerError = /*@__PURE__*/ ServerErrorImport;
20
22
  export const Bootstrap = /*@__PURE__*/ BootstrapImport;
21
23
  export const BootstrapStatic = /*@__PURE__*/ BootstrapStaticImport;
24
+ export const Button = /*@__PURE__*/ ButtonImport;
25
+ export const Head = /*@__PURE__*/ Helmet;
@@ -114,7 +114,7 @@ export const SideNavigationItem = ({
114
114
  {linkContent}
115
115
  </Collapsible.Trigger>
116
116
  <Collapsible.Content className="CollapsibleContent ms-[calc(var(--padding-nav-item)*1.125)]">
117
- <ul className="mt-1 border-border border-l ps-1.5">
117
+ <ul className="mt-1 border-l ps-1.5">
118
118
  {item.children.map((child) => (
119
119
  <SideNavigationItem
120
120
  key={isPathItem(child) ? child.path : child.href}
@@ -95,7 +95,7 @@ export type ZudokuContextOptions = {
95
95
  };
96
96
 
97
97
  export class DevPortalContext {
98
- private plugins: NonNullable<ZudokuContextOptions["plugins"]> = [];
98
+ public plugins: NonNullable<ZudokuContextOptions["plugins"]> = [];
99
99
  public navigation: ZudokuContextOptions["navigation"];
100
100
  public meta: ZudokuContextOptions["metadata"];
101
101
  public page: ZudokuContextOptions["page"];
@@ -12,6 +12,7 @@ export type PluginNavigationCategory = {
12
12
 
13
13
  export type DevPortalPlugin =
14
14
  | CommonPlugin
15
+ | ProfileMenuPlugin
15
16
  | NavigationPlugin
16
17
  | ApiIdentityPlugin;
17
18
 
@@ -24,11 +25,26 @@ export interface ApiIdentityPlugin {
24
25
  getIdentities: (context: DevPortalContext) => Promise<ApiIdentity[]>;
25
26
  }
26
27
 
28
+ export interface ProfileMenuPlugin {
29
+ getProfileMenuItems: (context: DevPortalContext) => NavigationItem[];
30
+ }
31
+
32
+ export type NavigationItem = {
33
+ label: string;
34
+ path?: string;
35
+ children?: NavigationItem[];
36
+ };
37
+
27
38
  export interface CommonPlugin {
28
39
  initialize?: (context: DevPortalContext) => Promise<void> | void;
29
40
  getHead?: () => ReactElement | undefined;
30
41
  }
31
42
 
43
+ export const isProfileMenuPlugin = (
44
+ obj: DevPortalPlugin,
45
+ ): obj is ProfileMenuPlugin =>
46
+ "getProfileMenuItems" in obj && typeof obj.getProfileMenuItems === "function";
47
+
32
48
  export const isNavigationPlugin = (
33
49
  obj: DevPortalPlugin,
34
50
  ): obj is NavigationPlugin =>
@@ -47,7 +47,7 @@ export const CreateApiKey = ({ service }: { service: ApiKeyService }) => {
47
47
 
48
48
  return (
49
49
  <div className="max-w-screen-lg pt-[--padding-content-top] pb-[--padding-content-bottom]">
50
- <div className="flex justify-between mb-4 border-b border-border pb-1">
50
+ <div className="flex justify-between mb-4 border-b pb-1">
51
51
  <h1 className="font-medium text-2xl">New API Key</h1>
52
52
  </div>
53
53
  <form