zudoku 0.37.1 → 0.38.0

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 (131) hide show
  1. package/dist/app/main.js +2 -0
  2. package/dist/app/main.js.map +1 -1
  3. package/dist/config/validators/common.d.ts +287 -18
  4. package/dist/config/validators/common.js +2 -0
  5. package/dist/config/validators/common.js.map +1 -1
  6. package/dist/config/validators/validate.d.ts +107 -7
  7. package/dist/lib/authentication/authentication.d.ts +1 -0
  8. package/dist/lib/authentication/providers/clerk.js +19 -0
  9. package/dist/lib/authentication/providers/clerk.js.map +1 -1
  10. package/dist/lib/authentication/providers/openid.d.ts +1 -0
  11. package/dist/lib/authentication/providers/openid.js +5 -0
  12. package/dist/lib/authentication/providers/openid.js.map +1 -1
  13. package/dist/lib/authentication/providers/supabase.js +5 -0
  14. package/dist/lib/authentication/providers/supabase.js.map +1 -1
  15. package/dist/lib/authentication/state.d.ts +0 -26
  16. package/dist/lib/authentication/state.js +1 -16
  17. package/dist/lib/authentication/state.js.map +1 -1
  18. package/dist/lib/components/Layout.js +5 -3
  19. package/dist/lib/components/Layout.js.map +1 -1
  20. package/dist/lib/core/ZudokuContext.d.ts +7 -0
  21. package/dist/lib/core/ZudokuContext.js +8 -3
  22. package/dist/lib/core/ZudokuContext.js.map +1 -1
  23. package/dist/lib/core/plugins.d.ts +1 -1
  24. package/dist/lib/plugins/markdown/MdxPage.js +2 -8
  25. package/dist/lib/plugins/markdown/MdxPage.js.map +1 -1
  26. package/dist/lib/plugins/openapi/Endpoint.js +1 -1
  27. package/dist/lib/plugins/openapi/Endpoint.js.map +1 -1
  28. package/dist/lib/plugins/openapi/OperationList.js +1 -1
  29. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  30. package/dist/lib/plugins/openapi/Sidecar.js +29 -5
  31. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  32. package/dist/lib/plugins/openapi/interfaces.d.ts +26 -0
  33. package/dist/lib/plugins/openapi/playground/Playground.js +1 -1
  34. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  35. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js +2 -2
  36. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js.map +1 -1
  37. package/dist/lib/plugins/openapi/state.d.ts +25 -0
  38. package/dist/lib/plugins/openapi/state.js +18 -0
  39. package/dist/lib/plugins/openapi/state.js.map +1 -0
  40. package/dist/lib/plugins/search-pagefind/PagefindSearch.js +13 -4
  41. package/dist/lib/plugins/search-pagefind/PagefindSearch.js.map +1 -1
  42. package/dist/lib/plugins/search-pagefind/ResultList.js +19 -12
  43. package/dist/lib/plugins/search-pagefind/ResultList.js.map +1 -1
  44. package/dist/lib/plugins/search-pagefind/get-results.d.ts +8 -1
  45. package/dist/lib/plugins/search-pagefind/get-results.js +9 -4
  46. package/dist/lib/plugins/search-pagefind/get-results.js.map +1 -1
  47. package/dist/lib/util/traverse.d.ts +2 -8
  48. package/dist/lib/util/traverse.js.map +1 -1
  49. package/dist/lib/util/types.d.ts +7 -0
  50. package/dist/lib/util/types.js +2 -0
  51. package/dist/lib/util/types.js.map +1 -0
  52. package/dist/lib/util/useScrollToAnchor.js +18 -12
  53. package/dist/lib/util/useScrollToAnchor.js.map +1 -1
  54. package/dist/vite/plugin-api.js +4 -1
  55. package/dist/vite/plugin-api.js.map +1 -1
  56. package/lib/{AuthenticationPlugin-Cij2tPWa.js → AuthenticationPlugin-Duy_R1TU.js} +3 -3
  57. package/lib/{AuthenticationPlugin-Cij2tPWa.js.map → AuthenticationPlugin-Duy_R1TU.js.map} +1 -1
  58. package/lib/{Markdown-DT5Rrq8_.js → Markdown-DIZ8nBVC.js} +742 -738
  59. package/lib/{Markdown-DT5Rrq8_.js.map → Markdown-DIZ8nBVC.js.map} +1 -1
  60. package/lib/{MdxPage-D2rD1vC4.js → MdxPage-JEdbfW-f.js} +42 -47
  61. package/lib/MdxPage-JEdbfW-f.js.map +1 -0
  62. package/lib/{OasProvider-DdEBf2qS.js → OasProvider-D1A10JeA.js} +4 -4
  63. package/lib/{OasProvider-DdEBf2qS.js.map → OasProvider-D1A10JeA.js.map} +1 -1
  64. package/lib/{OperationList-DT4-gm_S.js → OperationList-yOmYzMIp.js} +1128 -1112
  65. package/lib/OperationList-yOmYzMIp.js.map +1 -0
  66. package/lib/{Select-z1Lwl0-J.js → Select-fAYcJ0OU.js} +8 -8
  67. package/lib/{Select-z1Lwl0-J.js.map → Select-fAYcJ0OU.js.map} +1 -1
  68. package/lib/{SlotletProvider-D8OBnr77.js → SlotletProvider-BEwNY8q0.js} +4 -4
  69. package/lib/{SlotletProvider-D8OBnr77.js.map → SlotletProvider-BEwNY8q0.js.map} +1 -1
  70. package/lib/{chunk-HA7DTUK3-ZGg2W6yV.js → chunk-HA7DTUK3-C4gP41vD.js} +5 -5
  71. package/lib/{chunk-HA7DTUK3-ZGg2W6yV.js.map → chunk-HA7DTUK3-C4gP41vD.js.map} +1 -1
  72. package/lib/hook-Cge6LiTK.js +1483 -0
  73. package/lib/hook-Cge6LiTK.js.map +1 -0
  74. package/lib/{index-DdQSV2RF.js → index-B0y3fTg-.js} +261 -243
  75. package/lib/index-B0y3fTg-.js.map +1 -0
  76. package/lib/{mutation-_Z5C2wFZ.js → mutation-EChriCeF.js} +2 -2
  77. package/lib/{mutation-_Z5C2wFZ.js.map → mutation-EChriCeF.js.map} +1 -1
  78. package/lib/post-processors/traverse.js.map +1 -1
  79. package/lib/{useExposedProps-BslIn-FE.js → useExposedProps-B9qXJedG.js} +2 -2
  80. package/lib/{useExposedProps-BslIn-FE.js.map → useExposedProps-B9qXJedG.js.map} +1 -1
  81. package/lib/zudoku.auth-auth0.js +1 -1
  82. package/lib/zudoku.auth-clerk.js +59 -41
  83. package/lib/zudoku.auth-clerk.js.map +1 -1
  84. package/lib/zudoku.auth-openid.js +76 -73
  85. package/lib/zudoku.auth-openid.js.map +1 -1
  86. package/lib/zudoku.components.js +229 -215
  87. package/lib/zudoku.components.js.map +1 -1
  88. package/lib/zudoku.hooks.js +1 -1
  89. package/lib/zudoku.plugin-api-catalog.js +23 -24
  90. package/lib/zudoku.plugin-api-catalog.js.map +1 -1
  91. package/lib/zudoku.plugin-api-keys.js +15 -16
  92. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  93. package/lib/zudoku.plugin-custom-pages.js +2 -2
  94. package/lib/zudoku.plugin-markdown.js +1 -1
  95. package/lib/zudoku.plugin-openapi.js +5 -6
  96. package/lib/zudoku.plugin-openapi.js.map +1 -1
  97. package/lib/zudoku.plugin-redirect.js +1 -1
  98. package/lib/zudoku.plugin-search-pagefind.js +133 -98
  99. package/lib/zudoku.plugin-search-pagefind.js.map +1 -1
  100. package/lib/zudoku.plugins.js.map +1 -1
  101. package/package.json +1 -1
  102. package/src/app/main.tsx +2 -0
  103. package/src/lib/authentication/authentication.ts +2 -0
  104. package/src/lib/authentication/providers/clerk.tsx +20 -0
  105. package/src/lib/authentication/providers/openid.tsx +6 -0
  106. package/src/lib/authentication/providers/supabase.tsx +6 -0
  107. package/src/lib/authentication/state.ts +1 -35
  108. package/src/lib/components/Layout.tsx +14 -2
  109. package/src/lib/core/ZudokuContext.ts +13 -6
  110. package/src/lib/core/plugins.ts +1 -1
  111. package/src/lib/plugins/markdown/MdxPage.tsx +1 -8
  112. package/src/lib/plugins/openapi/Endpoint.tsx +1 -1
  113. package/src/lib/plugins/openapi/OperationList.tsx +1 -1
  114. package/src/lib/plugins/openapi/Sidecar.tsx +36 -7
  115. package/src/lib/plugins/openapi/interfaces.ts +29 -0
  116. package/src/lib/plugins/openapi/playground/Playground.tsx +1 -1
  117. package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +2 -1
  118. package/src/lib/plugins/openapi/state.ts +36 -0
  119. package/src/lib/plugins/search-pagefind/PagefindSearch.tsx +26 -4
  120. package/src/lib/plugins/search-pagefind/ResultList.tsx +59 -47
  121. package/src/lib/plugins/search-pagefind/get-results.tsx +31 -10
  122. package/src/lib/util/traverse.ts +2 -6
  123. package/src/lib/util/types.ts +7 -0
  124. package/src/lib/util/useScrollToAnchor.ts +20 -12
  125. package/lib/MdxPage-D2rD1vC4.js.map +0 -1
  126. package/lib/OperationList-DT4-gm_S.js.map +0 -1
  127. package/lib/hook-DzQC8PzJ.js +0 -355
  128. package/lib/hook-DzQC8PzJ.js.map +0 -1
  129. package/lib/index-DdQSV2RF.js.map +0 -1
  130. package/lib/joinUrl-BjDooT-T.js +0 -1154
  131. package/lib/joinUrl-BjDooT-T.js.map +0 -1
