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,108 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type { OpenAPIDocument } from "../index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Upgrade from OpenAPI 3.0.x to 3.1.0
|
|
5
|
+
*
|
|
6
|
+
* Taken from https://github.com/scalar/openapi-parser/blob/main/packages/openapi-parser/src/utils/upgradeFromThreeToThreeOne.ts
|
|
7
|
+
* https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0
|
|
8
|
+
*/
|
|
9
|
+
export function traverse(
|
|
10
|
+
specification: Record<string, any>,
|
|
11
|
+
transform: (specification: Record<string, any>) => Record<string, any>,
|
|
12
|
+
) {
|
|
13
|
+
const result: Record<string, any> = {};
|
|
14
|
+
|
|
15
|
+
for (const [key, value] of Object.entries(specification)) {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
result[key] = value.map((item) =>
|
|
18
|
+
typeof item === "object" && item !== null
|
|
19
|
+
? traverse(item, transform)
|
|
20
|
+
: item,
|
|
21
|
+
);
|
|
22
|
+
} else if (typeof value === "object" && value !== null) {
|
|
23
|
+
result[key] = traverse(value, transform);
|
|
24
|
+
} else {
|
|
25
|
+
result[key] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return transform(result);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const upgradeSchema = (schema: Record<string, any>): OpenAPIDocument => {
|
|
33
|
+
if (schema.openapi?.startsWith("3.0")) {
|
|
34
|
+
schema.openapi = "3.1.0";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
schema = traverse(schema, (sub) => {
|
|
38
|
+
if (sub.type !== "undefined" && sub.nullable === true) {
|
|
39
|
+
sub.type = ["null", sub.type];
|
|
40
|
+
delete sub.nullable;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return sub;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
schema = traverse(schema, (sub) => {
|
|
47
|
+
if (sub.exclusiveMinimum === true) {
|
|
48
|
+
sub.exclusiveMinimum = sub.minimum;
|
|
49
|
+
delete sub.minimum;
|
|
50
|
+
} else if (sub.exclusiveMinimum === false) {
|
|
51
|
+
delete sub.exclusiveMinimum;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (sub.exclusiveMaximum === true) {
|
|
55
|
+
sub.exclusiveMaximum = sub.maximum;
|
|
56
|
+
delete sub.maximum;
|
|
57
|
+
} else if (sub.exclusiveMaximum === false) {
|
|
58
|
+
delete sub.exclusiveMaximum;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return sub;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
schema = traverse(schema, (sub) => {
|
|
65
|
+
if (sub.example !== undefined) {
|
|
66
|
+
sub.examples = {
|
|
67
|
+
default: sub.example,
|
|
68
|
+
};
|
|
69
|
+
delete sub.example;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return sub;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
schema = traverse(schema, (sub) => {
|
|
76
|
+
if (sub.type === "object" && sub.properties !== undefined) {
|
|
77
|
+
for (const [, value] of Object.entries(sub.properties)) {
|
|
78
|
+
const v = (value ?? {}) as Record<string, any>;
|
|
79
|
+
if (v.type === "string" && v.format === "binary") {
|
|
80
|
+
v.contentEncoding = "application/octet-stream";
|
|
81
|
+
delete v.format;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return sub;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
schema = traverse(schema, (sub) => {
|
|
89
|
+
if (sub.type === "string" && sub.format === "binary") {
|
|
90
|
+
return undefined as any;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return sub;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
schema = traverse(schema, (sub) => {
|
|
97
|
+
if (sub.type === "string" && sub.format === "base64") {
|
|
98
|
+
return {
|
|
99
|
+
type: "string",
|
|
100
|
+
contentEncoding: "base64",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return sub;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return schema as OpenAPIDocument;
|
|
108
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useApiIdentities } from "../../components/context/DevPortalProvider.js";
|
|
2
|
+
import { type ApiKeyPluginOptions } from "./index.js";
|
|
3
|
+
|
|
4
|
+
export const SettingsApiKeys = ({
|
|
5
|
+
options,
|
|
6
|
+
}: {
|
|
7
|
+
options: ApiKeyPluginOptions;
|
|
8
|
+
}) => {
|
|
9
|
+
const keys = useApiIdentities();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="prose dark:prose-invert">
|
|
13
|
+
<h1>Who's who</h1>
|
|
14
|
+
<button className="border">Create API Key</button>
|
|
15
|
+
<ul>
|
|
16
|
+
{keys.data.map((key) => (
|
|
17
|
+
<li>{key.name}</li>
|
|
18
|
+
))}
|
|
19
|
+
</ul>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { DevPortalSystemPaths } from "../../DevPortal.js";
|
|
2
|
+
import { DevPortalContext } from "../../core/DevPortalContext.js";
|
|
3
|
+
import {
|
|
4
|
+
type ApiIdentityPlugin,
|
|
5
|
+
type DevPortalPlugin,
|
|
6
|
+
} from "../../core/plugins.js";
|
|
7
|
+
import { SettingsApiKeys } from "./SettingsApiKeys.js";
|
|
8
|
+
|
|
9
|
+
export type GetConsumerOptions =
|
|
10
|
+
| {
|
|
11
|
+
getConsumers: (context: DevPortalContext) => Promise<ConsumerData | void>;
|
|
12
|
+
}
|
|
13
|
+
| { consumerEndpoint: string };
|
|
14
|
+
|
|
15
|
+
export type ApiKeyPluginOptions = {
|
|
16
|
+
rollKey?: (
|
|
17
|
+
id: string,
|
|
18
|
+
consumerName: string,
|
|
19
|
+
context: DevPortalContext,
|
|
20
|
+
) => Promise<void>;
|
|
21
|
+
deleteKey?: (
|
|
22
|
+
id: string,
|
|
23
|
+
consumerName: string,
|
|
24
|
+
context: DevPortalContext,
|
|
25
|
+
) => Promise<void>;
|
|
26
|
+
updateConsumerDescription?: (
|
|
27
|
+
consumerName: string,
|
|
28
|
+
description: string,
|
|
29
|
+
context: DevPortalContext,
|
|
30
|
+
) => Promise<void>;
|
|
31
|
+
createConsumer?: (
|
|
32
|
+
description: string,
|
|
33
|
+
context: DevPortalContext,
|
|
34
|
+
) => Promise<void>;
|
|
35
|
+
deleteConsumer?: (
|
|
36
|
+
consumerName: string,
|
|
37
|
+
context: DevPortalContext,
|
|
38
|
+
) => Promise<void>;
|
|
39
|
+
} & GetConsumerOptions;
|
|
40
|
+
|
|
41
|
+
export interface ConsumerData {
|
|
42
|
+
consumers: Consumer[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Consumer {
|
|
46
|
+
name: string;
|
|
47
|
+
createdOn?: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
apiKeys: ConsumerApiKey[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ConsumerApiKey {
|
|
53
|
+
id: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
createdOn?: string;
|
|
56
|
+
updatedOn?: string;
|
|
57
|
+
expiresOn?: string;
|
|
58
|
+
key: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const defaultGetConsumers = async (context: DevPortalContext) => {
|
|
62
|
+
const accessToken = await context.authentication?.getToken?.(context);
|
|
63
|
+
|
|
64
|
+
if (!accessToken) {
|
|
65
|
+
throw new Error("No token found");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const consumers = await fetch(
|
|
69
|
+
"https://zudoku-customer-main-b36fa2f.d2.zuplo.dev/v1/developer/api-keys",
|
|
70
|
+
{
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: `Bearer ${accessToken}`,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return { consumers: [await consumers.json()] };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const apiKeyPlugin = (
|
|
81
|
+
options: ApiKeyPluginOptions,
|
|
82
|
+
): DevPortalPlugin & ApiIdentityPlugin => {
|
|
83
|
+
const getConsumers =
|
|
84
|
+
"getConsumers" in options ? options.getConsumers : defaultGetConsumers;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
getNavigation: async (path: string) => {
|
|
88
|
+
if (path !== DevPortalSystemPaths.Settings) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
path: "settings/api-keys",
|
|
95
|
+
label: "API Keys",
|
|
96
|
+
children: [],
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
},
|
|
100
|
+
getIdentities: async (context) => {
|
|
101
|
+
return [];
|
|
102
|
+
// throw new Error("Not implemented");
|
|
103
|
+
// const keys = await getConsumers(context);
|
|
104
|
+
// return (keys ?? { consumers: [] }).consumers
|
|
105
|
+
// .flatMap((consumer) => consumer.apiKeys)
|
|
106
|
+
// .map((key) => ({
|
|
107
|
+
// authorizeRequest: (request: Request) => {
|
|
108
|
+
// request.headers.set("Authorization", `Bearer ${key.key}`);
|
|
109
|
+
// },
|
|
110
|
+
// id: key.id,
|
|
111
|
+
// name: key.id,
|
|
112
|
+
// }));
|
|
113
|
+
},
|
|
114
|
+
getRoutes: () => {
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
path: "/settings/api-keys",
|
|
118
|
+
element: <SettingsApiKeys options={options} />,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useMemo, type PropsWithChildren, type ReactNode } from "react";
|
|
2
|
+
import { Heading } from "../../Heading.js";
|
|
3
|
+
import { CategoryHeading } from "../../components/CategoryHeading.js";
|
|
4
|
+
import { ProseClasses } from "../../components/Markdown.js";
|
|
5
|
+
import { useTopNavigationItem } from "../../components/context/DevPortalProvider.js";
|
|
6
|
+
import { isPathItem } from "../../components/navigation/util.js";
|
|
7
|
+
import { Helmet } from "../../core/helmet.js";
|
|
8
|
+
import { Link, useLocation } from "../../core/router.js";
|
|
9
|
+
import { cn } from "../../util/cn.js";
|
|
10
|
+
import slugify from "../../util/slugify.js";
|
|
11
|
+
import { traverseNavigation } from "../../util/traverseNavigation.js";
|
|
12
|
+
import { Toc } from "./Toc.js";
|
|
13
|
+
import type { MDXImport } from "./index.js";
|
|
14
|
+
|
|
15
|
+
export const MdxPage = ({
|
|
16
|
+
children,
|
|
17
|
+
frontmatter,
|
|
18
|
+
tableOfContents,
|
|
19
|
+
}: PropsWithChildren<Omit<MDXImport, "default">>) => {
|
|
20
|
+
const navItem = useTopNavigationItem();
|
|
21
|
+
const location = useLocation();
|
|
22
|
+
|
|
23
|
+
const categoryTitle = navItem
|
|
24
|
+
? traverseNavigation(navItem, (_node, fullPath, parentNodes) => {
|
|
25
|
+
if (fullPath === location.pathname) {
|
|
26
|
+
return parentNodes.at(0)?.label;
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
: undefined;
|
|
30
|
+
|
|
31
|
+
const title = frontmatter?.title;
|
|
32
|
+
const category = frontmatter?.category ?? categoryTitle;
|
|
33
|
+
const hideToc = frontmatter?.toc === false;
|
|
34
|
+
|
|
35
|
+
const pageTitle =
|
|
36
|
+
tableOfContents.find((item) => item.depth === 1)?.value ?? title;
|
|
37
|
+
|
|
38
|
+
const tocEntries =
|
|
39
|
+
tableOfContents.find((item) => item.depth === 1)?.children ??
|
|
40
|
+
// if `title` is provided by frontmatter it does not appear in the table of contents
|
|
41
|
+
tableOfContents.filter((item) => item.depth === 2);
|
|
42
|
+
|
|
43
|
+
const showToc = !hideToc && tocEntries.length > 0;
|
|
44
|
+
|
|
45
|
+
const { prev, next } = useMemo(() => {
|
|
46
|
+
let prev = { path: "", label: "" as ReactNode };
|
|
47
|
+
let next = { path: "", label: "" as ReactNode };
|
|
48
|
+
let shouldStop = false;
|
|
49
|
+
|
|
50
|
+
if (!navItem) return { prev, next };
|
|
51
|
+
|
|
52
|
+
traverseNavigation(navItem, (node, fullPath) => {
|
|
53
|
+
const item = { path: fullPath, label: node.label };
|
|
54
|
+
|
|
55
|
+
if (shouldStop && isPathItem(node) && !node.children?.length) {
|
|
56
|
+
next = item;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (fullPath === location.pathname) {
|
|
60
|
+
shouldStop = true;
|
|
61
|
+
}
|
|
62
|
+
if (!shouldStop && isPathItem(node) && !node.children?.length) {
|
|
63
|
+
prev = item;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { prev, next } as const;
|
|
68
|
+
}, [navItem, location.pathname]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="xl:grid grid-cols-[--sidecar-grid-cols] gap-8 justify-between">
|
|
72
|
+
<Helmet>
|
|
73
|
+
<title>{pageTitle}</title>
|
|
74
|
+
</Helmet>
|
|
75
|
+
<div
|
|
76
|
+
className={cn(
|
|
77
|
+
ProseClasses,
|
|
78
|
+
"mx-auto max-w-full xl:w-full xl:max-w-prose flex-1 flex-shrink pt-[--padding-content-top] pb-[--padding-content-bottom]",
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<header className="-translate-y-1">
|
|
82
|
+
{category && <CategoryHeading>{category}</CategoryHeading>}
|
|
83
|
+
{title && (
|
|
84
|
+
<Heading level={1} id={slugify(title, { lower: true })}>
|
|
85
|
+
{title}
|
|
86
|
+
</Heading>
|
|
87
|
+
)}
|
|
88
|
+
{frontmatter?.description && (
|
|
89
|
+
<p className="prose-lg">{frontmatter.description}</p>
|
|
90
|
+
)}
|
|
91
|
+
</header>
|
|
92
|
+
{children}
|
|
93
|
+
<hr />
|
|
94
|
+
<div className="not-prose flex items-center justify-between gap-8">
|
|
95
|
+
{prev.path ? (
|
|
96
|
+
<Link
|
|
97
|
+
to={prev.path}
|
|
98
|
+
className="flex flex-col items-stretch gap-2 flex-1 truncate border border-border rounded px-6 py-4 text-start hover:border-primary/85 transition shadow-sm hover:shadow-md"
|
|
99
|
+
title={typeof prev.label === "string" ? prev.label : undefined}
|
|
100
|
+
>
|
|
101
|
+
<div className="text-sm text-muted-foreground">
|
|
102
|
+
← Previous page
|
|
103
|
+
</div>
|
|
104
|
+
<div className="text-lg text-primary truncate">{prev.label}</div>
|
|
105
|
+
</Link>
|
|
106
|
+
) : (
|
|
107
|
+
<div className="flex-1" />
|
|
108
|
+
)}
|
|
109
|
+
{next.path ? (
|
|
110
|
+
<Link
|
|
111
|
+
to={next.path}
|
|
112
|
+
className="flex flex-col items-stretch gap-2 flex-1 truncate border border-border rounded px-6 py-4 text-end hover:border-primary/85 transition shadow-sm hover:shadow-md"
|
|
113
|
+
title={typeof next.label === "string" ? next.label : undefined}
|
|
114
|
+
>
|
|
115
|
+
<div className="text-sm text-muted-foreground">Next page →</div>
|
|
116
|
+
<div className="text-lg text-primary truncate">{next.label}</div>
|
|
117
|
+
</Link>
|
|
118
|
+
) : (
|
|
119
|
+
<div className="flex-1" />
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="hidden xl:block">
|
|
124
|
+
{showToc && <Toc entries={tocEntries} />}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { TocEntry } from "@stefanprobst/rehype-extract-toc";
|
|
2
|
+
import {
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type CSSProperties,
|
|
7
|
+
type PropsWithChildren,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { AnchorLink } from "../../components/AnchorLink.js";
|
|
10
|
+
import { useViewportAnchor } from "../../components/context/ViewportAnchorContext.js";
|
|
11
|
+
import { ListTreeIcon } from "../../core/icons.js";
|
|
12
|
+
import { cn } from "../../util/cn.js";
|
|
13
|
+
|
|
14
|
+
const DATA_ANCHOR_ATTR = "data-active";
|
|
15
|
+
|
|
16
|
+
const TocItem = ({
|
|
17
|
+
item,
|
|
18
|
+
children,
|
|
19
|
+
className,
|
|
20
|
+
isActive,
|
|
21
|
+
}: PropsWithChildren<{
|
|
22
|
+
item: TocEntry;
|
|
23
|
+
isActive: boolean;
|
|
24
|
+
className?: string;
|
|
25
|
+
}>) => {
|
|
26
|
+
return (
|
|
27
|
+
<li
|
|
28
|
+
className={cn(
|
|
29
|
+
"truncate",
|
|
30
|
+
isActive
|
|
31
|
+
? "text-primary"
|
|
32
|
+
: "text-foreground/65 dark:text-foreground/75",
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
title={item.value}
|
|
36
|
+
>
|
|
37
|
+
<AnchorLink
|
|
38
|
+
to={`#${item.id}`}
|
|
39
|
+
{...{ [DATA_ANCHOR_ATTR]: item.id }}
|
|
40
|
+
className={cn(
|
|
41
|
+
isActive
|
|
42
|
+
? "text-primary"
|
|
43
|
+
: "text-foreground/65 dark:text-foreground/75 hover:text-foreground",
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{item.value}
|
|
47
|
+
</AnchorLink>
|
|
48
|
+
{children}
|
|
49
|
+
</li>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Toc = ({ entries }: { entries: TocEntry[] }) => {
|
|
54
|
+
const { activeAnchor } = useViewportAnchor();
|
|
55
|
+
const listWrapperRef = useRef<HTMLUListElement>(null);
|
|
56
|
+
const [indicatorStyle, setIndicatorStyles] = useState<CSSProperties>({});
|
|
57
|
+
|
|
58
|
+
// synchronize active anchor indicator with the scroll position
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!listWrapperRef.current) return;
|
|
61
|
+
|
|
62
|
+
const activeElement = listWrapperRef.current.querySelector(
|
|
63
|
+
`[${DATA_ANCHOR_ATTR}='${activeAnchor}']`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (!activeElement) {
|
|
67
|
+
setIndicatorStyles({
|
|
68
|
+
"--indicator-top": "0",
|
|
69
|
+
"--indicator-opacity": 0,
|
|
70
|
+
} as CSSProperties);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const topParent = listWrapperRef.current.getBoundingClientRect().top;
|
|
75
|
+
const topElement = activeElement.getBoundingClientRect().top;
|
|
76
|
+
|
|
77
|
+
setIndicatorStyles({
|
|
78
|
+
"--indicator-top": `${topElement - topParent}px`,
|
|
79
|
+
"--indicator-opacity": 1,
|
|
80
|
+
} as CSSProperties);
|
|
81
|
+
}, [activeAnchor]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<aside className="sticky top-[--header-height] h-[calc(100vh-var(--header-height))] pt-[--padding-content-top] pb-[--padding-content-bottom] overflow-y-auto ps-1 text-sm">
|
|
85
|
+
<div className="flex items-center gap-2 font-medium mb-2">
|
|
86
|
+
<ListTreeIcon size={16} />
|
|
87
|
+
On this page
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<ul
|
|
91
|
+
ref={listWrapperRef}
|
|
92
|
+
style={indicatorStyle}
|
|
93
|
+
className={cn(
|
|
94
|
+
"relative ms-2 ps-4 font-medium list-none mt-0 space-y-2",
|
|
95
|
+
"before:absolute before:inset-0 before:right-auto before:bg-border before:w-[2px]",
|
|
96
|
+
"after:absolute after:-left-px after:-translate-y-1 after:top-[--indicator-top] after:opacity-[--indicator-opacity] after:h-6 after:bg-primary after:w-[4px] after:rounded after:ease-out after:[transition:top__150ms,opacity_325ms]",
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
{entries.map((item) => (
|
|
100
|
+
<TocItem
|
|
101
|
+
isActive={item.id === activeAnchor}
|
|
102
|
+
key={item.id}
|
|
103
|
+
item={item}
|
|
104
|
+
className="pl-0"
|
|
105
|
+
>
|
|
106
|
+
{item.children && (
|
|
107
|
+
<ul className="list-none pl-4 pt-2 space-y-2">
|
|
108
|
+
{item.children.map((child) => (
|
|
109
|
+
<TocItem
|
|
110
|
+
item={child}
|
|
111
|
+
isActive={child.id === activeAnchor}
|
|
112
|
+
key={child.id}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
</ul>
|
|
116
|
+
)}
|
|
117
|
+
</TocItem>
|
|
118
|
+
))}
|
|
119
|
+
</ul>
|
|
120
|
+
</aside>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Heading } from "../../Heading.js";
|
|
2
|
+
import { useTopNavigationItem } from "../../components/context/DevPortalProvider.js";
|
|
3
|
+
import { isPathItem } from "../../components/navigation/util.js";
|
|
4
|
+
import { Navigate, type RouteObject } from "../../core/router.js";
|
|
5
|
+
import type { MdxComponentsType } from "../../util/MdxComponents.js";
|
|
6
|
+
import { traverseNavigation } from "../../util/traverseNavigation.js";
|
|
7
|
+
import { MdxPage } from "./MdxPage.js";
|
|
8
|
+
import type { MarkdownPluginOptions } from "./index.js";
|
|
9
|
+
|
|
10
|
+
const MarkdownHeadings = {
|
|
11
|
+
h2: ({ children, id }) => (
|
|
12
|
+
<Heading level={2} id={id} children={children} registerSidebarAnchor />
|
|
13
|
+
),
|
|
14
|
+
h3: ({ children, id }) => (
|
|
15
|
+
<Heading level={3} id={id} children={children} registerSidebarAnchor />
|
|
16
|
+
),
|
|
17
|
+
} satisfies MdxComponentsType;
|
|
18
|
+
|
|
19
|
+
export const generateRoutes = (
|
|
20
|
+
markdownFiles: MarkdownPluginOptions["markdownFiles"],
|
|
21
|
+
): RouteObject[] => {
|
|
22
|
+
const routes = Object.entries(markdownFiles).flatMap(
|
|
23
|
+
([file, importPromise]) => {
|
|
24
|
+
// @todo we can pass in the folder name and then filter the markdown files based on that path
|
|
25
|
+
const match = file.match(/pages\/(.*).mdx?$/);
|
|
26
|
+
const path = match?.at(1);
|
|
27
|
+
|
|
28
|
+
if (!path) return [];
|
|
29
|
+
|
|
30
|
+
const pathSegments = path.split("/");
|
|
31
|
+
const isIndexFile = pathSegments.at(-1) === "index";
|
|
32
|
+
const routePath = isIndexFile
|
|
33
|
+
? pathSegments.slice(0, -1).join("/")
|
|
34
|
+
: path;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
path: routePath,
|
|
38
|
+
lazy: async () => {
|
|
39
|
+
const { default: Component, ...props } = await importPromise();
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
element: (
|
|
43
|
+
<MdxPage {...props}>
|
|
44
|
+
<Component components={MarkdownHeadings} />
|
|
45
|
+
</MdxPage>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
} satisfies RouteObject;
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const rootRoutes = Array.from(
|
|
54
|
+
new Set(routes.map((route) => route.path.split("/").at(0))),
|
|
55
|
+
).map((dir) => ({
|
|
56
|
+
path: `/${dir}`,
|
|
57
|
+
element: <Redirect />,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
return [...routes, ...rootRoutes];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const Redirect = () => {
|
|
64
|
+
const navItem = useTopNavigationItem();
|
|
65
|
+
|
|
66
|
+
if (!navItem) return null;
|
|
67
|
+
|
|
68
|
+
return traverseNavigation(navItem, (node, fullPath) => {
|
|
69
|
+
if ("children" in node || !isPathItem(node)) return;
|
|
70
|
+
return <Navigate to={fullPath} replace />;
|
|
71
|
+
});
|
|
72
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Toc } from "@stefanprobst/rehype-extract-toc";
|
|
2
|
+
import type { MDXProps } from "mdx/types.js";
|
|
3
|
+
import type { DevPortalPlugin } from "../../core/plugins.js";
|
|
4
|
+
import { generateRoutes } from "./generateRoutes.js";
|
|
5
|
+
|
|
6
|
+
export type MarkdownPluginOptions = {
|
|
7
|
+
markdownFiles: Record<string, () => Promise<MDXImport>>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type Frontmatter = {
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
category?: string;
|
|
14
|
+
toc?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type MDXImport = {
|
|
18
|
+
tableOfContents: Toc;
|
|
19
|
+
frontmatter: Frontmatter;
|
|
20
|
+
default: (props: MDXProps) => JSX.Element;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const markdownPlugin = ({
|
|
24
|
+
markdownFiles,
|
|
25
|
+
}: MarkdownPluginOptions): DevPortalPlugin => {
|
|
26
|
+
return {
|
|
27
|
+
getRoutes() {
|
|
28
|
+
return generateRoutes(markdownFiles);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
2
|
+
import { useTheme } from "../../components/context/ThemeContext.js";
|
|
3
|
+
import { cn } from "../../util/cn.js";
|
|
4
|
+
import { pastellize } from "../../util/pastellize.js";
|
|
5
|
+
|
|
6
|
+
export const DATA_ATTR = "data-linked-param";
|
|
7
|
+
|
|
8
|
+
export const usePastellizedColor = (name: string) => {
|
|
9
|
+
const [isDark] = useTheme();
|
|
10
|
+
return pastellize(
|
|
11
|
+
name,
|
|
12
|
+
!isDark ? { saturation: 100, lightness: 70 } : undefined,
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ColorizedParam = ({
|
|
17
|
+
name,
|
|
18
|
+
value,
|
|
19
|
+
className,
|
|
20
|
+
backgroundOpacity = "100%",
|
|
21
|
+
slug,
|
|
22
|
+
children,
|
|
23
|
+
onClick,
|
|
24
|
+
}: {
|
|
25
|
+
name: string;
|
|
26
|
+
value?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
backgroundOpacity?: string;
|
|
29
|
+
slug?: string;
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
onClick?: () => void;
|
|
32
|
+
}) => {
|
|
33
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
34
|
+
const normalized = name.replace("{", "").replace("}", "");
|
|
35
|
+
const color = usePastellizedColor(normalized);
|
|
36
|
+
|
|
37
|
+
const borderColor = `hsl(${color})`;
|
|
38
|
+
const backgroundColor = `hsl(${color} / ${backgroundOpacity})`;
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!slug) return;
|
|
42
|
+
if (!ref.current) return;
|
|
43
|
+
|
|
44
|
+
const onMouseEnter = () => {
|
|
45
|
+
document.querySelectorAll(`[${DATA_ATTR}="${slug}"]`).forEach((el) => {
|
|
46
|
+
if (el instanceof HTMLElement) {
|
|
47
|
+
el.dataset.active = "true";
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const onMouseLeave = () => {
|
|
52
|
+
document.querySelectorAll(`[${DATA_ATTR}="${slug}"]`).forEach((el) => {
|
|
53
|
+
if (el instanceof HTMLElement) {
|
|
54
|
+
el.dataset.active = "false";
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
ref.current.addEventListener("mouseenter", onMouseEnter);
|
|
60
|
+
ref.current.addEventListener("mouseleave", onMouseLeave);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
ref.current?.removeEventListener("mouseenter", onMouseEnter);
|
|
64
|
+
ref.current?.removeEventListener("mouseleave", onMouseLeave);
|
|
65
|
+
};
|
|
66
|
+
}, [slug]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<span
|
|
70
|
+
className={cn("inline-flex relative rounded group", className)}
|
|
71
|
+
{...{ [DATA_ATTR]: slug }}
|
|
72
|
+
ref={ref}
|
|
73
|
+
onClick={onClick}
|
|
74
|
+
>
|
|
75
|
+
<span
|
|
76
|
+
className="absolute inset-0 border-b-2 transition-opacity duration-200 opacity-30 group-data-[active=true]:opacity-100"
|
|
77
|
+
style={{ borderColor, backgroundColor }}
|
|
78
|
+
/>
|
|
79
|
+
<span className="relative">{children || name}</span>
|
|
80
|
+
</span>
|
|
81
|
+
);
|
|
82
|
+
};
|