zudoku 0.33.1 → 0.34.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 (244) hide show
  1. package/dist/config/validators/common.d.ts +572 -354
  2. package/dist/config/validators/common.js +26 -8
  3. package/dist/config/validators/common.js.map +1 -1
  4. package/dist/config/validators/validate.d.ts +254 -167
  5. package/dist/lib/authentication/hook.d.ts +1 -0
  6. package/dist/lib/authentication/hook.js +11 -1
  7. package/dist/lib/authentication/hook.js.map +1 -1
  8. package/dist/lib/authentication/providers/clerk.js +6 -6
  9. package/dist/lib/authentication/providers/clerk.js.map +1 -1
  10. package/dist/lib/components/AnchorLink.d.ts +2 -2
  11. package/dist/lib/components/AnchorLink.js +4 -4
  12. package/dist/lib/components/AnchorLink.js.map +1 -1
  13. package/dist/lib/components/Banner.js +1 -1
  14. package/dist/lib/components/Banner.js.map +1 -1
  15. package/dist/lib/components/Heading.d.ts +2 -2
  16. package/dist/lib/components/Layout.js +1 -1
  17. package/dist/lib/components/Layout.js.map +1 -1
  18. package/dist/lib/components/context/ZudokuContext.d.ts +1 -1
  19. package/dist/lib/components/index.d.ts +1 -0
  20. package/dist/lib/components/navigation/SidebarItem.js +6 -5
  21. package/dist/lib/components/navigation/SidebarItem.js.map +1 -1
  22. package/dist/lib/core/RouteGuard.js +2 -1
  23. package/dist/lib/core/RouteGuard.js.map +1 -1
  24. package/dist/lib/core/ZudokuContext.d.ts +4 -0
  25. package/dist/lib/core/ZudokuContext.js.map +1 -1
  26. package/dist/lib/plugins/api-catalog/Catalog.d.ts +3 -1
  27. package/dist/lib/plugins/api-catalog/Catalog.js +7 -4
  28. package/dist/lib/plugins/api-catalog/Catalog.js.map +1 -1
  29. package/dist/lib/plugins/api-catalog/index.js +1 -1
  30. package/dist/lib/plugins/api-catalog/index.js.map +1 -1
  31. package/dist/lib/plugins/markdown/MdxPage.js +1 -1
  32. package/dist/lib/plugins/markdown/MdxPage.js.map +1 -1
  33. package/dist/lib/plugins/openapi/OperationList.d.ts +1 -1
  34. package/dist/lib/plugins/openapi/OperationList.js +5 -1
  35. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  36. package/dist/lib/plugins/openapi/OperationListItem.d.ts +1 -1
  37. package/dist/lib/plugins/openapi/OperationListItem.js +6 -3
  38. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  39. package/dist/lib/plugins/openapi/ParameterList.d.ts +2 -1
  40. package/dist/lib/plugins/openapi/ParameterList.js +3 -2
  41. package/dist/lib/plugins/openapi/ParameterList.js.map +1 -1
  42. package/dist/lib/plugins/openapi/PlaygroundDialogWrapper.js +3 -1
  43. package/dist/lib/plugins/openapi/PlaygroundDialogWrapper.js.map +1 -1
  44. package/dist/lib/plugins/openapi/Sidecar.js +1 -1
  45. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  46. package/dist/lib/plugins/openapi/graphql/gql.d.ts +1 -1
  47. package/dist/lib/plugins/openapi/graphql/gql.js +1 -1
  48. package/dist/lib/plugins/openapi/graphql/gql.js.map +1 -1
  49. package/dist/lib/plugins/openapi/graphql/graphql.d.ts +1 -0
  50. package/dist/lib/plugins/openapi/graphql/graphql.js +2 -0
  51. package/dist/lib/plugins/openapi/graphql/graphql.js.map +1 -1
  52. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.d.ts +2 -2
  53. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js +1 -5
  54. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js.map +1 -1
  55. package/dist/lib/plugins/openapi/playground/Headers.js +1 -1
  56. package/dist/lib/plugins/openapi/playground/Headers.js.map +1 -1
  57. package/dist/lib/plugins/openapi/playground/IdentityDialog.d.ts +11 -0
  58. package/dist/lib/plugins/openapi/playground/IdentityDialog.js +14 -0
  59. package/dist/lib/plugins/openapi/playground/IdentityDialog.js.map +1 -0
  60. package/dist/lib/plugins/openapi/playground/IdentitySelector.d.ts +7 -0
  61. package/dist/lib/plugins/openapi/playground/IdentitySelector.js +10 -0
  62. package/dist/lib/plugins/openapi/playground/IdentitySelector.js.map +1 -0
  63. package/dist/lib/plugins/openapi/playground/PathParams.d.ts +3 -2
  64. package/dist/lib/plugins/openapi/playground/PathParams.js +3 -2
  65. package/dist/lib/plugins/openapi/playground/PathParams.js.map +1 -1
  66. package/dist/lib/plugins/openapi/playground/Playground.d.ts +13 -2
  67. package/dist/lib/plugins/openapi/playground/Playground.js +80 -26
  68. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  69. package/dist/lib/plugins/openapi/playground/QueryParams.js +1 -1
  70. package/dist/lib/plugins/openapi/playground/QueryParams.js.map +1 -1
  71. package/dist/lib/plugins/openapi/playground/RequestLoginDialog.d.ts +7 -0
  72. package/dist/lib/plugins/openapi/playground/RequestLoginDialog.js +8 -0
  73. package/dist/lib/plugins/openapi/playground/RequestLoginDialog.js.map +1 -0
  74. package/dist/lib/plugins/openapi/playground/rememberedIdentity.d.ts +17 -0
  75. package/dist/lib/plugins/openapi/playground/rememberedIdentity.js +11 -0
  76. package/dist/lib/plugins/openapi/playground/rememberedIdentity.js.map +1 -0
  77. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js +19 -13
  78. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js.map +1 -1
  79. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.d.ts +6 -4
  80. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js +4 -3
  81. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js.map +1 -1
  82. package/dist/lib/plugins/search-pagefind/PagefindSearch.d.ts +6 -0
  83. package/dist/lib/plugins/search-pagefind/PagefindSearch.js +66 -0
  84. package/dist/lib/plugins/search-pagefind/PagefindSearch.js.map +1 -0
  85. package/dist/lib/plugins/search-pagefind/ResultList.d.ts +8 -0
  86. package/dist/lib/plugins/search-pagefind/ResultList.js +31 -0
  87. package/dist/lib/plugins/search-pagefind/ResultList.js.map +1 -0
  88. package/dist/lib/plugins/search-pagefind/get-results.d.ts +3 -0
  89. package/dist/lib/plugins/search-pagefind/get-results.js +37 -0
  90. package/dist/lib/plugins/search-pagefind/get-results.js.map +1 -0
  91. package/dist/lib/plugins/search-pagefind/index.d.ts +8 -0
  92. package/dist/lib/plugins/search-pagefind/index.js +9 -0
  93. package/dist/lib/plugins/search-pagefind/index.js.map +1 -0
  94. package/dist/lib/plugins/search-pagefind/types.d.ts +85 -0
  95. package/dist/lib/plugins/search-pagefind/types.js +2 -0
  96. package/dist/lib/plugins/search-pagefind/types.js.map +1 -0
  97. package/dist/lib/ui/Checkbox.d.ts +2 -8
  98. package/dist/lib/ui/Checkbox.js +1 -13
  99. package/dist/lib/ui/Checkbox.js.map +1 -1
  100. package/dist/lib/ui/Command.d.ts +13 -7
  101. package/dist/lib/ui/Command.js +2 -2
  102. package/dist/lib/ui/Command.js.map +1 -1
  103. package/dist/lib/ui/Select.js +1 -1
  104. package/dist/lib/ui/Select.js.map +1 -1
  105. package/dist/lib/ui/SyntaxHighlight.d.ts +2 -1
  106. package/dist/lib/ui/SyntaxHighlight.js +19 -15
  107. package/dist/lib/ui/SyntaxHighlight.js.map +1 -1
  108. package/dist/lib/util/MdxComponents.d.ts +1 -1
  109. package/dist/lib/util/MdxComponents.js +2 -2
  110. package/dist/lib/util/MdxComponents.js.map +1 -1
  111. package/dist/lib/util/useScrollToAnchor.js +6 -8
  112. package/dist/lib/util/useScrollToAnchor.js.map +1 -1
  113. package/dist/vite/build.js +4 -0
  114. package/dist/vite/build.js.map +1 -1
  115. package/dist/vite/config.js +7 -2
  116. package/dist/vite/config.js.map +1 -1
  117. package/dist/vite/dev-server.js +8 -0
  118. package/dist/vite/dev-server.js.map +1 -1
  119. package/dist/vite/pagefind.d.ts +4 -0
  120. package/dist/vite/pagefind.js +15 -0
  121. package/dist/vite/pagefind.js.map +1 -0
  122. package/dist/vite/plugin-component.js +4 -0
  123. package/dist/vite/plugin-component.js.map +1 -1
  124. package/dist/vite/plugin-search.js +4 -0
  125. package/dist/vite/plugin-search.js.map +1 -1
  126. package/dist/vite/prerender/prerender.js +1 -1
  127. package/dist/vite/prerender/prerender.js.map +1 -1
  128. package/dist/vite/sitemap.js +2 -1
  129. package/dist/vite/sitemap.js.map +1 -1
  130. package/lib/{AuthenticationPlugin-CiO1FM6Q.js → AuthenticationPlugin-4ip08maU.js} +3 -3
  131. package/lib/{AuthenticationPlugin-CiO1FM6Q.js.map → AuthenticationPlugin-4ip08maU.js.map} +1 -1
  132. package/lib/Callout-B_sEhkYd.js +211 -0
  133. package/lib/Callout-B_sEhkYd.js.map +1 -0
  134. package/lib/{Dialog-DIKGQxQc.js → Dialog-sbgekbjb.js} +47 -32
  135. package/lib/{Dialog-DIKGQxQc.js.map → Dialog-sbgekbjb.js.map} +1 -1
  136. package/lib/{Markdown-DePfm7oQ.js → Markdown-DZXjQjpH.js} +4099 -3848
  137. package/lib/Markdown-DZXjQjpH.js.map +1 -0
  138. package/lib/MdxPage-52vRwa_7.js +200 -0
  139. package/lib/MdxPage-52vRwa_7.js.map +1 -0
  140. package/lib/{OasProvider-SzD9mHJc.js → OasProvider-CR2nG1Eg.js} +4 -4
  141. package/lib/{OasProvider-SzD9mHJc.js.map → OasProvider-CR2nG1Eg.js.map} +1 -1
  142. package/lib/{OperationList-DDs9NblY.js → OperationList-DndcCJUG.js} +2069 -1983
  143. package/lib/OperationList-DndcCJUG.js.map +1 -0
  144. package/lib/{Select-Dqtcn53H.js → Select-FAYHOYTy.js} +4 -4
  145. package/lib/{Select-Dqtcn53H.js.map → Select-FAYHOYTy.js.map} +1 -1
  146. package/lib/{SlotletProvider-DdtIOUi6.js → SlotletProvider-TydSHROc.js} +4 -4
  147. package/lib/{SlotletProvider-DdtIOUi6.js.map → SlotletProvider-TydSHROc.js.map} +1 -1
  148. package/lib/{chunk-IR6S3I6Y-D_3UmFIn.js → chunk-HA7DTUK3-ZGg2W6yV.js} +277 -277
  149. package/lib/chunk-HA7DTUK3-ZGg2W6yV.js.map +1 -0
  150. package/lib/hook-CfCFKZ-2.js +350 -0
  151. package/lib/hook-CfCFKZ-2.js.map +1 -0
  152. package/lib/index-DK7IuUyR.js +2201 -0
  153. package/lib/index-DK7IuUyR.js.map +1 -0
  154. package/lib/{index.esm-CQHE3GEU.js → index.esm-CltAN0Tf.js} +259 -239
  155. package/lib/index.esm-CltAN0Tf.js.map +1 -0
  156. package/lib/{mutation-EclmI0is.js → mutation-B81DztCT.js} +2 -2
  157. package/lib/{mutation-EclmI0is.js.map → mutation-B81DztCT.js.map} +1 -1
  158. package/lib/objectEntries-BS7aAgOm.js +12 -0
  159. package/lib/objectEntries-BS7aAgOm.js.map +1 -0
  160. package/lib/ui/Checkbox.js +15 -25
  161. package/lib/ui/Checkbox.js.map +1 -1
  162. package/lib/ui/Command.js +96 -70
  163. package/lib/ui/Command.js.map +1 -1
  164. package/lib/ui/Select.js +1 -1
  165. package/lib/ui/Select.js.map +1 -1
  166. package/lib/ui/SyntaxHighlight.js +483 -502
  167. package/lib/ui/SyntaxHighlight.js.map +1 -1
  168. package/lib/{useExposedProps-RIvey2Oy.js → useExposedProps-BslIn-FE.js} +2 -2
  169. package/lib/{useExposedProps-RIvey2Oy.js.map → useExposedProps-BslIn-FE.js.map} +1 -1
  170. package/lib/useQuery-CQUwWR9i.js +1137 -0
  171. package/lib/useQuery-CQUwWR9i.js.map +1 -0
  172. package/lib/zudoku.auth-auth0.js +1 -1
  173. package/lib/zudoku.auth-clerk.js +29 -29
  174. package/lib/zudoku.auth-clerk.js.map +1 -1
  175. package/lib/zudoku.auth-openid.js +3 -3
  176. package/lib/zudoku.components.js +146 -149
  177. package/lib/zudoku.components.js.map +1 -1
  178. package/lib/zudoku.hooks.js +1 -1
  179. package/lib/zudoku.plugin-api-catalog.js +87 -71
  180. package/lib/zudoku.plugin-api-catalog.js.map +1 -1
  181. package/lib/zudoku.plugin-api-keys.js +16 -15
  182. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  183. package/lib/zudoku.plugin-custom-pages.js +2 -2
  184. package/lib/zudoku.plugin-markdown.js +1 -1
  185. package/lib/zudoku.plugin-openapi.js +3 -3
  186. package/lib/zudoku.plugin-redirect.js +1 -1
  187. package/lib/zudoku.plugin-search-pagefind.js +204 -0
  188. package/lib/zudoku.plugin-search-pagefind.js.map +1 -0
  189. package/package.json +10 -5
  190. package/src/lib/authentication/hook.ts +12 -1
  191. package/src/lib/authentication/providers/clerk.tsx +10 -6
  192. package/src/lib/components/AnchorLink.tsx +7 -7
  193. package/src/lib/components/Banner.tsx +1 -0
  194. package/src/lib/components/Heading.tsx +1 -1
  195. package/src/lib/components/Layout.tsx +1 -0
  196. package/src/lib/components/navigation/SidebarItem.tsx +8 -23
  197. package/src/lib/core/RouteGuard.tsx +2 -1
  198. package/src/lib/core/ZudokuContext.ts +4 -0
  199. package/src/lib/plugins/api-catalog/Catalog.tsx +23 -7
  200. package/src/lib/plugins/api-catalog/index.tsx +1 -0
  201. package/src/lib/plugins/markdown/MdxPage.tsx +5 -1
  202. package/src/lib/plugins/openapi/OperationList.tsx +83 -31
  203. package/src/lib/plugins/openapi/OperationListItem.tsx +107 -86
  204. package/src/lib/plugins/openapi/ParameterList.tsx +4 -0
  205. package/src/lib/plugins/openapi/PlaygroundDialogWrapper.tsx +7 -0
  206. package/src/lib/plugins/openapi/Sidecar.tsx +1 -0
  207. package/src/lib/plugins/openapi/graphql/gql.ts +3 -3
  208. package/src/lib/plugins/openapi/graphql/graphql.ts +3 -0
  209. package/src/lib/plugins/openapi/playground/ExamplesDropdown.tsx +30 -32
  210. package/src/lib/plugins/openapi/playground/Headers.tsx +0 -1
  211. package/src/lib/plugins/openapi/playground/IdentityDialog.tsx +74 -0
  212. package/src/lib/plugins/openapi/playground/IdentitySelector.tsx +54 -0
  213. package/src/lib/plugins/openapi/playground/PathParams.tsx +8 -2
  214. package/src/lib/plugins/openapi/playground/Playground.tsx +175 -88
  215. package/src/lib/plugins/openapi/playground/QueryParams.tsx +0 -1
  216. package/src/lib/plugins/openapi/playground/RequestLoginDialog.tsx +51 -0
  217. package/src/lib/plugins/openapi/playground/rememberedIdentity.ts +26 -0
  218. package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +24 -4
  219. package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +66 -45
  220. package/src/lib/plugins/search-pagefind/PagefindSearch.tsx +135 -0
  221. package/src/lib/plugins/search-pagefind/ResultList.tsx +104 -0
  222. package/src/lib/plugins/search-pagefind/get-results.tsx +54 -0
  223. package/src/lib/plugins/search-pagefind/index.tsx +21 -0
  224. package/src/lib/plugins/search-pagefind/types.ts +118 -0
  225. package/src/lib/ui/Checkbox.tsx +8 -24
  226. package/src/lib/ui/Command.tsx +25 -3
  227. package/src/lib/ui/Select.tsx +1 -1
  228. package/src/lib/ui/SyntaxHighlight.tsx +94 -96
  229. package/src/lib/util/MdxComponents.tsx +2 -2
  230. package/src/lib/util/useScrollToAnchor.ts +8 -8
  231. package/lib/Markdown-DePfm7oQ.js.map +0 -1
  232. package/lib/MdxPage-DZTt9ld7.js +0 -193
  233. package/lib/MdxPage-DZTt9ld7.js.map +0 -1
  234. package/lib/OperationList-DDs9NblY.js.map +0 -1
  235. package/lib/chunk-IR6S3I6Y-D_3UmFIn.js.map +0 -1
  236. package/lib/hook-CN__aZIt.js +0 -1464
  237. package/lib/hook-CN__aZIt.js.map +0 -1
  238. package/lib/index-CibzSNks.js +0 -2100
  239. package/lib/index-CibzSNks.js.map +0 -1
  240. package/lib/index.esm-CQHE3GEU.js.map +0 -1
  241. package/lib/objectEntries-yMIkr2mI.js +0 -5
  242. package/lib/objectEntries-yMIkr2mI.js.map +0 -1
  243. package/lib/useScrollToAnchor-C7ilRSts.js +0 -290
  244. package/lib/useScrollToAnchor-C7ilRSts.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