@@ -5,6 +5,7 @@ import type { Location } from "react-router";
5
5
  import type { TopNavigationItem } from "../../config/validators/common.js";
6
6
  import type { SidebarConfig } from "../../config/validators/SidebarSchema.js";
7
7
  import type { AuthenticationProvider } from "../authentication/authentication.js";
8
+ import { type AuthState, useAuthState } from "../authentication/state.js";
8
9
  import type { ComponentsContextType } from "../components/context/ComponentsContext.js";
9
10
  import type { Slotlets } from "../components/SlotletProvider.js";
10
11
  import { joinPath } from "../util/joinPath.js";
@@ -21,6 +22,7 @@ import {
21
22
 
22
23
  export interface ZudokuEvents {
23
24
  location: (event: { from?: Location; to: Location }) => void;
25
+ auth: (auth: { prev: AuthState; next: AuthState }) => void;
24
26
  }
25
27
 
26
28
  export interface ApiIdentity {
@@ -67,6 +69,8 @@ type Page = Partial<{
67
69
  }>;
68
70
 
69
71
  export type ZudokuContextOptions = {
72
+ basePath?: string;
73
+ canonicalUrlOrigin?: string;
70
74
  metadata?: Metadata;
71
75
  page?: Page;
72
76
  authentication?: AuthenticationProvider;
@@ -106,7 +110,14 @@ export class ZudokuContext {
106
110
  if (!isEventConsumerPlugin(plugin)) return;
107
111
 
108
112
  objectEntries(plugin.events).forEach(([event, handler]) => {
109
- this.emitter.on(event, handler);
113
+ this.emitter.on(event, handler!);
114
+ });
115
+ });
116
+
117
+ useAuthState.subscribe((state, prevState) => {
118
+ this.emitEvent("auth", {
119
+ prev: prevState,
120
+ next: state,
110
121
  });
111
122
  });
112
123
  }
@@ -158,10 +169,6 @@ export class ZudokuContext {
158
169
  throw new Error("No authentication provider configured");
159
170
  }
160
171
 
161
- const accessToken = await this.authentication.getAccessToken();
162
-
163
- request.headers.set("Authorization", `Bearer ${accessToken}`);
164
-
165
- return request;
172
+ return await this.authentication.signRequest(request);
166
173
  };
