zudoku 0.1.1-dev.17 → 0.1.1-dev.19
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.
- package/dist/lib/plugins/openapi/worker/createSharedWorkerClient.js +2 -3
- package/dist/lib/plugins/openapi/worker/createSharedWorkerClient.js.map +1 -1
- package/dist/lib/plugins/openapi/worker/shared-worker.d.ts +1 -0
- package/dist/lib/plugins/openapi/worker/shared-worker.js +6 -0
- package/dist/lib/plugins/openapi/worker/shared-worker.js.map +1 -0
- package/dist/vite/config.d.ts +1 -1
- package/dist/vite/config.js +13 -13
- package/dist/vite/config.js.map +1 -1
- package/dist/vite/dev-server.js +1 -1
- package/dist/vite/dev-server.js.map +1 -1
- package/lib/DevPortal-DqcnbwLT.js +12967 -0
- package/lib/assets/index-BPdJm2ty.js +4764 -0
- package/lib/assets/worker-CnXQsqxH.js +14511 -0
- package/lib/prism-bash.min-DadFsM4Z.js +6 -0
- package/lib/prism-java.min-d5iT_mOd.js +6 -0
- package/lib/prism-json.min-B1GJqK1k.js +1 -0
- package/lib/prism-markup-templating-DZrrEs0A.js +61 -0
- package/lib/prism-php.min-o7FpoMP_.js +10 -0
- package/lib/prism-ruby.min-C7LwcKyz.js +9 -0
- package/lib/zudoku.auth.js +19712 -0
- package/lib/zudoku.components.js +6 -0
- package/lib/zudoku.openapi-worker.js +12 -0
- package/lib/zudoku.plugins.js +17382 -0
- package/package.json +6 -2
- package/src/cli/build/handler.ts +14 -0
- package/src/cli/cli.ts +77 -0
- package/src/cli/cmds/build.ts +24 -0
- package/src/cli/cmds/dev.ts +29 -0
- package/src/cli/common/analytics/lib.ts +89 -0
- package/src/cli/common/constants.ts +10 -0
- package/src/cli/common/logger.ts +5 -0
- package/src/cli/common/machine-id/lib.ts +85 -0
- package/src/cli/common/outdated.ts +102 -0
- package/src/cli/common/output.ts +86 -0
- package/src/cli/common/utils/box.license.txt +202 -0
- package/src/cli/common/utils/box.ts +116 -0
- package/src/cli/common/utils/ports.ts +21 -0
- package/src/cli/common/validators/lib.ts +43 -0
- package/src/cli/common/xdg/lib.ts +36 -0
- package/src/cli/dev/handler.ts +42 -0
- package/src/config/config.ts +56 -0
- package/src/index.ts +8 -0
- package/src/lib/DevPortal.tsx +93 -0
- package/src/lib/Heading.tsx +60 -0
- package/src/lib/Router.tsx +28 -0
- package/src/lib/auth.ts +1 -0
- package/src/lib/authentication/authentication.ts +18 -0
- package/src/lib/authentication/clerk.ts +45 -0
- package/src/lib/authentication/openid.ts +192 -0
- package/src/lib/components/AnchorLink.tsx +19 -0
- package/src/lib/components/CategoryHeading.tsx +16 -0
- package/src/lib/components/Dialog.tsx +119 -0
- package/src/lib/components/DynamicIcon.tsx +60 -0
- package/src/lib/components/Header.tsx +69 -0
- package/src/lib/components/Input.tsx +24 -0
- package/src/lib/components/Layout.tsx +56 -0
- package/src/lib/components/Markdown.tsx +37 -0
- package/src/lib/components/SyntaxHighlight.tsx +94 -0
- package/src/lib/components/TopNavigation.tsx +32 -0
- package/src/lib/components/context/ComponentsContext.tsx +24 -0
- package/src/lib/components/context/DevPortalProvider.ts +54 -0
- package/src/lib/components/context/PluginSystem.ts +0 -0
- package/src/lib/components/context/ThemeContext.tsx +46 -0
- package/src/lib/components/context/ViewportAnchorContext.tsx +139 -0
- package/src/lib/components/navigation/SideNavigation.tsx +18 -0
- package/src/lib/components/navigation/SideNavigationCategory.tsx +74 -0
- package/src/lib/components/navigation/SideNavigationItem.tsx +143 -0
- package/src/lib/components/navigation/SideNavigationWrapper.tsx +15 -0
- package/src/lib/components/navigation/useNavigationCollapsibleState.ts +27 -0
- package/src/lib/components/navigation/util.ts +38 -0
- package/src/lib/components.ts +3 -0
- package/src/lib/core/DevPortalContext.ts +164 -0
- package/src/lib/core/helmet.ts +5 -0
- package/src/lib/core/icons.tsx +1 -0
- package/src/lib/core/plugins.ts +43 -0
- package/src/lib/core/router.tsx +1 -0
- package/src/lib/core/types/combine.ts +16 -0
- package/src/lib/oas/graphql/index.ts +422 -0
- package/src/lib/oas/graphql/server.ts +10 -0
- package/src/lib/oas/parser/dereference/index.ts +59 -0
- package/src/lib/oas/parser/dereference/resolveRef.ts +32 -0
- package/src/lib/oas/parser/index.ts +94 -0
- package/src/lib/oas/parser/schemas/v3.0.json +1489 -0
- package/src/lib/oas/parser/schemas/v3.1.json +1298 -0
- package/src/lib/oas/parser/upgrade/index.ts +108 -0
- package/src/lib/plugins/api-key/SettingsApiKeys.tsx +22 -0
- package/src/lib/plugins/api-key/index.tsx +123 -0
- package/src/lib/plugins/markdown/MdxPage.tsx +128 -0
- package/src/lib/plugins/markdown/Toc.tsx +122 -0
- package/src/lib/plugins/markdown/generateRoutes.tsx +72 -0
- package/src/lib/plugins/markdown/index.tsx +31 -0
- package/src/lib/plugins/openapi/ColorizedParam.tsx +82 -0
- package/src/lib/plugins/openapi/MakeRequest.tsx +49 -0
- package/src/lib/plugins/openapi/MethodBadge.tsx +36 -0
- package/src/lib/plugins/openapi/OperationList.tsx +117 -0
- package/src/lib/plugins/openapi/OperationListItem.tsx +55 -0
- package/src/lib/plugins/openapi/ParameterList.tsx +32 -0
- package/src/lib/plugins/openapi/ParameterListItem.tsx +60 -0
- package/src/lib/plugins/openapi/RequestBodySidecarBox.tsx +51 -0
- package/src/lib/plugins/openapi/ResponsesSidecarBox.tsx +60 -0
- package/src/lib/plugins/openapi/Select.tsx +35 -0
- package/src/lib/plugins/openapi/Sidecar.tsx +160 -0
- package/src/lib/plugins/openapi/SidecarBox.tsx +36 -0
- package/src/lib/plugins/openapi/graphql/fragment-masking.ts +111 -0
- package/src/lib/plugins/openapi/graphql/gql.ts +70 -0
- package/src/lib/plugins/openapi/graphql/graphql.ts +795 -0
- package/src/lib/plugins/openapi/graphql/index.ts +2 -0
- package/src/lib/plugins/openapi/index.tsx +142 -0
- package/src/lib/plugins/openapi/playground/Playground.tsx +309 -0
- package/src/lib/plugins/openapi/queries.graphql +6 -0
- package/src/lib/plugins/openapi/util/generateSchemaExample.ts +59 -0
- package/src/lib/plugins/openapi/util/urql.ts +8 -0
- package/src/lib/plugins/openapi/worker/createSharedWorkerClient.ts +60 -0
- package/src/lib/plugins/openapi/worker/shared-worker.ts +5 -0
- package/src/lib/plugins/openapi/worker/worker.ts +30 -0
- package/src/lib/plugins/redirect/index.tsx +20 -0
- package/src/lib/plugins.ts +5 -0
- package/src/lib/ui/Button.tsx +56 -0
- package/src/lib/ui/Callout.tsx +87 -0
- package/src/lib/ui/Card.tsx +82 -0
- package/src/lib/ui/Note.tsx +58 -0
- package/src/lib/ui/Tabs.tsx +52 -0
- package/src/lib/util/MdxComponents.tsx +70 -0
- package/src/lib/util/cn.ts +6 -0
- package/src/lib/util/createVariantComponent.tsx +30 -0
- package/src/lib/util/createWaitForNotify.ts +18 -0
- package/src/lib/util/groupBy.ts +24 -0
- package/src/lib/util/joinPath.tsx +10 -0
- package/src/lib/util/pastellize.ts +25 -0
- package/src/lib/util/slugify.ts +3 -0
- package/src/lib/util/traverseNavigation.ts +55 -0
- package/src/lib/util/useScrollToAnchor.ts +38 -0
- package/src/lib/util/useScrollToTop.ts +13 -0
- package/src/ts.ts +94 -0
- package/src/types.d.ts +24 -0
- package/src/vite/build.ts +33 -0
- package/src/vite/config.test.ts +10 -0
- package/src/vite/config.ts +183 -0
- package/src/vite/dev-server.ts +64 -0
- package/src/vite/html.ts +37 -0
- package/src/vite/plugin-api.ts +57 -0
- package/src/vite/plugin-auth.ts +32 -0
- package/src/vite/plugin-component.ts +26 -0
- package/src/vite/plugin-config.ts +31 -0
- package/src/vite/plugin-docs.test.ts +32 -0
- package/src/vite/plugin-docs.ts +52 -0
- package/src/vite/plugin-html.ts +50 -0
- package/src/vite/plugin-mdx.ts +74 -0
- package/src/vite/plugin-metadata.ts +30 -0
- package/src/vite/plugin.ts +23 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
|
3
|
+
import { Layout } from "./components/Layout.js";
|
|
4
|
+
import { DevPortalPlugin, isNavigationPlugin } from "./core/plugins.js";
|
|
5
|
+
|
|
6
|
+
export function Router({ plugins }: { plugins?: DevPortalPlugin[] }) {
|
|
7
|
+
const router = useMemo(() => {
|
|
8
|
+
const routes = (plugins ?? []).flatMap((plugin) =>
|
|
9
|
+
isNavigationPlugin(plugin) ? plugin.getRoutes() : [],
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
return createBrowserRouter([
|
|
13
|
+
{
|
|
14
|
+
path: "/",
|
|
15
|
+
element: <Layout />,
|
|
16
|
+
errorElement: (
|
|
17
|
+
<Layout>
|
|
18
|
+
<div className="h-[75vh] flex items-center justify-center">
|
|
19
|
+
Error, look at the console
|
|
20
|
+
</div>
|
|
21
|
+
</Layout>
|
|
22
|
+
),
|
|
23
|
+
children: routes,
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
}, [plugins]);
|
|
27
|
+
return <RouterProvider router={router} />;
|
|
28
|
+
}
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { clerkAuth } from "./authentication/clerk.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DevPortalContext } from "../core/DevPortalContext.js";
|
|
2
|
+
|
|
3
|
+
export interface AuthProvider {
|
|
4
|
+
initialize(context: DevPortalContext): Promise<unknown>;
|
|
5
|
+
login(context: DevPortalContext): Promise<unknown>;
|
|
6
|
+
getToken?: (context: DevPortalContext) => Promise<string | undefined>;
|
|
7
|
+
handleAuthenticationResponse?: (
|
|
8
|
+
path: Path,
|
|
9
|
+
context: DevPortalContext,
|
|
10
|
+
) => Promise<unknown>;
|
|
11
|
+
signOut(context: DevPortalContext): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Path = {
|
|
15
|
+
pathname: string;
|
|
16
|
+
search: string;
|
|
17
|
+
hash: string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Clerk } from "@clerk/clerk-js";
|
|
2
|
+
import { type AuthProvider } from "./authentication.js";
|
|
3
|
+
|
|
4
|
+
export const clerkAuth = ({
|
|
5
|
+
clerkPubKey,
|
|
6
|
+
}: {
|
|
7
|
+
clerkPubKey: string;
|
|
8
|
+
}): AuthProvider => {
|
|
9
|
+
const clerkApi = new Clerk(clerkPubKey);
|
|
10
|
+
|
|
11
|
+
const clerkIsLoaded = clerkApi.load({});
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
initialize: async (context) => {
|
|
15
|
+
await clerkIsLoaded;
|
|
16
|
+
|
|
17
|
+
if (clerkApi.session) {
|
|
18
|
+
await context.setUserProfile({
|
|
19
|
+
isLoggedIn: true,
|
|
20
|
+
name: clerkApi.session.user.fullName ?? undefined,
|
|
21
|
+
email:
|
|
22
|
+
clerkApi.session.user.emailAddresses.at(0)?.emailAddress ??
|
|
23
|
+
undefined,
|
|
24
|
+
});
|
|
25
|
+
} else {
|
|
26
|
+
await context.setUserProfile({
|
|
27
|
+
isLoggedIn: false,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
getToken: async () => {
|
|
33
|
+
await clerkIsLoaded;
|
|
34
|
+
const token = await clerkApi.session?.getToken();
|
|
35
|
+
return token ?? undefined;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
signOut() {
|
|
39
|
+
clerkApi.signOut();
|
|
40
|
+
},
|
|
41
|
+
login: async () => {
|
|
42
|
+
await clerkApi.redirectToSignIn();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as oauth from "oauth4webapi";
|
|
2
|
+
import { DevPortalContext } from "../core/DevPortalContext.js";
|
|
3
|
+
import { type AuthProvider } from "./authentication.js";
|
|
4
|
+
const algorithm = "oauth2";
|
|
5
|
+
|
|
6
|
+
const getAuthServerByIssuer = async (issuer: string) => {
|
|
7
|
+
const authorizationServer = await oauth
|
|
8
|
+
.discoveryRequest(new URL(issuer), { algorithm })
|
|
9
|
+
.then((response) =>
|
|
10
|
+
oauth.processDiscoveryResponse(new URL(issuer), response),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
return authorizationServer;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type AuthServerOption = {
|
|
17
|
+
issuer?: string;
|
|
18
|
+
authorizationEndpoint?: string;
|
|
19
|
+
tokenEndpoint?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getAuthServer = async ({
|
|
23
|
+
issuer,
|
|
24
|
+
authorizationEndpoint,
|
|
25
|
+
tokenEndpoint,
|
|
26
|
+
}: AuthServerOption) => {
|
|
27
|
+
return issuer
|
|
28
|
+
? await getAuthServerByIssuer(issuer)
|
|
29
|
+
: ({
|
|
30
|
+
issuer: new URL(authorizationEndpoint!).origin,
|
|
31
|
+
authorization_endpoint: authorizationEndpoint,
|
|
32
|
+
token_endpoint: tokenEndpoint,
|
|
33
|
+
code_challenge_methods_supported: [],
|
|
34
|
+
} satisfies oauth.AuthorizationServer);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const openIdAuth = ({
|
|
38
|
+
clientId,
|
|
39
|
+
clientSecret,
|
|
40
|
+
issuer,
|
|
41
|
+
authorizationEndpoint,
|
|
42
|
+
tokenEndpoint,
|
|
43
|
+
}: {
|
|
44
|
+
clientId: string;
|
|
45
|
+
clientSecret?: string;
|
|
46
|
+
audience?: string;
|
|
47
|
+
} & AuthServerOption): AuthProvider => {
|
|
48
|
+
const client: oauth.Client = {
|
|
49
|
+
client_id: clientId,
|
|
50
|
+
client_secret: clientSecret,
|
|
51
|
+
token_endpoint_auth_method: "none",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const redirect_uri = "http://localhost:5173/oauth/callback";
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
initialize() {
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
},
|
|
60
|
+
signOut() {},
|
|
61
|
+
login: async (context: DevPortalContext) => {
|
|
62
|
+
const code_challenge_method = "S256";
|
|
63
|
+
const authorizationServer = await getAuthServer({
|
|
64
|
+
issuer,
|
|
65
|
+
authorizationEndpoint,
|
|
66
|
+
tokenEndpoint,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!authorizationServer.authorization_endpoint) {
|
|
70
|
+
throw new Error("No authorization endpoint");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The following MUST be generated for every redirect to the authorization_endpoint. You must store
|
|
75
|
+
* the codeVerifier and nonce in the end-user session such that it can be recovered as the user
|
|
76
|
+
* gets redirected from the authorization server back to your application.
|
|
77
|
+
*/
|
|
78
|
+
const codeVerifier = oauth.generateRandomCodeVerifier();
|
|
79
|
+
const codeChallenge =
|
|
80
|
+
await oauth.calculatePKCECodeChallenge(codeVerifier);
|
|
81
|
+
|
|
82
|
+
await context.sessionStorage.set("codeVerifier", codeVerifier);
|
|
83
|
+
|
|
84
|
+
// redirect user to as.authorization_endpoint
|
|
85
|
+
const authorizationUrl = new URL(
|
|
86
|
+
authorizationServer.authorization_endpoint,
|
|
87
|
+
);
|
|
88
|
+
authorizationUrl.searchParams.set("client_id", client.client_id);
|
|
89
|
+
authorizationUrl.searchParams.set("redirect_uri", redirect_uri);
|
|
90
|
+
authorizationUrl.searchParams.set("response_type", "code");
|
|
91
|
+
// authorizationUrl.searchParams.set("scope", "api:read");
|
|
92
|
+
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
|
|
93
|
+
authorizationUrl.searchParams.set(
|
|
94
|
+
"code_challenge_method",
|
|
95
|
+
code_challenge_method,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is
|
|
100
|
+
* backwards compatible even if the AS doesn't support it which is why we're using it regardless.
|
|
101
|
+
*/
|
|
102
|
+
if (
|
|
103
|
+
authorizationServer.code_challenge_methods_supported?.includes(
|
|
104
|
+
"S256",
|
|
105
|
+
) !== true
|
|
106
|
+
) {
|
|
107
|
+
const state = oauth.generateRandomState();
|
|
108
|
+
authorizationUrl.searchParams.set("state", state);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// now redirect the user to authorizationUrl.href
|
|
112
|
+
location.href = authorizationUrl.href;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
handleAuthenticationResponse: async ({ search }, { sessionStorage }) => {
|
|
116
|
+
const searchParams = new URLSearchParams(search);
|
|
117
|
+
const state = searchParams.get("state");
|
|
118
|
+
if (!state) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// one eternity later, the user lands back on the redirect_uri
|
|
123
|
+
// Authorization Code Grant Request & Response
|
|
124
|
+
const codeVerifier = await sessionStorage.get("codeVerifier");
|
|
125
|
+
|
|
126
|
+
if (!codeVerifier) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const as = await getAuthServer({
|
|
131
|
+
issuer,
|
|
132
|
+
authorizationEndpoint,
|
|
133
|
+
tokenEndpoint,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const params = oauth.validateAuthResponse(
|
|
137
|
+
as,
|
|
138
|
+
client,
|
|
139
|
+
searchParams,
|
|
140
|
+
state,
|
|
141
|
+
);
|
|
142
|
+
if (oauth.isOAuth2Error(params)) {
|
|
143
|
+
console.error("Error Response", params);
|
|
144
|
+
throw new Error(); // Handle OAuth 2.0 redirect error
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const response = await oauth.authorizationCodeGrantRequest(
|
|
148
|
+
as,
|
|
149
|
+
client,
|
|
150
|
+
params,
|
|
151
|
+
redirect_uri,
|
|
152
|
+
codeVerifier,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// @todo do we need to do these
|
|
156
|
+
// const challenges = oauth.parseWwwAuthenticateChallenges(response);
|
|
157
|
+
// if (challenges) {
|
|
158
|
+
// for (const challenge of challenges) {
|
|
159
|
+
// console.error("WWW-Authenticate Challenge", challenge);
|
|
160
|
+
// }
|
|
161
|
+
// throw new Error(); // Handle WWW-Authenticate Challenges as needed
|
|
162
|
+
// }
|
|
163
|
+
|
|
164
|
+
const oauthResult = await oauth.processAuthorizationCodeOpenIDResponse(
|
|
165
|
+
as,
|
|
166
|
+
client,
|
|
167
|
+
response,
|
|
168
|
+
);
|
|
169
|
+
if (oauth.isOAuth2Error(oauthResult)) {
|
|
170
|
+
console.error("Error Response", oauthResult);
|
|
171
|
+
throw new Error(oauthResult.error);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const userInfoResponse = await oauth.userInfoRequest(
|
|
175
|
+
as,
|
|
176
|
+
client,
|
|
177
|
+
oauthResult.access_token,
|
|
178
|
+
);
|
|
179
|
+
const userInfo = await userInfoResponse.json();
|
|
180
|
+
|
|
181
|
+
// void storage.setProfile({
|
|
182
|
+
// sub: userInfo.sub,
|
|
183
|
+
// email: userInfo.email,
|
|
184
|
+
// name: userInfo.name,
|
|
185
|
+
// email_verified: userInfo.email_verified ?? false,
|
|
186
|
+
// picture: userInfo.picture,
|
|
187
|
+
// id_token: oauthResult.id_token,
|
|
188
|
+
// access_token: oauthResult.access_token,
|
|
189
|
+
// });
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type LinkProps, useLocation } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Link that scrolls to anchor even if the hash is already set in the URL.
|
|
6
|
+
*/
|
|
7
|
+
export const AnchorLink = (props: LinkProps) => {
|
|
8
|
+
const location = useLocation();
|
|
9
|
+
const hash = typeof props.to === "string" ? props.to : props.to.hash;
|
|
10
|
+
|
|
11
|
+
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
|
12
|
+
if (!hash?.startsWith("#") || hash !== location.hash) return;
|
|
13
|
+
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
document.getElementById(hash.slice(1))?.scrollIntoView();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return <Link onClick={handleClick} {...props} />;
|
|
19
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cx } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
export const CategoryHeading = ({
|
|
5
|
+
children,
|
|
6
|
+
className,
|
|
7
|
+
}: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className={cx("text-sm font-semibold text-primary mb-2", className)}>
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
3
|
+
import { XIcon } from "lucide-react"
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
5
|
+
|
|
6
|
+
const Dialog = DialogPrimitive.Root
|
|
7
|
+
|
|
8
|
+
const DialogTrigger = DialogPrimitive.Trigger
|
|
9
|
+
|
|
10
|
+
const DialogPortal = DialogPrimitive.Portal
|
|
11
|
+
|
|
12
|
+
const DialogClose = DialogPrimitive.Close
|
|
13
|
+
|
|
14
|
+
const DialogOverlay = React.forwardRef<
|
|
15
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
16
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
17
|
+
>(({ className, ...props }, ref) => (
|
|
18
|
+
<DialogPrimitive.Overlay
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn(
|
|
21
|
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
))
|
|
27
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
28
|
+
|
|
29
|
+
const DialogContent = React.forwardRef<
|
|
30
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
31
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
32
|
+
>(({ className, children, ...props }, ref) => (
|
|
33
|
+
<DialogPortal>
|
|
34
|
+
<DialogOverlay />
|
|
35
|
+
<DialogPrimitive.Content
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn(
|
|
38
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
45
|
+
<XIcon className="h-4 w-4" />
|
|
46
|
+
<span className="sr-only">Close</span>
|
|
47
|
+
</DialogPrimitive.Close>
|
|
48
|
+
</DialogPrimitive.Content>
|
|
49
|
+
</DialogPortal>
|
|
50
|
+
))
|
|
51
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
52
|
+
|
|
53
|
+
const DialogHeader = ({
|
|
54
|
+
className,
|
|
55
|
+
...props
|
|
56
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
57
|
+
<div
|
|
58
|
+
className={cn(
|
|
59
|
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
DialogHeader.displayName = "DialogHeader"
|
|
66
|
+
|
|
67
|
+
const DialogFooter = ({
|
|
68
|
+
className,
|
|
69
|
+
...props
|
|
70
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
DialogFooter.displayName = "DialogFooter"
|
|
80
|
+
|
|
81
|
+
const DialogTitle = React.forwardRef<
|
|
82
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
83
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
84
|
+
>(({ className, ...props }, ref) => (
|
|
85
|
+
<DialogPrimitive.Title
|
|
86
|
+
ref={ref}
|
|
87
|
+
className={cn(
|
|
88
|
+
"text-lg font-semibold leading-none tracking-tight",
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
))
|
|
94
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
95
|
+
|
|
96
|
+
const DialogDescription = React.forwardRef<
|
|
97
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
98
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
99
|
+
>(({ className, ...props }, ref) => (
|
|
100
|
+
<DialogPrimitive.Description
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
))
|
|
106
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
Dialog,
|
|
110
|
+
DialogPortal,
|
|
111
|
+
DialogOverlay,
|
|
112
|
+
DialogTrigger,
|
|
113
|
+
DialogClose,
|
|
114
|
+
DialogContent,
|
|
115
|
+
DialogHeader,
|
|
116
|
+
DialogFooter,
|
|
117
|
+
DialogTitle,
|
|
118
|
+
DialogDescription,
|
|
119
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// import { type LucideProps } from "lucide-react";
|
|
2
|
+
// import dynamicIconImports from "lucide-react/dynamicIconImports.js";
|
|
3
|
+
// import { Suspense, lazy, memo } from "react";
|
|
4
|
+
// import { cn } from "../util/cn.js";
|
|
5
|
+
|
|
6
|
+
// export type IconName = keyof typeof dynamicIconImports;
|
|
7
|
+
|
|
8
|
+
// const IconNames = Object.keys(dynamicIconImports) as IconName[];
|
|
9
|
+
|
|
10
|
+
// export const isValidIcon = (name: unknown): name is IconName =>
|
|
11
|
+
// typeof name === "string" && IconNames.includes(name as IconName);
|
|
12
|
+
|
|
13
|
+
// type IconProps = Omit<LucideProps, "ref"> & {
|
|
14
|
+
// name: IconName;
|
|
15
|
+
// inline?: boolean;
|
|
16
|
+
// };
|
|
17
|
+
|
|
18
|
+
// const Fallback = ({
|
|
19
|
+
// inline,
|
|
20
|
+
// size,
|
|
21
|
+
// }: {
|
|
22
|
+
// inline?: boolean;
|
|
23
|
+
// size?: string | number;
|
|
24
|
+
// }) => (
|
|
25
|
+
// <span
|
|
26
|
+
// className={cn(inline ? "inline-block" : "block", "bg-background")}
|
|
27
|
+
// style={{ height: size, width: size }}
|
|
28
|
+
// role="presentation"
|
|
29
|
+
// />
|
|
30
|
+
// );
|
|
31
|
+
|
|
32
|
+
// export const DynamicIcon = memo(function ({
|
|
33
|
+
// name,
|
|
34
|
+
// inline,
|
|
35
|
+
// className,
|
|
36
|
+
// size = 16,
|
|
37
|
+
// ...props
|
|
38
|
+
// }: IconProps) {
|
|
39
|
+
// const LucideIcon = lazy(dynamicIconImports[name]);
|
|
40
|
+
|
|
41
|
+
// return (
|
|
42
|
+
// <Suspense fallback={<Fallback inline={inline} size={size} />}>
|
|
43
|
+
// <LucideIcon
|
|
44
|
+
// {...props}
|
|
45
|
+
// size={size}
|
|
46
|
+
// className={cn(inline && "inline-block align-[-0.125em]", className)}
|
|
47
|
+
// />
|
|
48
|
+
// </Suspense>
|
|
49
|
+
// );
|
|
50
|
+
// });
|
|
51
|
+
|
|
52
|
+
export type IconName = string;
|
|
53
|
+
|
|
54
|
+
export function isValidIcon(name: unknown): name is IconName {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const DynamicIcon = (props: { name: string; size?: number }) => (
|
|
59
|
+
<div></div>
|
|
60
|
+
);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { MoonStarIcon, SearchIcon, SunIcon } from "lucide-react";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { useDevPortalState } from "../core/DevPortalContext.js";
|
|
4
|
+
import { TopNavigation } from "./TopNavigation.js";
|
|
5
|
+
import { useDevPortal } from "./context/DevPortalProvider.js";
|
|
6
|
+
import { useTheme } from "./context/ThemeContext.js";
|
|
7
|
+
|
|
8
|
+
export const Header = memo(() => {
|
|
9
|
+
const [isDark, toggleTheme] = useTheme();
|
|
10
|
+
const { isLoggedIn, email } = useDevPortalState();
|
|
11
|
+
const { login, logout, meta } = useDevPortal();
|
|
12
|
+
|
|
13
|
+
const ThemeIcon = isDark ? MoonStarIcon : SunIcon;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<header className="fixed top-0 w-full z-10 bg-background/80 backdrop-blur">
|
|
17
|
+
<div className="max-w-screen-2xl mx-auto">
|
|
18
|
+
<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]">
|
|
19
|
+
<div className="flex items-center gap-3.5">
|
|
20
|
+
{meta?.logo && (
|
|
21
|
+
<img src={meta.logo} alt="My Dev Portal" className="h-10" />
|
|
22
|
+
)}
|
|
23
|
+
<span className="font-bold text-2xl text-foreground/85 tracking-wide">
|
|
24
|
+
{meta?.headerTitle}
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
<div className="grid grid-cols-[--sidecar-grid-cols] items-center gap-8">
|
|
28
|
+
<div className="w-full max-w-prose mx-auto">
|
|
29
|
+
<button className="flex items-center border border-input hover:bg-accent hover:text-accent-foreground p-4 relative h-8 justify-start rounded-lg bg-background text-sm text-muted-foreground shadow-none w-40 sm:w-72">
|
|
30
|
+
<div className="flex items-center gap-2 flex-grow">
|
|
31
|
+
<SearchIcon size={14} />
|
|
32
|
+
Search
|
|
33
|
+
</div>
|
|
34
|
+
<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">
|
|
35
|
+
⌘K
|
|
36
|
+
</kbd>
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="items-center justify-self-end text-sm hidden lg:flex">
|
|
40
|
+
{!isLoggedIn ? (
|
|
41
|
+
<button
|
|
42
|
+
className="cursor-pointer hover:bg-secondary p-1 px-2 mx-2 rounded"
|
|
43
|
+
onClick={logout}
|
|
44
|
+
>
|
|
45
|
+
Logout {email ? `(${email})` : null}
|
|
46
|
+
</button>
|
|
47
|
+
) : (
|
|
48
|
+
<button
|
|
49
|
+
className="cursor-pointer hover:bg-secondary p-1 px-2 mx-2 rounded"
|
|
50
|
+
onClick={login}
|
|
51
|
+
>
|
|
52
|
+
Login
|
|
53
|
+
</button>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<button
|
|
57
|
+
className="cursor-pointer hover:bg-secondary p-2.5 -m-2.5 rounded-full"
|
|
58
|
+
onClick={toggleTheme}
|
|
59
|
+
>
|
|
60
|
+
<ThemeIcon size={18} />
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<TopNavigation />
|
|
66
|
+
</div>
|
|
67
|
+
</header>
|
|
68
|
+
);
|
|
69
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../util/cn.js";
|
|
3
|
+
|
|
4
|
+
export interface InputProps
|
|
5
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
6
|
+
|
|
7
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
8
|
+
({ className, type, ...props }, ref) => {
|
|
9
|
+
return (
|
|
10
|
+
<input
|
|
11
|
+
type={type}
|
|
12
|
+
className={cn(
|
|
13
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
ref={ref}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
Input.displayName = "Input";
|
|
23
|
+
|
|
24
|
+
export { Input };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Suspense, useEffect, useRef, type ReactNode } from "react";
|
|
2
|
+
import { Outlet, useLocation } from "react-router-dom";
|
|
3
|
+
import { Helmet } from "../core/helmet.js";
|
|
4
|
+
import { useScrollToAnchor } from "../util/useScrollToAnchor.js";
|
|
5
|
+
import { useScrollToTop } from "../util/useScrollToTop.js";
|
|
6
|
+
import { Header } from "./Header.js";
|
|
7
|
+
import { useDevPortal } from "./context/DevPortalProvider.js";
|
|
8
|
+
import { useViewportAnchor } from "./context/ViewportAnchorContext.js";
|
|
9
|
+
import { SideNavigation } from "./navigation/SideNavigation.js";
|
|
10
|
+
import { SideNavigationWrapper } from "./navigation/SideNavigationWrapper.js";
|
|
11
|
+
|
|
12
|
+
export const Layout = ({ children }: { children?: ReactNode }) => {
|
|
13
|
+
const location = useLocation();
|
|
14
|
+
const { setActiveAnchor } = useViewportAnchor();
|
|
15
|
+
const { meta, handleAuthenticationResponse } = useDevPortal();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
void handleAuthenticationResponse(location);
|
|
19
|
+
}, [handleAuthenticationResponse, location]);
|
|
20
|
+
|
|
21
|
+
useScrollToAnchor();
|
|
22
|
+
useScrollToTop();
|
|
23
|
+
|
|
24
|
+
const previousLocationPath = useRef(location.pathname);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
// always reset on location change
|
|
28
|
+
if (location.pathname !== previousLocationPath.current) {
|
|
29
|
+
setActiveAnchor("");
|
|
30
|
+
}
|
|
31
|
+
previousLocationPath.current = location.pathname;
|
|
32
|
+
}, [location.pathname, setActiveAnchor]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<Helmet titleTemplate={meta?.pageTitle}>
|
|
37
|
+
<title>Home</title>
|
|
38
|
+
{meta?.description && (
|
|
39
|
+
<meta name="description" content={meta.description} />
|
|
40
|
+
)}
|
|
41
|
+
{meta?.favicon && <link rel="icon" href={meta.favicon} />}
|
|
42
|
+
</Helmet>
|
|
43
|
+
<Header />
|
|
44
|
+
<div className="max-w-screen-2xl mx-auto pt-[--header-height] px-10 lg:px-12">
|
|
45
|
+
<Suspense
|
|
46
|
+
fallback={<SideNavigationWrapper>Loading...</SideNavigationWrapper>}
|
|
47
|
+
>
|
|
48
|
+
<SideNavigation />
|
|
49
|
+
</Suspense>
|
|
50
|
+
<main className="dark:border-white/10 lg:overflow-visible translate-x-0 lg:w-[calc(100%-var(--side-nav-width))] lg:translate-x-[--side-nav-width] pl-12">
|
|
51
|
+
{children ?? <Outlet />}
|
|
52
|
+
</main>
|
|
53
|
+
</div>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
};
|