@lukoweb/apitogo 0.1.1 → 0.1.2

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 (91) hide show
  1. package/dist/cli/cli.js +17 -6
  2. package/dist/declarations/lib/ui/Command.d.ts +1 -1
  3. package/dist/declarations/lib/util/MdxComponents.d.ts +1 -1
  4. package/package.json +1 -1
  5. package/src/app/main.tsx +150 -150
  6. package/src/config/loader.ts +245 -245
  7. package/src/index.ts +33 -33
  8. package/src/lib/authentication/components/CallbackHandler.tsx +45 -45
  9. package/src/lib/authentication/components/SignIn.tsx +46 -46
  10. package/src/lib/authentication/components/SignUp.tsx +48 -48
  11. package/src/lib/authentication/providers/clerk.tsx +224 -224
  12. package/src/lib/authentication/ui/AuthCard.tsx +7 -7
  13. package/src/lib/authentication/ui/EmailLinkCallbackUi.tsx +129 -125
  14. package/src/lib/authentication/ui/EmailLinkSentUi.tsx +105 -101
  15. package/src/lib/authentication/ui/EmailLinkSignInUi.tsx +100 -96
  16. package/src/lib/authentication/ui/EmailVerificationUi.tsx +128 -124
  17. package/src/lib/authentication/ui/ZudokuAuthUi.tsx +620 -616
  18. package/src/lib/components/AiAssistantMenuItems.tsx +102 -102
  19. package/src/lib/components/Autocomplete.tsx +123 -123
  20. package/src/lib/components/DeveloperHint.tsx +39 -35
  21. package/src/lib/components/Header.tsx +260 -260
  22. package/src/lib/components/Layout.tsx +53 -53
  23. package/src/lib/components/Main.tsx +51 -51
  24. package/src/lib/components/Mermaid.tsx +74 -70
  25. package/src/lib/components/MobileTopNavigation.tsx +276 -276
  26. package/src/lib/components/MultiSelect.tsx +86 -82
  27. package/src/lib/components/TopNavigation.tsx +103 -103
  28. package/src/lib/components/index.ts +26 -26
  29. package/src/lib/components/navigation/NavigationCategory.tsx +157 -157
  30. package/src/lib/components/navigation/NavigationFilterInput.tsx +35 -35
  31. package/src/lib/components/navigation/NavigationItem.tsx +181 -177
  32. package/src/lib/core/RouteGuard.tsx +193 -193
  33. package/src/lib/errors/ErrorMessage.tsx +50 -46
  34. package/src/lib/plugins/api-catalog/Catalog.tsx +80 -80
  35. package/src/lib/plugins/api-keys/CreateApiKey.tsx +128 -124
  36. package/src/lib/plugins/api-keys/CreateApiKeyDialog.tsx +49 -49
  37. package/src/lib/plugins/api-keys/SettingsApiKeys.tsx +72 -72
  38. package/src/lib/plugins/api-keys/settings/ApiKeyItem.tsx +342 -342
  39. package/src/lib/plugins/api-keys/settings/RevealApiKey.tsx +127 -127
  40. package/src/lib/plugins/markdown/MdxPage.tsx +285 -285
  41. package/src/lib/plugins/openapi/ApiHeader.tsx +96 -96
  42. package/src/lib/plugins/openapi/CollapsibleCode.tsx +88 -88
  43. package/src/lib/plugins/openapi/DownloadSchemaButton.tsx +94 -94
  44. package/src/lib/plugins/openapi/GeneratedExampleSidecarBox.tsx +52 -52
  45. package/src/lib/plugins/openapi/OperationListItem.tsx +183 -183
  46. package/src/lib/plugins/openapi/RequestBodySidecarBox.tsx +63 -63
  47. package/src/lib/plugins/openapi/ResponsesSidecarBox.tsx +120 -117
  48. package/src/lib/plugins/openapi/SchemaInfo.tsx +344 -344
  49. package/src/lib/plugins/openapi/SchemaList.tsx +113 -113
  50. package/src/lib/plugins/openapi/Sidecar.tsx +312 -309
  51. package/src/lib/plugins/openapi/SidecarExamples.tsx +177 -174
  52. package/src/lib/plugins/openapi/components/EnumValues.tsx +58 -58
  53. package/src/lib/plugins/openapi/components/ResponseContent.tsx +117 -114
  54. package/src/lib/plugins/openapi/playground/BodyPanel.tsx +274 -271
  55. package/src/lib/plugins/openapi/playground/CollapsibleHeader.tsx +51 -51
  56. package/src/lib/plugins/openapi/playground/ExamplesDropdown.tsx +60 -60
  57. package/src/lib/plugins/openapi/playground/Headers.tsx +188 -181
  58. package/src/lib/plugins/openapi/playground/IdentityDialog.tsx +75 -75
  59. package/src/lib/plugins/openapi/playground/IdentitySelector.tsx +38 -38
  60. package/src/lib/plugins/openapi/playground/ParamsGrid.tsx +45 -45
  61. package/src/lib/plugins/openapi/playground/Playground.tsx +602 -599
  62. package/src/lib/plugins/openapi/playground/PlaygroundDialog.tsx +56 -56
  63. package/src/lib/plugins/openapi/playground/QueryParams.tsx +151 -148
  64. package/src/lib/plugins/openapi/playground/RequestLoginDialog.tsx +70 -70
  65. package/src/lib/plugins/openapi/playground/request-panel/MultipartField.tsx +91 -91
  66. package/src/lib/plugins/openapi/playground/result-panel/AudioPlayer.tsx +50 -50
  67. package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +355 -355
  68. package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +106 -102
  69. package/src/lib/plugins/openapi/schema/SchemaPropertyItem.tsx +189 -184
  70. package/src/lib/plugins/openapi/schema/SchemaView.tsx +299 -299
  71. package/src/lib/plugins/search-pagefind/IndexingDialog.tsx +162 -162
  72. package/src/lib/plugins/search-pagefind/PagefindSearch.tsx +202 -202
  73. package/src/lib/plugins/search-pagefind/ResultList.tsx +122 -118
  74. package/src/lib/ui/ActionButton.tsx +21 -21
  75. package/src/lib/ui/Command.tsx +191 -191
  76. package/src/lib/util/MdxComponents.tsx +150 -150
  77. package/src/vite/config.ts +234 -231
  78. package/src/vite/dev-server.ts +291 -291
  79. package/src/vite/plugin-api-keys.ts +50 -50
  80. package/src/vite/plugin-api.ts +320 -318
  81. package/src/vite/plugin-auth.ts +38 -38
  82. package/src/vite/plugin-component.ts +45 -39
  83. package/src/vite/plugin-config.ts +75 -75
  84. package/src/vite/plugin-custom-pages.ts +36 -36
  85. package/src/vite/plugin-docs.ts +202 -202
  86. package/src/vite/plugin-markdown-export.ts +214 -214
  87. package/src/vite/plugin-mdx.ts +149 -149
  88. package/src/vite/plugin-navigation.ts +106 -106
  89. package/src/vite/plugin-search.ts +47 -47
  90. package/src/vite/plugin.ts +42 -42
  91. package/src/vite/prerender/prerender.ts +233 -233