2
  import { ChevronRightIcon } from "lucide-react";
3
3
  import { Fragment, useState } from "react";
4
+ import { Callout } from "zudoku/ui/Callout.js";
4
5
  import {
5
6
  Collapsible,
6
7
  CollapsibleContent,
@@ -30,6 +31,14 @@ const statusCodeMap: Record<number, string> = {
30
31
  500: "Internal Server Error",
31
32
  };
32
33
 
34
+ const humanFileSize = (bytes: number) => {
35
+ const exponent = Math.floor(Math.log(bytes) / Math.log(1000.0));
36
+ const decimal = (bytes / Math.pow(1000.0, exponent)).toFixed(
37
+ exponent ? 2 : 0,
38
+ );
39
+ return `${decimal} ${exponent ? `${"kMGTPEZY"[exponent - 1]}B` : "B"}`;
40
+ };
41
+
33
42
  const mimeTypeToLanguage = (mimeType: string) => {
34
43
  const mimeTypeMapping = {
35
44
  "application/json": "json",
@@ -83,6 +92,8 @@ const sortHeadersByRelevance = (
83
92
  });
84
93
  };
85
94
 
95
+ const SYNTAX_HIGHLIGHT_MAX_SIZE_THRESHOLD = 64_000;
96
+
86
97
  export const ResponseTab = ({
87
98
  body = "",
88
99
  headers,
@@ -114,9 +125,10 @@ export const ResponseTab = ({
114
125
  });
115
126
 
116
127
  const sortedHeaders = sortHeadersByRelevance([...headers]);
128
+ const shouldDisableHighlighting = size > SYNTAX_HIGHLIGHT_MAX_SIZE_THRESHOLD;
117
129
 
118
130
  return (
119
- <div className="flex flex-col gap-2 h-full overflow-y-scroll max-h-[calc(100vh-220px)] py-4">
131
+ <div className="flex flex-col gap-2 h-full overflow-auto max-h-[calc(100vh-220px)] ">
120
132
  <Collapsible defaultOpen>
121
133
  <CollapsibleTrigger className="flex items-center gap-2 hover:text-primary group">
122
134
  <ChevronRightIcon className="h-4 w-4 transition-transform duration-200 group-data-[state=open]:rotate-[90deg]" />
@@ -151,6 +163,12 @@ export const ResponseTab = ({
151
163
  </Collapsible>
152
164
 
153
165
  <Card className="shadow-none">
166
+ {shouldDisableHighlighting && (
167
+ <Callout type="info" className="my-0 p-2">
168
+ Code highlight is disabled for responses larger than{" "}
169
+ {humanFileSize(SYNTAX_HIGHLIGHT_MAX_SIZE_THRESHOLD)}
170
+ </Callout>
171
+ )}
154
172
  <SyntaxHighlight
155
173
  language={
156
174
  view === "types"
@@ -161,8 +179,9 @@ export const ResponseTab = ({
161
179
  : detectedLanguage
162
180
  : "json"
163
181
  }
182
+ showCopy="always"
183
+ disabled={shouldDisableHighlighting}
164
184
  noBackground
165
- // playground dialog has h-5/6 ≈ 83.333vh
166
185
  className="overflow-x-auto p-4 text-xs max-h-[calc(83.333vh-180px)]"
167
186
  code={
168
187
  (view === "raw"
@@ -173,7 +192,7 @@ export const ResponseTab = ({
173
192
  }
174
193
  />
175
194
  </Card>
176
- <div className="flex gap-2 justify-between">
195
+ <div className="flex gap-2 justify-between items-center">
177
196
  <div className="flex text-xs gap-2 border bg-muted rounded-md p-2 items-center h-8 font-mono divide-x">
178
197
  <div>
179
198
  <span className="text-muted-foreground">Status</span> {status}{" "}
@@ -184,7 +203,8 @@ export const ResponseTab = ({
184
203
  {time.toFixed(0)}ms
185
204
  </div>
186
205
  <div>
187
- <span className="text-muted-foreground">Size</span> {size}B
206
+ <span className="text-muted-foreground">Size</span>{" "}
207
+ {humanFileSize(size)}
188
208
  </div>
189
209
  </div>
190
210
  {jsonContent && (
@@ -1,5 +1,6 @@
1
- import { UseMutationResult } from "@tanstack/react-query";
1
+ import { type UseMutationResult } from "@tanstack/react-query";
2
2
  import { Spinner } from "../../../../components/Spinner.js";
3
+ import { Button } from "../../../../ui/Button.js";
3
4
  import { Callout } from "../../../../ui/Callout.js";
4
5
  import {
5
6
  Card,
@@ -14,20 +15,24 @@ import {
14
15
  TabsTrigger,
15
16
  } from "../../../../ui/Tabs.js";
16
17
  import { cn } from "../../../../util/cn.js";
17
- import { PlaygroundResult } from "../Playground.js";
18
+ import { type PlaygroundResult } from "../Playground.js";
18
19
  import { RequestTab } from "./RequestTab.js";
19
20
  import { ResponseTab } from "./ResponseTab.js";
20
21
 
21
22
  export const ResultPanel = ({
22
23
  queryMutation,
23
24
  showPathParamsWarning,
25
+ showLongRunningWarning,
26
+ onCancel,
24
27
  }: {
25
- queryMutation: UseMutationResult<PlaygroundResult, Error, any, unknown>;
28
+ queryMutation: UseMutationResult<PlaygroundResult, Error, any>;
26
29
  showPathParamsWarning: boolean;
30
+ showLongRunningWarning?: boolean;
31
+ onCancel?: () => void;
27
32
  }) => {
28
33
  const status = ((queryMutation.data?.status ?? 0) / 100).toFixed(0);
29
34
  return (
30
- <div className="min-w-0 p-8 bg-muted/70 overflow-y-auto">
35
+ <div className="min-w-0 p-4 bg-muted/50">
31
36
  {queryMutation.error ? (
32
37
  <div className="flex flex-col gap-2">
33
38
  {showPathParamsWarning && (
@@ -49,49 +54,65 @@ export const ResultPanel = ({
49
54
  </Card>
50
55
  </div>
51
56
  ) : queryMutation.data ? (
52
- <div className="flex flex-col gap-2">
53
- <Tabs defaultValue="response">
54
- <TabsList>
55
- <TabsTrigger value="request">Request</TabsTrigger>
56
- <TabsTrigger value="response">
57
- Response
58
- <span
59
- className={cn(
60
- "text-xs font-mono ml-1",
61
- status === "2" && "text-green-500",
62
- status === "3" && "text-blue-500",
63
- status === "4" && "text-yellow-500",
64
- status === "5" && "text-red-500",
65
- )}
66
- >
67
- ({queryMutation.data.status})
68
- </span>
69
- </TabsTrigger>
70
- </TabsList>
71
- <TabsContent value="request">
72
- <RequestTab {...queryMutation.data.request} />
73
- </TabsContent>
74
- <TabsContent value="response">
75
- <ResponseTab
76
- status={queryMutation.data.status}
77
- time={queryMutation.data.time}
78
- size={queryMutation.data.size}
79
- headers={queryMutation.data.headers}
80
- body={queryMutation.data.body}
81
- url={queryMutation.data.request.url}
82
- />
83
- </TabsContent>
84
- </Tabs>
85
- </div>
57
+ <Tabs defaultValue="response">
58
+ <TabsList>
59
+ <TabsTrigger value="request">Request</TabsTrigger>
60
+ <TabsTrigger value="response">
61
+ Response
62
+ <span
63
+ className={cn(
64
+ "text-xs font-mono ml-1",
65
+ status === "2" && "text-green-500",
66
+ status === "3" && "text-blue-500",
67
+ status === "4" && "text-yellow-500",
68
+ status === "5" && "text-red-500",
69
+ )}
70
+ >
71
+ ({queryMutation.data.status})
72
+ </span>
73
+ </TabsTrigger>
74
+ </TabsList>
75
+ <TabsContent value="request">
76
+ <RequestTab {...queryMutation.data.request} />
77
+ </TabsContent>
78
+ <TabsContent value="response">
79
+ <ResponseTab
80
+ status={queryMutation.data.status}
81
+ time={queryMutation.data.time}
82
+ size={queryMutation.data.size}
83
+ headers={queryMutation.data.headers}
84
+ body={queryMutation.data.body}
85
+ url={queryMutation.data.request.url}
86
+ />
87
+ </TabsContent>
88
+ </Tabs>
86
89
  ) : (
87
90
  <div className="grid place-items-center h-full">
88
- <span className="text-[16px] font-semibold text-muted-foreground">
89
- {queryMutation.isPending ? (
90
- <Spinner />
91
- ) : (
92
- "Send a request first to see the response here"
93
- )}
94
- </span>
91
+ {queryMutation.isPending ? (
92
+ <div className="flex flex-col gap-2 items-center mt-20">
93
+ <Spinner size={20} />
94
+ <div
95
+ className={cn(
96
+ "opacity-0 pointer-events-none transition-opacity h-20 text-sm text-muted-foreground duration-300 flex flex-col gap-2 items-center",
97
+ showLongRunningWarning && "opacity-100 pointer-events-auto",
98
+ )}
99
+ >
100
+ Looks like the request is taking longer than expected.
101
+ <Button
102
+ onClick={onCancel}
103
+ size="sm"
104
+ className="w-fit"
105
+ variant="outline"
106
+ >
107
+ Cancel
108
+ </Button>
109
+ </div>
110
+ </div>
111
+ ) : (
112
+ <span className="text-[16px] font-semibold text-muted-foreground">
113
+ Send a request first to see the response here
114
+ </span>
115
+ )}
95
116
  </div>
96
117
  )}
97
118
  </div>
@@ -0,0 +1,135 @@
1
+ import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
2
+ import { keepPreviousData, useQuery } from "@tanstack/react-query";
3
+ import { useState } from "react";
4
+ import { Callout } from "zudoku/ui/Callout.js";
5
+ import {
6
+ CommandDialog,
7
+ CommandEmpty,
8
+ CommandInput,
9
+ } from "zudoku/ui/Command.js";
10
+ import { DialogTitle } from "zudoku/ui/Dialog.js";
11
+ import { joinUrl } from "../../util/joinUrl.js";
12
+ import { getResults } from "./get-results.js";
13
+ import type { PagefindOptions } from "./index.js";
14
+ import { ResultList } from "./ResultList.js";
15
+ import type { Pagefind } from "./types.js";
16
+
17
+ const DEFAULT_RANKING = {
18
+ // Slightly lower than default because API docs tend to have repetitive terms (parameter names, HTTP methods, etc.)
19
+ termFrequency: 0.8,
20
+ // Lower than default because API documentation pages tend to be longer due to comprehensive endpoint documentation
21
+ pageLength: 0.6,
22
+ // Slightly higher than default because in technical documentation, exact matches should be prioritized
23
+ termSimilarity: 1.2,
24
+ // Slightly lower than default because API docs might have legitimate repetition of terms
25
+ termSaturation: 1.2,
26
+ };
27
+
28
+ const importPagefind = (basePath?: string): Promise<Pagefind> =>
29
+ import.meta.env.DEV
30
+ ? // @ts-expect-error TypeScript can't resolve the import
31
+ import(/* @vite-ignore */ "/pagefind/pagefind.js")
32
+ : import(/* @vite-ignore */ joinUrl(basePath, "/pagefind/pagefind.js"));
33
+
34
+ const usePagefind = (options: PagefindOptions) => {
35
+ const { data: pagefind, ...result } = useQuery<Pagefind>({
36
+ queryKey: ["pagefind", options.ranking],
37
+ retry: false,
38
+ queryFn: async () => {
39
+ const pagefind = await importPagefind(options.basePath);
40
+ await pagefind.init();
41
+ await pagefind.options({
42
+ ranking: {
43
+ termFrequency:
44
+ options.ranking?.termFrequency ?? DEFAULT_RANKING.termFrequency,
45
+ pageLength: options.ranking?.pageLength ?? DEFAULT_RANKING.pageLength,
46
+ termSimilarity:
47
+ options.ranking?.termSimilarity ?? DEFAULT_RANKING.termSimilarity,
48
+ termSaturation:
49
+ options.ranking?.termSaturation ?? DEFAULT_RANKING.termSaturation,
50
+ },
51
+ });
52
+
53
+ return pagefind;
54
+ },
55
+ enabled: typeof window !== "undefined",
56
+ });
57
+
58
+ if (result.isError) {
59
+ // eslint-disable-next-line no-console
60
+ console.error(result.error);
61
+ }
62
+
63
+ return { ...result, pagefind };
64
+ };
65
+
66
+ export const PagefindSearch = ({
67
+ isOpen,
68
+ onClose,
69
+ options,
70
+ }: {
71
+ isOpen: boolean;
72
+ onClose: () => void;
73
+ options: PagefindOptions;
74
+ }) => {
75
+ const { pagefind, error, isError } = usePagefind(options);
76
+ const [searchTerm, setSearchTerm] = useState("");
77
+
78
+ const { data: searchResults } = useQuery({
79
+ queryKey: ["pagefind-search", searchTerm],
80
+ queryFn: async () => {
81
+ const search = await pagefind?.search(searchTerm);
82
+ if (!search) return [];
83
+ return getResults(search, options);
84
+ },
85
+ placeholderData: keepPreviousData,
86
+ enabled: !!pagefind && !!searchTerm,
87
+ });
88
+
89
+ return (
90
+ <CommandDialog
91
+ command={{ shouldFilter: false }}
92
+ content={{ className: "max-w-[750px]" }}
93
+ open={isOpen}
94
+ onOpenChange={onClose}
95
+ >
96
+ <VisuallyHidden>
97
+ <DialogTitle>Search</DialogTitle>
98
+ </VisuallyHidden>
99
+ <CommandInput
100
+ placeholder="Search..."
101
+ value={searchTerm}
102
+ onValueChange={setSearchTerm}
103
+ disabled={isError}
104
+ />
105
+ <CommandEmpty>
106
+ {searchTerm ? "No results found." : "Start typing to search"}
107
+ </CommandEmpty>
108
+ {isError ? (
109
+ <div className="p-4 text-sm">
110
+ {error.message === "NOT_BUILT_YET" ? (
111
+ <Callout type="info">
112
+ Search is currently not available in development mode by default.
113
+ <br />
114
+ To still use search in development, run <code>
115
+ zudoku build
116
+ </code>{" "}
117
+ and copy the <code>dist/pagefind</code> directory to your{" "}
118
+ <code>public</code> directory.
119
+ </Callout>
120
+ ) : (
121
+ "An error occurred while loading search."
122
+ )}
123
+ </div>
124
+ ) : (
125
+ <ResultList
126
+ basePath={options.basePath}
127
+ searchResults={searchResults ?? []}
128
+ searchTerm={searchTerm}
129
+ onClose={onClose}
130
+ maxSubResults={options.maxSubResults}
131
+ />
132
+ )}
133
+ </CommandDialog>
134
+ );
135
+ };
@@ -0,0 +1,104 @@
1
+ import { FileTextIcon } from "lucide-react";
2
+ import { useCallback } from "react";
3
+ import { Link, useNavigate } from "react-router";
4
+ import { CommandGroup, CommandItem, CommandList } from "zudoku/ui/Command.js";
5
+ import {
6
+ type PagefindSearchFragment,
7
+ type PagefindSubResult,
8
+ } from "./types.js";
9
+
10
+ const sortSubResults = (a: PagefindSubResult, b: PagefindSubResult) => {
11
+ const aScore = a.weighted_locations.reduce(
12
+ (sum, loc) => sum + loc.balanced_score,
13
+ 0,
14
+ );
15
+ const bScore = b.weighted_locations.reduce(
16
+ (sum, loc) => sum + loc.balanced_score,
17
+ 0,
18
+ );
19
+ return bScore - aScore;
20
+ };
21
+
22
+ const hoverClassname = `cursor-pointer border border-transparent data-[selected=true]:border-border`;
23
+
24
+ export const ResultList = ({
25
+ basePath,
26
+ searchResults,
27
+ searchTerm,
28
+ onClose,
29
+ maxSubResults = 4,
30
+ }: {
31
+ basePath?: string;
32
+ searchResults: PagefindSearchFragment[];
33
+ searchTerm: string;
34
+ onClose: () => void;
35
+ maxSubResults?: number;
36
+ }) => {
37
+ const navigate = useNavigate();
38
+
39
+ const cleanResultUrl = useCallback(
40
+ (url: string) => {
41
+ const clean = url.replace(".html", "");
42
+ return basePath && clean.startsWith(basePath)
43
+ ? clean.slice(basePath.length)
44
+ : clean;
45
+ },
46
+ [basePath],
47
+ );
48
+
49
+ return (
50
+ <CommandList className="max-h-[450px]">
51
+ {searchTerm && searchResults.length > 0 && (
52
+ <CommandGroup
53
+ className="text-sm text-muted-foreground"
54
+ heading={`${searchResults.length} results for "${searchTerm}"`}
55
+ />
56
+ )}
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
+ }}
69
+ >
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
+ ))}
102
+ </CommandList>
103
+ );
104
+ };
@@ -0,0 +1,54 @@
1
+ import type { PagefindOptions } from "./index.js";
2
+ import type { PagefindSearchFragment, PagefindSearchResults } from "./types.js";
3
+
4
+ export const getResults = async (
5
+ search: PagefindSearchResults,
6
+ options: PagefindOptions,
7
+ ) => {
8
+ const maxResults = options.maxResults ?? 10;
9
+ const transformFn = options.transformResults ?? (() => true);
10
+
11
+ const transformedResults: PagefindSearchFragment[] = [];
12
+
13
+ const generator = searchResultGenerator(search, transformFn);
14
+
15
+ for await (const result of generator) {
16
+ transformedResults.push(result);
17
+ if (transformedResults.length >= maxResults) break;
18
+ }
19
+
20
+ return transformedResults;
21
+ };
22
+
23
+ async function* searchResultGenerator(
24
+ search: PagefindSearchResults,
25
+ transformFn: NonNullable<PagefindOptions["transformResults"]>,
26
+ ) {
27
+ const batchSize = 5;
28
+ let processedCount = 0;
29
+
30
+ while (processedCount < search.results.length) {
31
+ const batch = search.results.slice(
32
+ processedCount,
33
+ processedCount + batchSize,
34
+ );
35
+ processedCount += batch.length;
36
+
37
+ const batchData = await Promise.all(batch.map((result) => result.data()));
38
+
39
+ for (const result of batchData) {
40
+ const transformed = transformFn(result);
41
+
42
+ if (transformed === false) {
43
+ // Skip this result
44
+ continue;
45
+ } else if (transformed === true || transformed == null) {
46
+ // Keep the original result
47
+ yield result;
48
+ } else {
49
+ // Return the transformed result
50
+ yield transformed;
51
+ }
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,21 @@
1
+ import type { ZudokuConfig } from "../../../config/validators/validate.js";
2
+ import { ClientOnly } from "../../components/ClientOnly.js";
3
+ import type { ZudokuPlugin } from "../../core/plugins.js";
4
+ import { PagefindSearch } from "./PagefindSearch.js";
5
+
6
+ export type PagefindOptions = Extract<
7
+ ZudokuConfig["search"],
8
+ { type: "pagefind" }
9
+ > & { basePath?: string };
10
+
11
+ export const pagefindSearchPlugin = (
12
+ options: PagefindOptions,
13
+ ): ZudokuPlugin => {
14
+ return {
15
+ renderSearch: ({ isOpen, onClose }) => (
16
+ <ClientOnly>
17
+ <PagefindSearch isOpen={isOpen} onClose={onClose} options={options} />
18
+ </ClientOnly>
19
+ ),
20
+ };
21
+ };
@@ -0,0 +1,118 @@
1
+ interface PagefindIndexOptions {
2
+ basePath?: string;
3
+ baseUrl?: string;
4
+ excerptLength?: number;
5
+ indexWeight?: number;
6
+ mergeFilter?: Record<string, unknown>;
7
+ highlightParam?: string;
8
+ language?: string;
9
+ primary?: boolean;
10
+ ranking?: PagefindRankingWeights;
11
+ }
12
+
13
+ interface PagefindRankingWeights {
14
+ termSimilarity?: number;
15
+ pageLength?: number;
16
+ termSaturation?: number;
17
+ termFrequency?: number;
18
+ }
19
+
20
+ interface PagefindSearchOptions {
21
+ preload?: boolean;
22
+ verbose?: boolean;
23
+ filters?: Record<string, unknown>;
24
+ sort?: Record<string, unknown>;
25
+ }
26
+
27
+ type PagefindFilterCounts = Record<string, Record<string, number>> & {};
28
+
29
+ interface PagefindSearchResults {
30
+ results: PagefindSearchResult[];
31
+ unfilteredResultCount: number;
32
+ filters: PagefindFilterCounts;
33
+ totalFilters: PagefindFilterCounts;
34
+ timings: {
35
+ preload: number;
36
+ search: number;
37
+ total: number;
38
+ };
39
+ }
40
+
41
+ interface PagefindSearchResult {
42
+ id: string;
43
+ score: number;
44
+ words: number[];
45
+ data: () => Promise<PagefindSearchFragment>;
46
+ }
47
+
48
+ interface PagefindSearchFragment {
49
+ url: string;
50
+ raw_url?: string;
51
+ content: string;
52
+ raw_content?: string;
53
+ excerpt: string;
54
+ sub_results: PagefindSubResult[];
55
+ word_count: number;
56
+ locations: number[];
57
+ weighted_locations: PagefindWordLocation[];
58
+ filters: Record<string, string[]>;
59
+ meta: Record<string, string>;
60
+ anchors: PagefindSearchAnchor[];
61
+ }
62
+
63
+ interface PagefindSubResult {
64
+ title: string;
65
+ url: string;
66
+ locations: number[];
67
+ weighted_locations: PagefindWordLocation[];
68
+ excerpt: string;
69
+ anchor?: PagefindSearchAnchor;
70
+ }
71
+
72
+ interface PagefindWordLocation {
73
+ weight: number;
74
+ balanced_score: number;
75
+ location: number;
76
+ }
77
+
78
+ interface PagefindSearchAnchor {
79
+ element: string;
80
+ id: string;
81
+ text?: string;
82
+ location: number;
83
+ }
84
+
85
+ interface Pagefind {
86
+ debouncedSearch: (
87
+ query: string,
88
+ options?: PagefindSearchOptions,
89
+ duration?: number,
90
+ ) => Promise<PagefindSearchResults>;
91
+ destroy: () => Promise<void>;
92
+ filters: () => Promise<PagefindFilterCounts>;
93
+ init: () => Promise<void>;
94
+ mergeIndex: (
95
+ indexPath: string,
96
+ options?: Record<string, unknown>,
97
+ ) => Promise<void>;
98
+ options: (options: PagefindIndexOptions) => Promise<void>;
99
+ preload: (term: string, options?: PagefindIndexOptions) => Promise<void>;
100
+ search: (
101
+ term: string,
102
+ options?: PagefindSearchOptions,
103
+ ) => Promise<PagefindSearchResults>;
104
+ }
105
+
106
+ export type {
107
+ Pagefind,
108
+ PagefindFilterCounts,
109
+ PagefindIndexOptions,
110
+ PagefindRankingWeights,
111
+ PagefindSearchAnchor,
112
+ PagefindSearchFragment,
113
+ PagefindSearchOptions,
114
+ PagefindSearchResult,
115
+ PagefindSearchResults,
116
+ PagefindSubResult,
117
+ PagefindWordLocation,
118
+ };