167
174
  }
@@ -65,7 +65,7 @@ export interface CommonPlugin {
65
65
  }
66
66
 
67
67
  export type EventConsumerPlugin<Event extends ZudokuEvents = ZudokuEvents> = {
68
- events: { [K in keyof Event]: Event[K] };
68
+ events: { [K in keyof Event]?: Event[K] };
69
69
  };
70
70
 
71
71
  export const isEventConsumerPlugin = (
@@ -2,7 +2,7 @@ import { useMDXComponents } from "@mdx-js/react";
2
2
  import slugify from "@sindresorhus/slugify";
3
3
  import { Helmet } from "@zudoku/react-helmet-async";
4
4
  import { type PropsWithChildren, useEffect } from "react";
5
- import { Link, useHref } from "react-router";
5
+ import { Link } from "react-router";
6
6
  import { CategoryHeading } from "../../components/CategoryHeading.js";
7
7
  import { Heading } from "../../components/Heading.js";
8
8
  import { ProseClasses } from "../../components/Markdown.js";
@@ -51,12 +51,6 @@ export const MdxPage = ({
51
51
  }
52
52
  >) => {
53
53
  const categoryTitle = useCurrentItem()?.categoryLabel;
54
- let canonicalUrl = null;
55
- const path = useHref("");
56
- if (typeof window !== "undefined") {
57
- const domain = window.location.origin;
58
- canonicalUrl = `${domain}${path}`;
59
- }
60
54
 
61
55
  const title = frontmatter.title;
62
56
  const category = frontmatter.category ?? categoryTitle;
@@ -98,7 +92,6 @@ export const MdxPage = ({
98
92
  <Helmet>
99
93
  <title>{pageTitle}</title>
100
94
  {excerpt && <meta name="description" content={excerpt} />}
101
- {canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
102
95
  </Helmet>
103
96
  <div
104
97
  className={cn(
@@ -1,13 +1,13 @@
1
1
  import { useSuspenseQuery } from "@tanstack/react-query";
2
2
  import { CheckIcon, CopyIcon } from "lucide-react";
3
3
  import { useState, useTransition } from "react";
4
- import { useSelectedServer } from "../../authentication/state.js";
5
4
  import { InlineCode } from "../../components/InlineCode.js";
6
5
  import { Button } from "../../ui/Button.js";
7
6
  import { useCreateQuery } from "./client/useCreateQuery.js";
8
7
  import { useOasConfig } from "./context.js";
9
8
  import { graphql } from "./graphql/index.js";
10
9
  import { SimpleSelect } from "./SimpleSelect.js";
10
+ import { useSelectedServer } from "./state.js";
11
11
 
12
12
  const ServersQuery = graphql(/* GraphQL */ `
13
13
  query ServersQuery($input: JSON!, $type: SchemaType!) {
@@ -15,7 +15,6 @@ import {
15
15
  SelectTrigger,
16
16
  SelectValue,
17
17
  } from "zudoku/ui/Select.js";
18
- import { useSelectedServer } from "../../authentication/state.js";
19
18
  import { CategoryHeading } from "../../components/CategoryHeading.js";
20
19
  import { Heading } from "../../components/Heading.js";
21
20
  import { Markdown, ProseClasses } from "../../components/Markdown.js";
@@ -26,6 +25,7 @@ import { OperationListItem } from "./OperationListItem.js";
26
25
  import { useCreateQuery } from "./client/useCreateQuery.js";
27
26
  import { useOasConfig } from "./context.js";
28
27
  import { graphql } from "./graphql/index.js";
28
+ import { useSelectedServer } from "./state.js";
29
29
  import { sanitizeMarkdownForMetatag } from "./util/sanitizeMarkdownForMetatag.js";
30
30
 
31
31
  export const OperationsFragment = graphql(/* GraphQL */ `
@@ -2,7 +2,8 @@ import { useSuspenseQuery } from "@tanstack/react-query";
2
2
  import { HTTPSnippet } from "@zudoku/httpsnippet";
3
3
  import { useMemo, useState, useTransition } from "react";
4
4
  import { useSearchParams } from "react-router";
5
- import { useSelectedServer } from "../../authentication/state.js";
5
+ import { useZudoku } from "zudoku/components";
6
+ import { useAuthState } from "../../authentication/state.js";
6
7
  import { PathRenderer } from "../../components/PathRenderer.js";
7
8
  import type { SchemaObject } from "../../oas/parser/index.js";
8
9
  import { SyntaxHighlight } from "../../ui/SyntaxHighlight.js";
@@ -19,6 +20,7 @@ import { RequestBodySidecarBox } from "./RequestBodySidecarBox.js";
19
20
  import { ResponsesSidecarBox } from "./ResponsesSidecarBox.js";
20
21
  import * as SidecarBox from "./SidecarBox.js";
21
22
  import { SimpleSelect } from "./SimpleSelect.js";
23
+ import { useSelectedServer } from "./state.js";
22
24
  import { generateSchemaExample } from "./util/generateSchemaExample.js";
23
25
  import { methodForColor } from "./util/methodToColor.js";
24
26
 
@@ -101,8 +103,10 @@ export const Sidecar = ({
101
103
  onSelectResponse: (response: string) => void;
102
104
  }) => {
103
105
  const { input, type, options } = useOasConfig();
106
+ const auth = useAuthState();
104
107
  const query = useCreateQuery(GetServerQuery, { input, type });
105
108
  const result = useSuspenseQuery(query);
109
+ const context = useZudoku();
106
110
 
107
111
  const methodTextColor = methodForColor(operation.method);
108
112
 
@@ -115,6 +119,17 @@ export const Sidecar = ({
115
119
 
116
120
  const requestBodyContent = operation.requestBody?.content;
117
121
 
122
+ const transformedRequestBodyContent =
123
+ requestBodyContent && options?.transformExamples
124
+ ? options.transformExamples({
125
+ auth,
126
+ type: "request",
127
+ operation,
128
+ content: requestBodyContent,
129
+ context,
130
+ })
131
+ : requestBodyContent;
132
+
118
133
  const path = (
119
134
  <PathRenderer
120
135
  path={operation.path}
@@ -136,8 +151,10 @@ export const Sidecar = ({
136
151
  const code = useMemo(() => {
137
152
  const example =
138
153
  selectedExample ??
139
- (requestBodyContent?.[0]?.schema
140
- ? generateSchemaExample(requestBodyContent[0].schema as SchemaObject)
154
+ (transformedRequestBodyContent?.[0]?.schema
155
+ ? generateSchemaExample(
156
+ transformedRequestBodyContent[0].schema as SchemaObject,
157
+ )
141
158
  : undefined);
142
159
 
143
160
  const snippet = new HTTPSnippet({
@@ -162,7 +179,7 @@ export const Sidecar = ({
162
179
  return getConverted(snippet, selectedLang);
163
180
  }, [
164
181
  selectedExample,
165
- requestBodyContent,
182
+ transformedRequestBodyContent,
166
183
  operation.method,
167
184
  operation.path,
168
185
  selectedServer,
@@ -232,9 +249,9 @@ export const Sidecar = ({
232
249
  </>
233
250
  )}
234
251
  </SidecarBox.Root>
235
- {isOnScreen && requestBodyContent && (
252
+ {isOnScreen && transformedRequestBodyContent && (
236
253
  <RequestBodySidecarBox
237
- content={requestBodyContent}
254
+ content={transformedRequestBodyContent}
238
255
  onExampleChange={setSelectedExample}
239
256
  />
240
257
  )}
@@ -242,7 +259,19 @@ export const Sidecar = ({
242
259
  <ResponsesSidecarBox
243
260
  selectedResponse={selectedResponse}
244
261
  onSelectResponse={onSelectResponse}
245
- responses={operation.responses}
262
+ responses={operation.responses.map((response) => ({
263
+ ...response,
264
+ content:
265
+ response.content && options?.transformExamples
266
+ ? options.transformExamples({
267
+ auth,
268
+ type: "response",
269
+ context,
270
+ operation,
271
+ content: response.content,
272
+ })
273
+ : response.content,
274
+ }))}
246
275
  />
247
276
  )}
248
277
  </aside>
@@ -1,4 +1,7 @@
1
+ import { AuthState } from "../../authentication/state.js";
2
+ import { ZudokuContext } from "../../core/ZudokuContext.js";
1
3
  import type { SchemaImports } from "../../oas/graphql/index.js";
4
+ import { OperationListItemResult } from "./OperationList.js";
2
5
 
3
6
  type DynamicInput = () => Promise<unknown>;
4
7
 
@@ -12,6 +15,31 @@ export type ContextOasSource =
12
15
  | { type: "file"; input: DynamicInput }
13
16
  | { type: "raw"; input: string };
14
17
 
18
+ type Example = {
19
+ name: string;
20
+ description?: string | null;
21
+ externalValue?: string | null;
22
+ value?: any | null;
23
+ summary?: string | null;
24
+ };
25
+
26
+ type Content = {
27
+ mediaType: string;
28
+ schema?: any | null;
29
+ encoding?: Array<{
30
+ name: string;
31
+ }> | null;
32
+ examples?: Array<Example> | null;
33
+ };
34
+
35
+ export type transformExamples = (options: {
36
+ content: Content[];
37
+ context: ZudokuContext;
38
+ auth: AuthState;
39
+ operation: OperationListItemResult;
40
+ type: "request" | "response";
41
+ }) => Content[];
42
+
15
43
  type BaseOasConfig = {
16
44
  server?: string;
17
45
  navigationId?: string;
@@ -23,6 +51,7 @@ type BaseOasConfig = {
23
51
  disablePlayground?: boolean;
24
52
  showVersionSelect?: "always" | "if-available" | "hide";
25
53
  expandAllTags?: boolean;
54
+ transformExamples?: transformExamples;
26
55
  };
27
56
  };
28
57
 
@@ -13,7 +13,6 @@ import {
13
13
  SelectValue,
14
14
  } from "zudoku/ui/Select.js";
15
15
  import { Textarea } from "zudoku/ui/Textarea.js";
16
- import { useSelectedServer } from "../../../authentication/state.js";
17
16
  import { useApiIdentities } from "../../../components/context/ZudokuContext.js";
18
17
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/Tabs.js";
19
18
  import { cn } from "../../../util/cn.js";
@@ -21,6 +20,7 @@ import { objectEntries } from "../../../util/objectEntries.js";
21
20
  import { useLatest } from "../../../util/useLatest.js";
22
21
  import { ColorizedParam } from "../ColorizedParam.js";
23
22
  import { type Content } from "../SidecarExamples.js";
23
+ import { useSelectedServer } from "../state.js";
24
24
  import { createUrl } from "./createUrl.js";
25
25
  import ExamplesDropdown from "./ExamplesDropdown.js";
26
26
  import { Headers } from "./Headers.js";
@@ -32,7 +32,7 @@ export const ResultPanel = ({
32
32
  }) => {
33
33
  const status = ((queryMutation.data?.status ?? 0) / 100).toFixed(0);
34
34
  return (
35
- <div className="min-w-0 p-4 bg-muted/50">
35
+ <div className="min-w-0 p-4 py-8 bg-muted/50">
36
36
  {queryMutation.error ? (
37
37
  <div className="flex flex-col gap-2">
38
38
  {showPathParamsWarning && (
@@ -99,6 +99,7 @@ export const ResultPanel = ({
99
99
  >
100
100
  Looks like the request is taking longer than expected.
101
101
  <Button
102
+ type="button"
102
103
  onClick={onCancel}
103
104
  size="sm"
104
105
  className="w-fit"
@@ -0,0 +1,36 @@
1
+ import { useMemo } from "react";
2
+ import { create } from "zustand";
3
+ import { persist } from "zustand/middleware";
4
+
5
+ interface SelectedServerState {
6
+ selectedServer?: string;
7
+ setSelectedServer: (newServer: string) => void;
8
+ }
9
+
10
+ export const useSelectedServerStore = create<SelectedServerState>()(
11
+ persist(
12
+ (set) => ({
13
+ selectedServer: undefined,
14
+ setSelectedServer: (newServer: string) =>
15
+ set({ selectedServer: newServer }),
16
+ }),
17
+ { name: "zudoku-selected-server" },
18
+ ),
19
+ );
20
+
21
+ /**
22
+ * Simple wrapper for `useSelectedServerStore` to fall back to first of the provided servers
23
+ */
24
+ export const useSelectedServer = (servers: Array<{ url: string }>) => {
25
+ const { selectedServer, setSelectedServer } = useSelectedServerStore();
26
+
27
+ const finalSelectedServer = useMemo(
28
+ () =>
29
+ selectedServer && servers.some((s) => s.url === selectedServer)
30
+ ? selectedServer
31
+ : (servers.at(0)?.url ?? ""),
32
+ [selectedServer, servers],
33
+ );
34
+
35
+ return { selectedServer: finalSelectedServer, setSelectedServer };
36
+ };
@@ -1,6 +1,7 @@
1
1
  import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
2
2
  import { keepPreviousData, useQuery } from "@tanstack/react-query";
3
- import { useState } from "react";
3
+ import { useRef, useState } from "react";
4
+ import { Button } from "zudoku/ui/Button.js";
4
5
  import { Callout } from "zudoku/ui/Callout.js";
5
6
  import {
6
7
  CommandDialog,
@@ -8,6 +9,8 @@ import {
8
9
  CommandInput,
9
10
  } from "zudoku/ui/Command.js";
10
11
  import { DialogTitle } from "zudoku/ui/Dialog.js";
12
+ import { useAuthState } from "../../authentication/state.js";
13
+ import { useZudoku } from "../../components/context/ZudokuContext.js";
11
14
  import { joinUrl } from "../../util/joinUrl.js";
12
15
  import { getResults } from "./get-results.js";
13
16
  import type { PagefindOptions } from "./index.js";
@@ -55,7 +58,7 @@ const usePagefind = (options: PagefindOptions) => {
55
58
  enabled: typeof window !== "undefined",
56
59
  });
57
60
 
58
- if (result.isError) {
61
+ if (result.isError && result.error.message !== "NOT_BUILT_YET") {
59
62
  // eslint-disable-next-line no-console
60
63
  console.error(result.error);
61
64
  }
@@ -74,13 +77,16 @@ export const PagefindSearch = ({
74
77
  }) => {
75
78
  const { pagefind, error, isError } = usePagefind(options);
76
79
  const [searchTerm, setSearchTerm] = useState("");
80
+ const auth = useAuthState();
81
+ const context = useZudoku();
82
+ const inputRef = useRef<HTMLInputElement>(null);
77
83
 
78
84
  const { data: searchResults } = useQuery({
79
85
  queryKey: ["pagefind-search", searchTerm],
80
86
  queryFn: async () => {
81
87
  const search = await pagefind?.search(searchTerm);
82
88
  if (!search) return [];
83
- return getResults(search, options);
89
+ return getResults({ search, options, auth, context });
84
90
  },
85
91
  placeholderData: keepPreviousData,
86
92
  enabled: !!pagefind && !!searchTerm,
@@ -97,13 +103,29 @@ export const PagefindSearch = ({
97
103
  <DialogTitle>Search</DialogTitle>
98
104
  </VisuallyHidden>
99
105
  <CommandInput
106
+ ref={inputRef}
100
107
  placeholder="Search..."
101
108
  value={searchTerm}
102
109
  onValueChange={setSearchTerm}
103
110
  disabled={isError}
104
111
  />
105
112
  <CommandEmpty>
106
- {searchTerm ? "No results found." : "Start typing to search"}
113
+ {searchTerm ? (
114
+ <div className="flex flex-col items-center">
115
+ No results found.
116
+ <Button
117
+ variant="link"
118
+ onClick={() => {
119
+ setSearchTerm("");
120
+ inputRef.current?.focus();
121
+ }}
122
+ >
123
+ Clear search
124
+ </Button>
125
+ </div>
126
+ ) : (
127
+ "Start typing to search"
128
+ )}
107
129
  </CommandEmpty>
108
130
  {isError ? (
109
131
  <div className="p-4 text-sm">
@@ -1,5 +1,5 @@
1
- import { FileTextIcon } from "lucide-react";
2
- import { useCallback } from "react";
1
+ import { BracketsIcon, FileTextIcon } from "lucide-react";
2
+ import { useCallback, useLayoutEffect, useRef } from "react";
3
3
  import { Link, useNavigate } from "react-router";
4
4
  import { CommandGroup, CommandItem, CommandList } from "zudoku/ui/Command.js";
5
5
  import {
@@ -35,6 +35,7 @@ export const ResultList = ({
35
35
  maxSubResults?: number;
36
36
  }) => {
37
37
  const navigate = useNavigate();
38
+ const commandListRef = useRef<HTMLDivElement | null>(null);
38
39
 
39
40
  const cleanResultUrl = useCallback(
40
41
  (url: string) => {
@@ -46,59 +47,70 @@ export const ResultList = ({
46
47
  [basePath],
47
48
  );
48
49
 
50
+ useLayoutEffect(() => {
51
+ requestIdleCallback(() => {
52
+ commandListRef.current?.scrollTo({ top: 0 });
53
+ });
54
+ }, [searchTerm]);
55
+
49
56
  return (
50
- <CommandList className="max-h-[450px]">
57
+ <CommandList className="max-h-[450px]" ref={commandListRef}>
51
58
  {searchTerm && searchResults.length > 0 && (
52
59
  <CommandGroup
53
60
  className="text-sm text-muted-foreground"
54
61
  heading={`${searchResults.length} results for "${searchTerm}"`}
55
62
  />
56
63
  )}
57
- {searchResults.map((result) => (
58
- <CommandGroup
59
- key={[result.meta.title ?? result.excerpt, result.url].join("-")}
60
- >
61
- <CommandItem
62
- asChild
63
- value={`${result.meta.title}-${result.url}`}
64
- className={hoverClassname}
65
- onSelect={() => {
66
- void navigate(cleanResultUrl(result.url));
67
- onClose();
68
- }}
64
+ {searchTerm &&
65
+ searchResults.map((result) => (
66
+ <CommandGroup
67
+ key={[result.meta.title ?? result.excerpt, result.url].join("-")}
69
68
  >
70
- <Link to={cleanResultUrl(result.url)}>
71
- <FileTextIcon size={20} className="text-muted-foreground" />
72
- {result.meta.title}
73
- </Link>
74
- </CommandItem>
75
- {result.sub_results
76
- .sort(sortSubResults)
77
- .slice(0, maxSubResults)
78
- .map((subResult) => (
79
- <CommandItem
80
- asChild
81
- key={`${result.meta.title}-${subResult.url}`}
82
- value={`${result.meta.title}-${subResult.url}`}
83
- className={hoverClassname}
84
- onSelect={() => {
85
- void navigate(cleanResultUrl(subResult.url));
86
- onClose();
87
- }}
88
- >
89
- <Link to={cleanResultUrl(subResult.url)} onClick={onClose}>
90
- <div className="flex flex-col items-start gap-2 ms-2.5 ps-5 border-l border-muted-foreground/50">
91
- <span className="font-bold">{subResult.title}</span>
92
- <span
93
- className="text-[13px] [&_mark]:bg-primary [&_mark]:text-primary-foreground"
94
- dangerouslySetInnerHTML={{ __html: subResult.excerpt }}
95
- />
96
- </div>
97
- </Link>
98
- </CommandItem>
99
- ))}
100
- </CommandGroup>
101
- ))}
69
+ <CommandItem
70
+ asChild
71
+ value={`${result.meta.title}-${result.url}`}
72
+ className={hoverClassname}
73
+ onSelect={() => {
74
+ void navigate(cleanResultUrl(result.url));
75
+ onClose();
76
+ }}
77
+ >
78
+ <Link to={cleanResultUrl(result.url)}>
79
+ {result.meta.section === "openapi" ? (
80
+ <BracketsIcon />
81
+ ) : (
82
+ <FileTextIcon />
83
+ )}
84
+ {result.meta.title}
85
+ </Link>
86
+ </CommandItem>
87
+ {result.sub_results
88
+ .sort(sortSubResults)
89
+ .slice(0, maxSubResults)
90
+ .map((subResult) => (
91
+ <CommandItem
92
+ asChild
93
+ key={`sub-${result.meta.title}-${subResult.url}`}
94
+ value={`sub-${result.meta.title}-${subResult.url}`}
95
+ className={hoverClassname}
96
+ onSelect={() => {
97
+ void navigate(cleanResultUrl(subResult.url));
98
+ onClose();
99
+ }}
100
+ >
101
+ <Link to={cleanResultUrl(subResult.url)} onClick={onClose}>
102
+ <div className="flex flex-col items-start gap-2 ms-2.5 ps-5 border-l border-muted-foreground/50">
103
+ <span className="font-bold">{subResult.title}</span>
104
+ <span
105
+ className="text-[13px] [&_mark]:bg-primary [&_mark]:text-primary-foreground"
106
+ dangerouslySetInnerHTML={{ __html: subResult.excerpt }}
107
+ />
108
+ </div>
109
+ </Link>
110
+ </CommandItem>
111
+ ))}
112
+ </CommandGroup>
113
+ ))}
102
114
  </CommandList>
103
115
  );
104
116
  };
@@ -1,16 +1,30 @@
1
+ import type { AuthState } from "../../authentication/state.js";
2
+ import type { ZudokuContext } from "../../core/ZudokuContext.js";
1
3
  import type { PagefindOptions } from "./index.js";
2
4
  import type { PagefindSearchFragment, PagefindSearchResults } from "./types.js";
3
5
 
4
- export const getResults = async (
5
- search: PagefindSearchResults,
6
- options: PagefindOptions,
7
- ) => {
6
+ export const getResults = async ({
7
+ search,
8
+ options,
9
+ auth,
10
+ context,
11
+ }: {
12
+ search: PagefindSearchResults;
13
+ options: PagefindOptions;
14
+ auth: AuthState;
15
+ context: ZudokuContext;
16
+ }) => {
8
17
  const maxResults = options.maxResults ?? 10;
9
18
  const transformFn = options.transformResults ?? (() => true);
10
19
 
11
20
  const transformedResults: PagefindSearchFragment[] = [];
12
21
 
13
- const generator = searchResultGenerator(search, transformFn);
22
+ const generator = searchResultGenerator({
23
+ search,
24
+ transformFn,
25
+ auth,
26
+ context,
27
+ });
14
28
 
15
29
  for await (const result of generator) {
16
30
  transformedResults.push(result);
@@ -20,10 +34,17 @@ export const getResults = async (
20
34
  return transformedResults;
21
35
  };
22
36
 
23
- async function* searchResultGenerator(
24
- search: PagefindSearchResults,
25
- transformFn: NonNullable<PagefindOptions["transformResults"]>,
26
- ) {
37
+ async function* searchResultGenerator({
38
+ search,
39
+ transformFn,
40
+ auth,
41
+ context,
42
+ }: {
43
+ search: PagefindSearchResults;
44
+ transformFn: NonNullable<PagefindOptions["transformResults"]>;
45
+ auth: AuthState<unknown>;
46
+ context: ZudokuContext;
47
+ }) {
27
48
  const batchSize = 5;
28
49
  let processedCount = 0;
29
50
 
@@ -37,7 +58,7 @@ async function* searchResultGenerator(
37
58
  const batchData = await Promise.all(batch.map((result) => result.data()));
38
59
 
39
60
  for (const result of batchData) {
40
- const transformed = transformFn(result);
61
+ const transformed = transformFn({ result, auth, context });
41
62
 
42
63
  if (transformed === false) {
43
64
  // Skip this result
@@ -1,10 +1,6 @@
1
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
- export type RecordAny = Record<string, any>;
1
+ import type { JsonValue, RecordAny } from "./types.js";
3
2
 
4
- type JsonPrimitive = string | number | boolean | null;
5
- type JsonArray = JsonValue[];
6
- type JsonObject = { [key: string]: JsonValue };
7
- type JsonValue = JsonPrimitive | JsonArray | JsonObject;
3
+ export type { RecordAny };
8
4
 
9
5
  export const traverse = <T extends JsonValue = RecordAny>(
10
6
  specification: RecordAny,
@@ -0,0 +1,7 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ export type RecordAny = Record<string, any>;
3
+
4
+ export type JsonPrimitive = string | number | boolean | null;
5
+ export type JsonArray = JsonValue[];
6
+ export type JsonObject = { [key: string]: JsonValue };
7
+ export type JsonValue = JsonPrimitive | JsonArray | JsonObject;