@@ -1,193 +1,193 @@
1
- import { Helmet } from "@zudoku/react-helmet-async";
2
- import { use, useCallback, useEffect, useMemo } from "react";
3
- import {
4
- matchPath,
5
- Outlet,
6
- useBlocker,
7
- useLocation,
8
- useNavigate,
9
- } from "react-router";
10
- import { Button } from "@lukoweb/apitogo/ui/Button.js";
11
- import {
12
- Dialog,
13
- DialogContent,
14
- DialogDescription,
15
- DialogFooter,
16
- DialogHeader,
17
- DialogTitle,
18
- } from "@lukoweb/apitogo/ui/Dialog.js";
19
- import { REASON_CODES } from "../../config/validators/reason-codes.js";
20
- import { useAuth } from "../authentication/hook.js";
21
- import { RenderContext } from "../components/context/RenderContext.js";
22
- import { useZudoku } from "../components/context/ZudokuContext.js";
23
- import { Layout } from "../components/Layout.js";
24
- import { ZudokuError } from "../util/invariant.js";
25
-
26
- export const SEARCH_PROTECTED_SECTION = "protected";
27
-
28
- type LoginDialogProps = {
29
- open: boolean;
30
- onCancel: () => void;
31
- onLogin: () => void;
32
- onRegister: () => void;
33
- };
34
-
35
- const LoginDialog = ({
36
- open,
37
- onCancel,
38
- onLogin,
39
- onRegister,
40
- }: LoginDialogProps) => (
41
- <Dialog open={open} onOpenChange={(nextOpen) => !nextOpen && onCancel()}>
42
- <DialogContent>
43
- <DialogHeader>
44
- <DialogTitle>Login to continue</DialogTitle>
45
- </DialogHeader>
46
- <DialogDescription>Please login to access this page.</DialogDescription>
47
- <DialogFooter>
48
- <Button variant="outline" onClick={onCancel}>
49
- Cancel
50
- </Button>
51
- <div className="w-full" />
52
- <Button variant="secondary" onClick={onRegister}>
53
- Register
54
- </Button>
55
- <Button onClick={onLogin}>Login</Button>
56
- </DialogFooter>
57
- </DialogContent>
58
- </Dialog>
59
- );
60
-
61
- const BypassRoute = ({ isProtectedRoute }: { isProtectedRoute: boolean }) => (
62
- <>
63
- {isProtectedRoute && (
64
- <Helmet>
65
- <meta
66
- name="pagefind"
67
- data-pagefind-filter={`section:${SEARCH_PROTECTED_SECTION}`}
68
- content="true"
69
- />
70
- </Helmet>
71
- )}
72
- <Outlet />
73
- </>
74
- );
75
-
76
- const ForbiddenPage = () => {
77
- const renderContext = use(RenderContext);
78
- renderContext.status = 403;
79
-
80
- return (
81
- <Layout>
82
- <div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
83
- <h1 className="text-2xl font-bold">Access Denied</h1>
84
- <p className="text-muted-foreground">
85
- You don't have permission to access this page.
86
- </p>
87
- </div>
88
- </Layout>
89
- );
90
- };
91
-
92
- export const RouteGuard = () => {
93
- const auth = useAuth();
94
- const zudoku = useZudoku();
95
- const navigate = useNavigate();
96
- const location = useLocation();
97
- const renderContext = use(RenderContext);
98
- const shouldBypass = renderContext.bypassProtection;
99
- const authCheckContext = useMemo(
100
- () => ({ auth, context: zudoku, reasonCode: REASON_CODES }),
101
- [auth, zudoku],
102
- );
103
- const { protectedRoutes } = zudoku;
104
-
105
- const getAuthCheck = useCallback(
106
- (pathname: string) => {
107
- if (!protectedRoutes) return;
108
- for (const [pattern, check] of Object.entries(protectedRoutes)) {
109
- if (matchPath({ path: pattern, end: true }, pathname)) {
110
- return check;
111
- }
112
- }
113
- },
114
- [protectedRoutes],
115
- );
116
-
117
- const currentAuthCheck = getAuthCheck(location.pathname);
118
- const isProtectedRoute = currentAuthCheck !== undefined;
119
- const rawResult = currentAuthCheck?.(authCheckContext) ?? true;
120
- // Normalize: false is equivalent to UNAUTHORIZED
121
- const authResult =
122
- rawResult === false ? REASON_CODES.UNAUTHORIZED : rawResult;
123
- const isForbidden = authResult === REASON_CODES.FORBIDDEN;
124
- const needsToSignIn = authResult === REASON_CODES.UNAUTHORIZED;
125
-
126
- const blocker = useBlocker(({ nextLocation }) => {
127
- if (shouldBypass) return false;
128
- const check = getAuthCheck(nextLocation.pathname);
129
- if (!check) return false;
130
- const result = check(authCheckContext);
131
- // Only block for unauthorized (needs login), not for reason codes like "forbidden"
132
- return result === false || result === REASON_CODES.UNAUTHORIZED;
133
- });
134
- const isBlocked = blocker.state === "blocked";
135
-
136
- // Proceed after successful login
137
- useEffect(() => {
138
- if (!auth.isAuthenticated || !isBlocked) return;
139
- const check = getAuthCheck(blocker.location.pathname);
140
- if (!check) {
141
- blocker.proceed?.();
142
- return;
143
- }
144
- const result = check(authCheckContext);
145
- // Proceed whenever the result is no longer UNAUTHORIZED (e.g. true or FORBIDDEN)
146
- if (result !== false && result !== REASON_CODES.UNAUTHORIZED) {
147
- blocker.proceed?.();
148
- }
149
- }, [
150
- auth.isAuthenticated,
151
- isBlocked,
152
- blocker,
153
- authCheckContext,
154
- getAuthCheck,
155
- ]);
156
-
157
- if (isForbidden) {
158
- return <ForbiddenPage />;
159
- }
160
-
161
- if (shouldBypass) {
162
- return <BypassRoute isProtectedRoute={isProtectedRoute} />;
163
- }
164
-
165
- if (isProtectedRoute && !auth.isAuthEnabled) {
166
- throw new ZudokuError("Authentication is not enabled", {
167
- title: "Authentication is not enabled",
168
- developerHint:
169
- "To use protectedRoutes you need authentication to be enabled",
170
- });
171
- }
172
-
173
- if (needsToSignIn && auth.isPending && typeof window !== "undefined") {
174
- return null;
175
- }
176
-
177
- const showDialog = needsToSignIn || isBlocked;
178
- const redirectTo = isBlocked
179
- ? blocker.location.pathname + blocker.location.search
180
- : location.pathname + location.search;
181
-
182
- return (
183
- <>
184
- {!needsToSignIn && <Outlet />}
185
- <LoginDialog
186
- open={showDialog}
187
- onCancel={needsToSignIn ? () => navigate(-1) : () => blocker.reset?.()}
188
- onLogin={() => void auth.login({ redirectTo })}
189
- onRegister={() => void auth.signup({ redirectTo })}
190
- />
191
- </>
192
- );
193
- };
1
+ import { Button } from "@lukoweb/apitogo/ui/Button.js";
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from "@lukoweb/apitogo/ui/Dialog.js";
10
+ import { Helmet } from "@zudoku/react-helmet-async";
11
+ import { use, useCallback, useEffect, useMemo } from "react";
12
+ import {
13
+ matchPath,
14
+ Outlet,
15
+ useBlocker,
16
+ useLocation,
17
+ useNavigate,
18
+ } from "react-router";
19
+ import { REASON_CODES } from "../../config/validators/reason-codes.js";
20
+ import { useAuth } from "../authentication/hook.js";
21
+ import { RenderContext } from "../components/context/RenderContext.js";
22
+ import { useZudoku } from "../components/context/ZudokuContext.js";
23
+ import { Layout } from "../components/Layout.js";
24
+ import { ZudokuError } from "../util/invariant.js";
25
+
26
+ export const SEARCH_PROTECTED_SECTION = "protected";
27
+
28
+ type LoginDialogProps = {
29
+ open: boolean;
30
+ onCancel: () => void;
31
+ onLogin: () => void;
32
+ onRegister: () => void;
33
+ };
34
+
35
+ const LoginDialog = ({
36
+ open,
37
+ onCancel,
38
+ onLogin,
39
+ onRegister,
40
+ }: LoginDialogProps) => (
41
+ <Dialog open={open} onOpenChange={(nextOpen) => !nextOpen && onCancel()}>
42
+ <DialogContent>
43
+ <DialogHeader>
44
+ <DialogTitle>Login to continue</DialogTitle>
45
+ </DialogHeader>
46
+ <DialogDescription>Please login to access this page.</DialogDescription>
47
+ <DialogFooter>
48
+ <Button variant="outline" onClick={onCancel}>
49
+ Cancel
50
+ </Button>
51
+ <div className="w-full" />
52
+ <Button variant="secondary" onClick={onRegister}>
53
+ Register
54
+ </Button>
55
+ <Button onClick={onLogin}>Login</Button>
56
+ </DialogFooter>
57
+ </DialogContent>
58
+ </Dialog>
59
+ );
60
+
61
+ const BypassRoute = ({ isProtectedRoute }: { isProtectedRoute: boolean }) => (
62
+ <>
63
+ {isProtectedRoute && (
64
+ <Helmet>
65
+ <meta
66
+ name="pagefind"
67
+ data-pagefind-filter={`section:${SEARCH_PROTECTED_SECTION}`}
68
+ content="true"
69
+ />
70
+ </Helmet>
71
+ )}
72
+ <Outlet />
73
+ </>
74
+ );
75
+
76
+ const ForbiddenPage = () => {
77
+ const renderContext = use(RenderContext);
78
+ renderContext.status = 403;
79
+
80
+ return (
81
+ <Layout>
82
+ <div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
83
+ <h1 className="text-2xl font-bold">Access Denied</h1>
84
+ <p className="text-muted-foreground">
85
+ You don't have permission to access this page.
86
+ </p>
87
+ </div>
88
+ </Layout>
89
+ );
90
+ };
91
+
92
+ export const RouteGuard = () => {
93
+ const auth = useAuth();
94
+ const zudoku = useZudoku();
95
+ const navigate = useNavigate();
96
+ const location = useLocation();
97
+ const renderContext = use(RenderContext);
98
+ const shouldBypass = renderContext.bypassProtection;
99
+ const authCheckContext = useMemo(
100
+ () => ({ auth, context: zudoku, reasonCode: REASON_CODES }),
101
+ [auth, zudoku],
102
+ );
103
+ const { protectedRoutes } = zudoku;
104
+
105
+ const getAuthCheck = useCallback(
106
+ (pathname: string) => {
107
+ if (!protectedRoutes) return;
108
+ for (const [pattern, check] of Object.entries(protectedRoutes)) {
109
+ if (matchPath({ path: pattern, end: true }, pathname)) {
110
+ return check;
111
+ }
112
+ }
113
+ },
114
+ [protectedRoutes],
115
+ );
116
+
117
+ const currentAuthCheck = getAuthCheck(location.pathname);
118
+ const isProtectedRoute = currentAuthCheck !== undefined;
119
+ const rawResult = currentAuthCheck?.(authCheckContext) ?? true;
120
+ // Normalize: false is equivalent to UNAUTHORIZED
121
+ const authResult =
122
+ rawResult === false ? REASON_CODES.UNAUTHORIZED : rawResult;
123
+ const isForbidden = authResult === REASON_CODES.FORBIDDEN;
124
+ const needsToSignIn = authResult === REASON_CODES.UNAUTHORIZED;
125
+
126
+ const blocker = useBlocker(({ nextLocation }) => {
127
+ if (shouldBypass) return false;
128
+ const check = getAuthCheck(nextLocation.pathname);
129
+ if (!check) return false;
130
+ const result = check(authCheckContext);
131
+ // Only block for unauthorized (needs login), not for reason codes like "forbidden"
132
+ return result === false || result === REASON_CODES.UNAUTHORIZED;
133
+ });
134
+ const isBlocked = blocker.state === "blocked";
135
+
136
+ // Proceed after successful login
137
+ useEffect(() => {
138
+ if (!auth.isAuthenticated || !isBlocked) return;
139
+ const check = getAuthCheck(blocker.location.pathname);
140
+ if (!check) {
141
+ blocker.proceed?.();
142
+ return;
143
+ }
144
+ const result = check(authCheckContext);
145
+ // Proceed whenever the result is no longer UNAUTHORIZED (e.g. true or FORBIDDEN)
146
+ if (result !== false && result !== REASON_CODES.UNAUTHORIZED) {
147
+ blocker.proceed?.();
148
+ }
149
+ }, [
150
+ auth.isAuthenticated,
151
+ isBlocked,
152
+ blocker,
153
+ authCheckContext,
154
+ getAuthCheck,
155
+ ]);
156
+
157
+ if (isForbidden) {
158
+ return <ForbiddenPage />;
159
+ }
160
+
161
+ if (shouldBypass) {
162
+ return <BypassRoute isProtectedRoute={isProtectedRoute} />;
163
+ }
164
+
165
+ if (isProtectedRoute && !auth.isAuthEnabled) {
166
+ throw new ZudokuError("Authentication is not enabled", {
167
+ title: "Authentication is not enabled",
168
+ developerHint:
169
+ "To use protectedRoutes you need authentication to be enabled",
170
+ });
171
+ }
172
+
173
+ if (needsToSignIn && auth.isPending && typeof window !== "undefined") {
174
+ return null;
175
+ }
176
+
177
+ const showDialog = needsToSignIn || isBlocked;
178
+ const redirectTo = isBlocked
179
+ ? blocker.location.pathname + blocker.location.search
180
+ : location.pathname + location.search;
181
+
182
+ return (
183
+ <>
184
+ {!needsToSignIn && <Outlet />}
185
+ <LoginDialog
186
+ open={showDialog}
187
+ onCancel={needsToSignIn ? () => navigate(-1) : () => blocker.reset?.()}
188
+ onLogin={() => void auth.login({ redirectTo })}
189
+ onRegister={() => void auth.signup({ redirectTo })}
190
+ />
191
+ </>
192
+ );
193
+ };
@@ -1,46 +1,50 @@
1
- import { Suspense, lazy } from "react";
2
- import { Alert, AlertDescription, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
3
- import { DeveloperHint } from "../components/DeveloperHint.js";
4
- import { ZudokuError } from "../util/invariant.js";
5
-
6
- const SyntaxHighlight = lazy(() =>
7
- import("@lukoweb/apitogo/ui/SyntaxHighlight.js").then((m) => ({
8
- default: m.SyntaxHighlight,
9
- })),
10
- );
11
-
12
- export function ErrorMessage({ error }: { error: unknown }) {
13
- const message =
14
- error instanceof Error ? error.message : "Something went wrong";
15
-
16
- const showDeveloperHints = process.env.NODE_ENV === "development";
17
-
18
- const hint = error instanceof ZudokuError ? error.developerHint : undefined;
19
- const title =
20
- error instanceof ZudokuError ? error.title : "Something went wrong";
21
- const stack = error instanceof Error ? error.stack : undefined;
22
- const cause = error instanceof Error ? error.cause : undefined;
23
-
24
- const stringError = cause instanceof Error ? String(cause.stack) : stack;
25
-
26
- return (
27
- <>
28
- <Alert variant="destructive">
29
- <AlertTitle>{title}</AlertTitle>
30
- <AlertDescription>{message}</AlertDescription>
31
- </Alert>
32
- {showDeveloperHints && hint && (
33
- <DeveloperHint className="mb-4">{hint}</DeveloperHint>
34
- )}
35
- {showDeveloperHints && stringError && (
36
- <Suspense>
37
- <SyntaxHighlight
38
- className="max-h-[400px] [&>pre]:p-4"
39
- language="js"
40
- code={stringError}
41
- />
42
- </Suspense>
43
- )}
44
- </>
45
- );
46
- }
1
+ import {
2
+ Alert,
3
+ AlertDescription,
4
+ AlertTitle,
5
+ } from "@lukoweb/apitogo/ui/Alert.js";
6
+ import { Suspense, lazy } from "react";
7
+ import { DeveloperHint } from "../components/DeveloperHint.js";
8
+ import { ZudokuError } from "../util/invariant.js";
9
+
10
+ const SyntaxHighlight = lazy(() =>
11
+ import("@lukoweb/apitogo/ui/SyntaxHighlight.js").then((m) => ({
12
+ default: m.SyntaxHighlight,
13
+ })),
14
+ );
15
+
16
+ export function ErrorMessage({ error }: { error: unknown }) {
17
+ const message =
18
+ error instanceof Error ? error.message : "Something went wrong";
19
+
20
+ const showDeveloperHints = process.env.NODE_ENV === "development";
21
+
22
+ const hint = error instanceof ZudokuError ? error.developerHint : undefined;
23
+ const title =
24
+ error instanceof ZudokuError ? error.title : "Something went wrong";
25
+ const stack = error instanceof Error ? error.stack : undefined;
26
+ const cause = error instanceof Error ? error.cause : undefined;
27
+
28
+ const stringError = cause instanceof Error ? String(cause.stack) : stack;
29
+
30
+ return (
31
+ <>
32
+ <Alert variant="destructive">
33
+ <AlertTitle>{title}</AlertTitle>
34
+ <AlertDescription>{message}</AlertDescription>
35
+ </Alert>
36
+ {showDeveloperHints && hint && (
37
+ <DeveloperHint className="mb-4">{hint}</DeveloperHint>
38
+ )}
39
+ {showDeveloperHints && stringError && (
40
+ <Suspense>
41
+ <SyntaxHighlight
42
+ className="max-h-[400px] [&>pre]:p-4"
43
+ language="js"
44
+ code={stringError}
45
+ />
46
+ </Suspense>
47
+ )}
48
+ </>
49
+ );
50
+ }
@@ -1,80 +1,80 @@
1
- import { useSuspenseQuery } from "@tanstack/react-query";
2
- import { Helmet } from "@zudoku/react-helmet-async";
3
- import { useMatch } from "react-router";
4
- import { Link } from "@lukoweb/apitogo/components";
5
- import { useAuthState } from "../../authentication/state.js";
6
- import { Heading } from "../../components/Heading.js";
7
- import { Markdown } from "../../components/Markdown.js";
8
- import { joinUrl } from "../../util/joinUrl.js";
9
- import { type ApiCatalogPluginOptions, getKey } from "./index.js";
10
-
11
- export const Catalog = ({
12
- items,
13
- filterCatalogItems = (items) => items,
14
- label = "API Library",
15
- categoryLabel,
16
- }: Omit<ApiCatalogPluginOptions, "path"> & {
17
- categoryLabel?: string;
18
- }) => {
19
- const auth = useAuthState();
20
- const match = useMatch({ path: "/catalog/:category" });
21
- const activeCategory = match?.params.category;
22
-
23
- const catalogItems = useSuspenseQuery({
24
- queryFn: () => filterCatalogItems(items, { auth }),
25
- queryKey: ["catalogItems", auth],
26
- });
27
-
28
- // Only index the overview page, ignore the rest
29
- const dataSet = activeCategory ? { "data-pagefind-ignore": "all" } : {};
30
-
31
- return (
32
- <section className="pt-(--padding-content-top)" {...dataSet}>
33
- <Helmet>
34
- <title>
35
- {categoryLabel ? `${categoryLabel} - ` : ""}
36
- {label}
37
- </title>
38
- </Helmet>
39
- <div className="grid gap-4">
40
- <Heading level={2}>
41
- {label}
42
- {categoryLabel && ` - ${categoryLabel}`}
43
- </Heading>
44
-
45
- <div className="grid grid-cols-2 gap-4">
46
- {catalogItems.data
47
- .filter(
48
- (api) =>
49
- !activeCategory ||
50
- api.categories.find((c) =>
51
- c.tags.find((t) => getKey(c.label, t) === activeCategory),
52
- ),
53
- )
54
- .map((api) => (
55
- <Link
56
- to={joinUrl(api.path)}
57
- className="no-underline hover:!text-foreground"
58
- key={api.path}
59
- >
60
- <div className="border h-full rounded-lg p-4 flex flex-col gap-2 cursor-pointer hover:bg-border/20 font-normal">
61
- <span className="font-semibold">{api.label}</span>
62
- <Markdown
63
- className="text-sm whitespace-pre-wrap mb-6 line-clamp-2"
64
- content={api.description}
65
- components={{
66
- // Because we're wrapping the description in a Link already,
67
- // we need to strip out other links to not get a hydration error, like:
68
- // > In HTML, <a> cannot be a descendant of <a>.
69
- // > This will cause a hydration error.
70
- a: (props) => <span {...props} />,
71
- }}
72
- />
73
- </div>
74
- </Link>
75
- ))}
76
- </div>
77
- </div>
78
- </section>
79
- );
80
- };
1
+ import { Link } from "@lukoweb/apitogo/components";
2
+ import { useSuspenseQuery } from "@tanstack/react-query";
3
+ import { Helmet } from "@zudoku/react-helmet-async";
4
+ import { useMatch } from "react-router";
5
+ import { useAuthState } from "../../authentication/state.js";
6
+ import { Heading } from "../../components/Heading.js";
7
+ import { Markdown } from "../../components/Markdown.js";
8
+ import { joinUrl } from "../../util/joinUrl.js";
9
+ import { type ApiCatalogPluginOptions, getKey } from "./index.js";
10
+
11
+ export const Catalog = ({
12
+ items,
13
+ filterCatalogItems = (items) => items,
14
+ label = "API Library",
15
+ categoryLabel,
16
+ }: Omit<ApiCatalogPluginOptions, "path"> & {
17
+ categoryLabel?: string;
18
+ }) => {
19
+ const auth = useAuthState();
20
+ const match = useMatch({ path: "/catalog/:category" });
21
+ const activeCategory = match?.params.category;
22
+
23
+ const catalogItems = useSuspenseQuery({
24
+ queryFn: () => filterCatalogItems(items, { auth }),
25
+ queryKey: ["catalogItems", auth],
26
+ });
27
+
28
+ // Only index the overview page, ignore the rest
29
+ const dataSet = activeCategory ? { "data-pagefind-ignore": "all" } : {};
30
+
31
+ return (
32
+ <section className="pt-(--padding-content-top)" {...dataSet}>
33
+ <Helmet>
34
+ <title>
35
+ {categoryLabel ? `${categoryLabel} - ` : ""}
36
+ {label}
37
+ </title>
38
+ </Helmet>
39
+ <div className="grid gap-4">
40
+ <Heading level={2}>
41
+ {label}
42
+ {categoryLabel && ` - ${categoryLabel}`}
43
+ </Heading>
44
+
45
+ <div className="grid grid-cols-2 gap-4">
46
+ {catalogItems.data
47
+ .filter(
48
+ (api) =>
49
+ !activeCategory ||
50
+ api.categories.find((c) =>
51
+ c.tags.find((t) => getKey(c.label, t) === activeCategory),
52
+ ),
53
+ )
54
+ .map((api) => (
55
+ <Link
56
+ to={joinUrl(api.path)}
57
+ className="no-underline hover:!text-foreground"
58
+ key={api.path}
59
+ >
60
+ <div className="border h-full rounded-lg p-4 flex flex-col gap-2 cursor-pointer hover:bg-border/20 font-normal">
61
+ <span className="font-semibold">{api.label}</span>
62
+ <Markdown
63
+ className="text-sm whitespace-pre-wrap mb-6 line-clamp-2"
64
+ content={api.description}
65
+ components={{
66
+ // Because we're wrapping the description in a Link already,
67
+ // we need to strip out other links to not get a hydration error, like:
68
+ // > In HTML, <a> cannot be a descendant of <a>.
69
+ // > This will cause a hydration error.
70
+ a: (props) => <span {...props} />,
71
+ }}
72
+ />
73
+ </div>
74
+ </Link>
75
+ ))}
76
+ </div>
77
+ </div>
78
+ </section>
79
+ );
80
+ };