zudoku 0.46.1 → 0.46.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 (168) hide show
  1. package/dist/config/validators/validate.d.ts +12 -12
  2. package/dist/config/validators/validate.js +3 -1
  3. package/dist/config/validators/validate.js.map +1 -1
  4. package/dist/lib/authentication/AuthenticationPlugin.d.ts +0 -5
  5. package/dist/lib/authentication/AuthenticationPlugin.js +0 -12
  6. package/dist/lib/authentication/AuthenticationPlugin.js.map +1 -1
  7. package/dist/lib/plugins/api-keys/SettingsApiKeys.js +110 -15
  8. package/dist/lib/plugins/api-keys/SettingsApiKeys.js.map +1 -1
  9. package/dist/lib/plugins/api-keys/index.d.ts +3 -3
  10. package/dist/lib/plugins/api-keys/index.js +14 -14
  11. package/dist/lib/plugins/api-keys/index.js.map +1 -1
  12. package/dist/lib/plugins/openapi/Sidecar.js +11 -91
  13. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  14. package/dist/lib/plugins/openapi/playground/BodyPanel.d.ts +5 -0
  15. package/dist/lib/plugins/openapi/playground/BodyPanel.js +22 -0
  16. package/dist/lib/plugins/openapi/playground/BodyPanel.js.map +1 -0
  17. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js +2 -1
  18. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js.map +1 -1
  19. package/dist/lib/plugins/openapi/playground/Headers.js +25 -25
  20. package/dist/lib/plugins/openapi/playground/Headers.js.map +1 -1
  21. package/dist/lib/plugins/openapi/playground/IdentitySelector.js +1 -1
  22. package/dist/lib/plugins/openapi/playground/IdentitySelector.js.map +1 -1
  23. package/dist/lib/plugins/openapi/playground/ParamsGrid.js +2 -2
  24. package/dist/lib/plugins/openapi/playground/ParamsGrid.js.map +1 -1
  25. package/dist/lib/plugins/openapi/playground/PathParams.js +1 -1
  26. package/dist/lib/plugins/openapi/playground/PathParams.js.map +1 -1
  27. package/dist/lib/plugins/openapi/playground/Playground.d.ts +3 -8
  28. package/dist/lib/plugins/openapi/playground/Playground.js +70 -65
  29. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  30. package/dist/lib/plugins/openapi/playground/PlaygroundDialog.js +1 -1
  31. package/dist/lib/plugins/openapi/playground/PlaygroundDialog.js.map +1 -1
  32. package/dist/lib/plugins/openapi/playground/QueryParams.js +1 -1
  33. package/dist/lib/plugins/openapi/playground/QueryParams.js.map +1 -1
  34. package/dist/lib/plugins/openapi/playground/fileUtils.d.ts +2 -0
  35. package/dist/lib/plugins/openapi/playground/fileUtils.js +22 -0
  36. package/dist/lib/plugins/openapi/playground/fileUtils.js.map +1 -0
  37. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.d.ts +4 -1
  38. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js +29 -20
  39. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js.map +1 -1
  40. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js +2 -2
  41. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js.map +1 -1
  42. package/dist/lib/plugins/openapi/playground/useRememberSkipLoginDialog.d.ts +16 -0
  43. package/dist/lib/plugins/openapi/playground/useRememberSkipLoginDialog.js +10 -0
  44. package/dist/lib/plugins/openapi/playground/useRememberSkipLoginDialog.js.map +1 -0
  45. package/dist/lib/plugins/openapi/util/createHttpSnippet.d.ts +11 -0
  46. package/dist/lib/plugins/openapi/util/createHttpSnippet.js +89 -0
  47. package/dist/lib/plugins/openapi/util/createHttpSnippet.js.map +1 -0
  48. package/dist/lib/shiki.d.ts +8 -12
  49. package/dist/lib/shiki.js +11 -13
  50. package/dist/lib/shiki.js.map +1 -1
  51. package/dist/lib/ui/CodeBlock.js +1 -1
  52. package/dist/lib/ui/CodeBlock.js.map +1 -1
  53. package/dist/lib/ui/Dialog.d.ts +3 -1
  54. package/dist/lib/ui/Dialog.js +2 -2
  55. package/dist/lib/ui/Dialog.js.map +1 -1
  56. package/dist/lib/util/humanFileSize.d.ts +6 -0
  57. package/dist/lib/util/humanFileSize.js +14 -0
  58. package/dist/lib/util/humanFileSize.js.map +1 -0
  59. package/dist/lib/util/humanFileSize.test.d.ts +1 -0
  60. package/dist/lib/util/humanFileSize.test.js +22 -0
  61. package/dist/lib/util/humanFileSize.test.js.map +1 -0
  62. package/lib/{Callout-BkgOUkoZ.js → Callout-CoVxYafP.js} +2 -2
  63. package/lib/{Callout-BkgOUkoZ.js.map → Callout-CoVxYafP.js.map} +1 -1
  64. package/lib/{Dialog-Du6WMcIA.js → Dialog-BxpuVLh9.js} +25 -25
  65. package/lib/Dialog-BxpuVLh9.js.map +1 -0
  66. package/lib/{Markdown-BRAyzyUJ.js → Markdown-Cm4kj26S.js} +6 -5
  67. package/lib/{Markdown-BRAyzyUJ.js.map → Markdown-Cm4kj26S.js.map} +1 -1
  68. package/lib/{MdxPage-B3v1BSKr.js → MdxPage-fDGQtB5w.js} +5 -5
  69. package/lib/{MdxPage-B3v1BSKr.js.map → MdxPage-fDGQtB5w.js.map} +1 -1
  70. package/lib/{OasProvider-5jrFuhVk.js → OasProvider-CFBvfR3r.js} +3 -3
  71. package/lib/{OasProvider-5jrFuhVk.js.map → OasProvider-CFBvfR3r.js.map} +1 -1
  72. package/lib/{OperationList-BmoMLQPO.js → OperationList-Xs4KWmsh.js} +1139 -1131
  73. package/lib/OperationList-Xs4KWmsh.js.map +1 -0
  74. package/lib/{Pagination-Cr0fWZS3.js → Pagination-CCxhL836.js} +2 -2
  75. package/lib/{Pagination-Cr0fWZS3.js.map → Pagination-CCxhL836.js.map} +1 -1
  76. package/lib/{RouteGuard-PrSVLbSr.js → RouteGuard-CZ_uLv3g.js} +6 -6
  77. package/lib/{RouteGuard-PrSVLbSr.js.map → RouteGuard-CZ_uLv3g.js.map} +1 -1
  78. package/lib/{SchemaList-B4riYLoP.js → SchemaList-BWaNlmUJ.js} +6 -6
  79. package/lib/{SchemaList-B4riYLoP.js.map → SchemaList-BWaNlmUJ.js.map} +1 -1
  80. package/lib/{SchemaView-CPZ6RgsF.js → SchemaView-DdKJt2ln.js} +3 -3
  81. package/lib/{SchemaView-CPZ6RgsF.js.map → SchemaView-DdKJt2ln.js.map} +1 -1
  82. package/lib/{SignUp-CWaiH0tY.js → SignUp-B-1Pvc-8.js} +3 -3
  83. package/lib/{SignUp-CWaiH0tY.js.map → SignUp-B-1Pvc-8.js.map} +1 -1
  84. package/lib/{Slot-Bo6K4tnb.js → Slot-B99cbD-q.js} +11 -11
  85. package/lib/{Slot-Bo6K4tnb.js.map → Slot-B99cbD-q.js.map} +1 -1
  86. package/lib/{SyntaxHighlight-DedRjJNr.js → SyntaxHighlight-Cz6Me7-F.js} +4474 -3323
  87. package/lib/SyntaxHighlight-Cz6Me7-F.js.map +1 -0
  88. package/lib/{Toc-lL3fzNkl.js → Toc-Qe7A4uj_.js} +2 -2
  89. package/lib/{Toc-lL3fzNkl.js.map → Toc-Qe7A4uj_.js.map} +1 -1
  90. package/lib/{chunk-BAXFHI7N-C9WnHsLV.js → chunk-DQRVZFIR-BblmKnHy.js} +697 -697
  91. package/lib/chunk-DQRVZFIR-BblmKnHy.js.map +1 -0
  92. package/lib/{circular-oB4auIIg.js → circular-w5eL5J8a.js} +1812 -1807
  93. package/lib/circular-w5eL5J8a.js.map +1 -0
  94. package/lib/{createServer-DCB82j2t.js → createServer-p3yUA8Bu.js} +3648 -3493
  95. package/lib/createServer-p3yUA8Bu.js.map +1 -0
  96. package/lib/{hook-DawSLaZr.js → hook-k7PfUIsj.js} +10 -10
  97. package/lib/{hook-DawSLaZr.js.map → hook-k7PfUIsj.js.map} +1 -1
  98. package/lib/{index-BXYvD5-7.js → index-yqBxBqxF.js} +1053 -1095
  99. package/lib/index-yqBxBqxF.js.map +1 -0
  100. package/lib/index.esm-Cp4wkyud.js +1236 -0
  101. package/lib/index.esm-Cp4wkyud.js.map +1 -0
  102. package/lib/{mutation-oxMvODNQ.js → mutation-BSeQ8pEK.js} +2 -2
  103. package/lib/{mutation-oxMvODNQ.js.map → mutation-BSeQ8pEK.js.map} +1 -1
  104. package/lib/react-nprogress.esm-C2MPXjiJ.js +389 -0
  105. package/lib/react-nprogress.esm-C2MPXjiJ.js.map +1 -0
  106. package/lib/ui/CodeBlock.js +17 -16
  107. package/lib/ui/CodeBlock.js.map +1 -1
  108. package/lib/ui/Command.js +1 -1
  109. package/lib/ui/Dialog.js +25 -25
  110. package/lib/ui/Dialog.js.map +1 -1
  111. package/lib/ui/Form.js +1 -1
  112. package/lib/ui/SyntaxHighlight.js +2 -2
  113. package/lib/{useExposedProps-DG8J6ewJ.js → useExposedProps-BZQkZneR.js} +2 -2
  114. package/lib/{useExposedProps-DG8J6ewJ.js.map → useExposedProps-BZQkZneR.js.map} +1 -1
  115. package/lib/{useMutation-C_j3dA_L.js → useMutation-CZSmsIGW.js} +3 -3
  116. package/lib/{useMutation-C_j3dA_L.js.map → useMutation-CZSmsIGW.js.map} +1 -1
  117. package/lib/zudoku.auth-auth0.js +1 -1
  118. package/lib/zudoku.auth-clerk.js +2 -2
  119. package/lib/zudoku.auth-openid.js +57 -66
  120. package/lib/zudoku.auth-openid.js.map +1 -1
  121. package/lib/zudoku.components.js +1698 -2082
  122. package/lib/zudoku.components.js.map +1 -1
  123. package/lib/zudoku.hooks.js +11 -11
  124. package/lib/zudoku.plugin-api-catalog.js +5 -5
  125. package/lib/zudoku.plugin-api-keys.js +473 -4970
  126. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  127. package/lib/zudoku.plugin-custom-pages.js +2 -2
  128. package/lib/zudoku.plugin-markdown.js +1 -1
  129. package/lib/zudoku.plugin-openapi.js +3 -3
  130. package/lib/zudoku.plugin-redirect.js +1 -1
  131. package/lib/zudoku.plugin-search-pagefind.js +5 -5
  132. package/package.json +33 -34
  133. package/src/lib/authentication/AuthenticationPlugin.tsx +0 -14
  134. package/src/lib/plugins/api-keys/SettingsApiKeys.tsx +193 -48
  135. package/src/lib/plugins/api-keys/index.tsx +25 -18
  136. package/src/lib/plugins/openapi/Sidecar.tsx +11 -97
  137. package/src/lib/plugins/openapi/playground/BodyPanel.tsx +46 -0
  138. package/src/lib/plugins/openapi/playground/ExamplesDropdown.tsx +4 -1
  139. package/src/lib/plugins/openapi/playground/Headers.tsx +110 -106
  140. package/src/lib/plugins/openapi/playground/IdentitySelector.tsx +13 -11
  141. package/src/lib/plugins/openapi/playground/ParamsGrid.tsx +2 -2
  142. package/src/lib/plugins/openapi/playground/PathParams.tsx +1 -1
  143. package/src/lib/plugins/openapi/playground/Playground.tsx +127 -211
  144. package/src/lib/plugins/openapi/playground/PlaygroundDialog.tsx +2 -1
  145. package/src/lib/plugins/openapi/playground/QueryParams.tsx +1 -1
  146. package/src/lib/plugins/openapi/playground/fileUtils.ts +32 -0
  147. package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +74 -39
  148. package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +4 -1
  149. package/src/lib/plugins/openapi/playground/useRememberSkipLoginDialog.tsx +20 -0
  150. package/src/lib/plugins/openapi/util/createHttpSnippet.ts +107 -0
  151. package/src/lib/shiki.ts +21 -22
  152. package/src/lib/ui/CodeBlock.tsx +1 -0
  153. package/src/lib/ui/Dialog.tsx +11 -7
  154. package/src/lib/util/humanFileSize.test.ts +24 -0
  155. package/src/lib/util/humanFileSize.ts +16 -0
  156. package/dist/lib/plugins/openapi/playground/SubmitButton.d.ts +0 -7
  157. package/dist/lib/plugins/openapi/playground/SubmitButton.js +0 -19
  158. package/dist/lib/plugins/openapi/playground/SubmitButton.js.map +0 -1
  159. package/lib/Dialog-Du6WMcIA.js.map +0 -1
  160. package/lib/OperationList-BmoMLQPO.js.map +0 -1
  161. package/lib/SyntaxHighlight-DedRjJNr.js.map +0 -1
  162. package/lib/chunk-BAXFHI7N-C9WnHsLV.js.map +0 -1
  163. package/lib/circular-oB4auIIg.js.map +0 -1
  164. package/lib/createServer-DCB82j2t.js.map +0 -1
  165. package/lib/index-BXYvD5-7.js.map +0 -1
  166. package/lib/index.esm-DSfX_eMP.js +0 -1216
  167. package/lib/index.esm-DSfX_eMP.js.map +0 -1
  168. package/src/lib/plugins/openapi/playground/SubmitButton.tsx +0 -70
@@ -1,10 +1,8 @@
1
+ import { useNProgress } from "@tanem/react-nprogress";
1
2
  import { useMutation } from "@tanstack/react-query";
2
- import { InfoIcon } from "lucide-react";
3
3
  import { Fragment, useEffect, useRef, useState, useTransition } from "react";
4
4
  import { FormProvider, useForm } from "react-hook-form";
5
- import { Alert, AlertDescription, AlertTitle } from "zudoku/ui/Alert.js";
6
- import { PathRenderer } from "../../../components/PathRenderer.js";
7
-
5
+ import { Button } from "zudoku/ui/Button.js";
8
6
  import {
9
7
  Select,
10
8
  SelectContent,
@@ -12,17 +10,15 @@ import {
12
10
  SelectTrigger,
13
11
  SelectValue,
14
12
  } from "zudoku/ui/Select.js";
15
- import { Textarea } from "zudoku/ui/Textarea.js";
16
13
  import { useApiIdentities } from "../../../components/context/ZudokuContext.js";
17
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/Tabs.js";
18
- import { cn } from "../../../util/cn.js";
19
- import { objectEntries } from "../../../util/objectEntries.js";
14
+ import { PathRenderer } from "../../../components/PathRenderer.js";
20
15
  import { useLatest } from "../../../util/useLatest.js";
21
16
  import { ColorizedParam } from "../ColorizedParam.js";
22
17
  import { type Content } from "../SidecarExamples.js";
23
18
  import { useSelectedServer } from "../state.js";
19
+ import BodyPanel from "./BodyPanel.js";
24
20
  import { createUrl } from "./createUrl.js";
25
- import ExamplesDropdown from "./ExamplesDropdown.js";
21
+ import { extractFileName, isBinaryContentType } from "./fileUtils.js";
26
22
  import { Headers } from "./Headers.js";
27
23
  import { IdentityDialog } from "./IdentityDialog.js";
28
24
  import IdentitySelector from "./IdentitySelector.js";
@@ -31,7 +27,7 @@ import { QueryParams } from "./QueryParams.js";
31
27
  import { useIdentityStore } from "./rememberedIdentity.js";
32
28
  import RequestLoginDialog from "./RequestLoginDialog.js";
33
29
  import { ResultPanel } from "./result-panel/ResultPanel.js";
34
- import SubmitButton from "./SubmitButton.js";
30
+ import { useRememberSkipLoginDialog } from "./useRememberSkipLoginDialog.js";
35
31
 
36
32
  export const NO_IDENTITY = "__none";
37
33
 
@@ -59,17 +55,8 @@ export type PathParam = {
59
55
  isRequired?: boolean;
60
56
  };
61
57
 
62
- const bodyContentTypeMap = {
63
- Plain: "text/plain",
64
- JSON: "application/json",
65
- XML: "application/xml",
66
- YAML: "application/yaml",
67
- CSV: "text/csv",
68
- } as const;
69
-
70
58
  export type PlaygroundForm = {
71
59
  body: string;
72
- bodyContentType: keyof typeof bodyContentTypeMap;
73
60
  queryParams: Array<{
74
61
  name: string;
75
62
  value: string;
@@ -95,6 +82,9 @@ export type PlaygroundResult = {
95
82
  size: number;
96
83
  body: string;
97
84
  time: number;
85
+ isBinary?: boolean;
86
+ fileName?: string;
87
+ blob?: Blob;
98
88
  request: {
99
89
  method: string;
100
90
  url: string;
@@ -139,7 +129,7 @@ export const Playground = ({
139
129
  const identities = useApiIdentities();
140
130
  const { setRememberedIdentity, getRememberedIdentity } = useIdentityStore();
141
131
  const [, startTransition] = useTransition();
142
- const [skipLogin, setSkipLogin] = useState(false);
132
+ const { skipLogin, setSkipLogin } = useRememberSkipLoginDialog();
143
133
  const [showLongRunningWarning, setShowLongRunningWarning] = useState(false);
144
134
  const abortControllerRef = useRef<AbortController | undefined>(undefined);
145
135
  const latestSetRememberedIdentity = useLatest(setRememberedIdentity);
@@ -148,46 +138,47 @@ export const Playground = ({
148
138
  useForm<PlaygroundForm>({
149
139
  defaultValues: {
150
140
  body: defaultBody,
151
- bodyContentType: "JSON",
152
- queryParams: queryParams
153
- .map((param) => ({
154
- name: param.name,
155
- value: param.defaultValue ?? "",
156
- active: param.defaultActive ?? false,
157
- enum: param.enum ?? [],
158
- }))
159
- .concat([
160
- {
161
- name: "",
162
- value: "",
163
- active: false,
164
- enum: [],
165
- },
166
- ]),
141
+ queryParams:
142
+ queryParams.length > 0
143
+ ? queryParams.map((param) => ({
144
+ name: param.name,
145
+ value: param.defaultValue ?? "",
146
+ active: param.defaultActive ?? false,
147
+ enum: param.enum ?? [],
148
+ }))
149
+ : [
150
+ {
151
+ name: "",
152
+ value: "",
153
+ active: false,
154
+ enum: [],
155
+ },
156
+ ],
167
157
  pathParams: pathParams.map((param) => ({
168
158
  name: param.name,
169
159
  value: param.defaultValue ?? "",
170
160
  })),
171
- headers: headers
172
- .map((header) => ({
173
- name: header.name,
174
- value: header.defaultValue ?? "",
175
- active: header.defaultActive ?? false,
176
- }))
177
- .concat([
178
- {
179
- name: "",
180
- value: "",
181
- active: false,
182
- },
183
- ]),
184
- identity: getRememberedIdentity(
185
- identities.data?.map((i) => i.id) ?? [],
186
- ),
161
+ headers:
162
+ headers.length > 0
163
+ ? headers.map((header) => ({
164
+ name: header.name,
165
+ value: header.defaultValue ?? "",
166
+ active: header.defaultActive ?? false,
167
+ }))
168
+ : [
169
+ {
170
+ name: "",
171
+ value: "",
172
+ active: false,
173
+ },
174
+ ],
175
+ identity: getRememberedIdentity([
176
+ NO_IDENTITY,
177
+ ...(identities.data?.map((i) => i.id) ?? []),
178
+ ]),
187
179
  },
188
180
  });
189
181
  const formState = watch();
190
- const formRef = useRef<HTMLFormElement>(null);
191
182
 
192
183
  useEffect(() => {
193
184
  if (formState.identity) {
@@ -200,17 +191,10 @@ export const Playground = ({
200
191
  mutationFn: async (data: PlaygroundForm) => {
201
192
  const start = performance.now();
202
193
 
203
- const shouldSetContentType = !data.headers.some(
204
- (h) => h.active && h.name.toLowerCase() === "content-type",
205
- );
206
-
207
194
  const headers = Object.fromEntries([
208
195
  ...data.headers
209
196
  .filter((h) => h.name && h.active)
210
197
  .map((header) => [header.name, header.value]),
211
- ...(shouldSetContentType
212
- ? [["content-type", bodyContentTypeMap[data.bodyContentType]]]
213
- : []),
214
198
  ]);
215
199
 
216
200
  const request = new Request(
@@ -244,15 +228,34 @@ export const Playground = ({
244
228
  setShowLongRunningWarning(false);
245
229
 
246
230
  const time = performance.now() - start;
247
- const body = await response.text();
248
231
  const url = new URL(request.url);
232
+ const responseHeaders = Array.from(response.headers.entries());
233
+ const contentType = response.headers.get("content-type") || "";
234
+ const isBinary = isBinaryContentType(contentType);
235
+
236
+ let body = "";
237
+ let blob: Blob | undefined;
238
+ let fileName: string | undefined;
239
+
240
+ if (isBinary) {
241
+ blob = await response.blob();
242
+ fileName = extractFileName(responseHeaders, request.url);
243
+ body = `Binary content (${contentType})`;
244
+ } else {
245
+ body = await response.text();
246
+ }
247
+
248
+ const responseSize = response.headers.get("content-length");
249
249
 
250
250
  return {
251
251
  status: response.status,
252
- headers: Array.from(response.headers.entries()),
253
- size: body.length,
252
+ headers: responseHeaders,
253
+ size: responseSize ? parseInt(responseSize) : body.length,
254
254
  body,
255
255
  time,
256
+ isBinary,
257
+ fileName,
258
+ blob,
256
259
  request: {
257
260
  method: request.method.toUpperCase(),
258
261
  url: request.url,
@@ -278,6 +281,16 @@ export const Playground = ({
278
281
  },
279
282
  });
280
283
 
284
+ const isRequestAnimating = queryMutation.isPending;
285
+ const [isAnimating, setIsAnimating] = useState(false);
286
+
287
+ useEffect(() => {
288
+ const timer = setTimeout(() => setIsAnimating(isRequestAnimating), 100);
289
+ return () => clearTimeout(timer);
290
+ }, [isRequestAnimating]);
291
+
292
+ const { isFinished, progress } = useNProgress({ isAnimating });
293
+
281
294
  useEffect(() => {
282
295
  return () => {
283
296
  abortControllerRef.current?.abort();
@@ -362,7 +375,6 @@ export const Playground = ({
362
375
  setShowSelectIdentity(true);
363
376
  }
364
377
  })}
365
- ref={formRef}
366
378
  className="relative"
367
379
  >
368
380
  <IdentityDialog
@@ -384,164 +396,68 @@ export const Playground = ({
384
396
  onLogin={onLogin}
385
397
  />
386
398
 
387
- <div className="grid grid-cols-2 text-sm h-full">
388
- <div className="flex flex-col gap-4 p-4 after:bg-muted-foreground/20 relative after:absolute after:w-px after:inset-0 after:start-auto">
399
+ <div className="grid grid-cols-[1fr_min-content_1fr] text-sm">
400
+ <div className="col-span-3 p-4 border-b">
389
401
  <div className="flex gap-2 items-stretch">
390
- <div className="flex flex-1 items-center w-full border rounded-md">
402
+ <div className="flex flex-1 items-center w-full border rounded-md relative overflow-hidden">
391
403
  <div className="border-r p-2 bg-muted rounded-l-md self-stretch font-semibold font-mono flex items-center">
392
404
  {method.toUpperCase()}
393
405
  </div>
394
- <div className="items-center px-2 py-0.5 font-mono text-xs break-all leading-6">
395
- {serverSelect}
396
- {path}
397
- {urlQueryParams.length > 0 ? "?" : ""}
398
- {urlQueryParams}
406
+ <div className="items-center px-2 font-mono text-xs break-all leading-6 relative h-full w-full">
407
+ <div className="h-full py-1.5">
408
+ {serverSelect}
409
+ {path}
410
+ {urlQueryParams.length > 0 ? "?" : ""}
411
+ {urlQueryParams}
412
+ </div>
413
+ <div
414
+ className="h-[1px] bg-primary absolute left-0 -bottom-0 z-10 transition-all duration-300 ease-in-out"
415
+ style={{
416
+ opacity: isFinished ? 0 : 1,
417
+ width: isFinished ? 0 : `${progress * 100}%`,
418
+ }}
419
+ />
399
420
  </div>
400
421
  </div>
401
422
 
402
- <SubmitButton
403
- identities={identities.data ?? []}
404
- formRef={formRef}
423
+ <Button
424
+ type="submit"
405
425
  disabled={identities.isLoading || form.formState.isSubmitting}
406
- />
426
+ >
427
+ Send
428
+ </Button>
407
429
  </div>
408
- <Tabs defaultValue="parameters">
409
- <div className="flex flex-wrap gap-1 justify-between">
410
- <TabsList>
411
- <TabsTrigger value="parameters">
412
- Parameters
413
- {(formState.pathParams.some((p) => p.value !== "") ||
414
- formState.queryParams.some((p) => p.active)) && (
415
- <div className="w-2 h-2 rounded-full bg-blue-400 ms-2" />
416
- )}
417
- </TabsTrigger>
418
- <TabsTrigger value="headers">
419
- Headers
420
- {formState.headers.filter((h) => h.active).length > 0 && (
421
- <div className="w-2 h-2 rounded-full bg-blue-400 ms-2" />
422
- )}
423
- </TabsTrigger>
424
- <TabsTrigger value="auth">
425
- Auth
426
- {formState.identity !== NO_IDENTITY && (
427
- <div className="w-2 h-2 rounded-full bg-blue-400 ms-2" />
428
- )}
429
- </TabsTrigger>
430
- <TabsTrigger value="body">
431
- Body
432
- {formState.body && (
433
- <div className="w-2 h-2 rounded-full bg-blue-400 ms-2" />
434
- )}
435
- </TabsTrigger>
436
- </TabsList>
437
- </div>
438
- <TabsContent value="headers">
439
- <Headers control={control} headers={headers} />
440
- </TabsContent>
441
- <TabsContent value="parameters">
442
- {pathParams.length > 0 && (
443
- <div className="flex flex-col gap-4 my-4">
444
- <span className="font-semibold">Path Parameters</span>
445
- <PathParams url={url} control={control} />
446
- </div>
447
- )}
448
- <div className="flex flex-col gap-4 my-4">
449
- <span className="font-semibold">Query Parameters</span>
450
- <QueryParams control={control} queryParams={queryParams} />
451
- </div>
452
- </TabsContent>
453
- <TabsContent value="body">
454
- {!["POST", "PUT", "PATCH", "DELETE"].includes(
455
- method.toUpperCase(),
456
- ) && (
457
- <Alert className="mb-2">
458
- <InfoIcon className="w-4 h-4" />
459
- <AlertTitle>Body</AlertTitle>
460
- <AlertDescription>
461
- Body is only supported for POST, PUT, PATCH, and DELETE
462
- requests
463
- </AlertDescription>
464
- </Alert>
465
- )}
466
- <Textarea
467
- {...register("body")}
468
- className={cn(
469
- "border w-full rounded-lg bg-muted/40 p-2 h-64 font-mono text-[13px]",
470
- !isBodySupported && "h-20 bg-muted",
471
- )}
472
- placeholder={
473
- !isBodySupported
474
- ? "This request does not support a body"
475
- : undefined
476
- }
477
- disabled={!isBodySupported}
478
- />
479
- {isBodySupported && (
480
- <div className="flex items-center gap-2 mt-2 justify-between">
481
- <Select
482
- value={formState.bodyContentType}
483
- onValueChange={(value) =>
484
- setValue(
485
- "bodyContentType",
486
- value as keyof typeof bodyContentTypeMap,
487
- )
488
- }
489
- >
490
- <SelectTrigger className="w-[100px]">
491
- <SelectValue />
492
- </SelectTrigger>
493
- <SelectContent>
494
- {Object.keys(bodyContentTypeMap).map((format) => (
495
- <SelectItem key={format} value={format}>
496
- {format}
497
- </SelectItem>
498
- ))}
499
- </SelectContent>
500
- </Select>
501
- {examples && examples.length > 0 && (
502
- <ExamplesDropdown
503
- examples={examples}
504
- onSelect={(example, mediaType) => {
505
- setValue(
506
- "body",
507
- JSON.stringify(example.value, null, 2),
508
- );
509
-
510
- const format = objectEntries(bodyContentTypeMap).find(
511
- ([_, contentType]) => contentType === mediaType,
512
- )?.[0];
513
-
514
- if (format) {
515
- setValue("bodyContentType", format);
516
- }
517
- }}
518
- />
519
- )}
520
- </div>
521
- )}
522
- </TabsContent>
523
- <TabsContent value="auth">
524
- <div className="flex flex-col gap-4 my-4">
525
- {identities.data?.length === 0 && (
526
- <Alert>
527
- <InfoIcon className="w-4 h-4" />
528
- <AlertTitle>Authentication</AlertTitle>
529
- <AlertDescription>
530
- No identities found. Please create an identity first.
531
- </AlertDescription>
532
- </Alert>
533
- )}
534
- <div className="flex flex-col items-center gap-2">
535
- <IdentitySelector
536
- value={formState.identity}
537
- identities={identities.data ?? []}
538
- setValue={(value) => setValue("identity", value)}
539
- />
540
- </div>
430
+ </div>
431
+ <div className="flex flex-col gap-5 p-4 after:bg-muted-foreground/20 relative overflow-y-auto h-[80vh]">
432
+ {identities.data?.length !== 0 && (
433
+ <div className="flex flex-col gap-2">
434
+ <div className="flex flex-col gap-2">
435
+ <span className="font-semibold">Authentication</span>
436
+ <IdentitySelector
437
+ value={formState.identity}
438
+ identities={identities.data ?? []}
439
+ setValue={(value) => setValue("identity", value)}
440
+ />
541
441
  </div>
542
- </TabsContent>
543
- </Tabs>
442
+ </div>
443
+ )}
444
+
445
+ {pathParams.length > 0 && (
446
+ <div className="flex flex-col gap-2">
447
+ <span className="font-semibold">Path Parameters</span>
448
+ <PathParams url={url} control={control} />
449
+ </div>
450
+ )}
451
+
452
+ <div className="flex flex-col gap-2">
453
+ <span className="font-semibold">Query Parameters</span>
454
+ <QueryParams control={control} queryParams={queryParams} />
455
+ </div>
456
+
457
+ <Headers control={control} headers={headers} />
458
+ {isBodySupported && <BodyPanel examples={examples} />}
544
459
  </div>
460
+ <div className="w-px bg-muted-foreground/20" />
545
461
  <ResultPanel
546
462
  queryMutation={queryMutation}
547
463
  showPathParamsWarning={formState.pathParams.some(
@@ -50,8 +50,9 @@ const PlaygroundDialog = (props: PlaygroundDialogProps) => {
50
50
  </DialogTrigger>
51
51
 
52
52
  <DialogContent
53
- className="max-w-screen-xl w-full h-5/6 overflow-hidden p-0"
53
+ className="max-w-screen-xl w-full overflow-hidden p-0"
54
54
  aria-describedby={undefined}
55
+ showCloseButton={false}
55
56
  >
56
57
  <VisuallyHidden>
57
58
  <DialogTitle>Playground</DialogTitle>
@@ -28,7 +28,7 @@ export const QueryParams = ({
28
28
  const requiredFields = queryParams.map((param) => Boolean(param.isRequired));
29
29
 
30
30
  return (
31
- <Card className="rounded-lg">
31
+ <Card className="rounded-lg overflow-hidden">
32
32
  <div className="w-full ">
33
33
  <ParamsGrid>
34
34
  {fields.map((field, i) => {
@@ -0,0 +1,32 @@
1
+ export function isBinaryContentType(contentType: string) {
2
+ return /^(application\/octet-stream|image\/|audio\/|video\/|font\/|application\/pdf|application\/zip|application\/x-protobuf|application\/x-binary)/i.test(
3
+ contentType,
4
+ );
5
+ }
6
+
7
+ export const extractFileName = (
8
+ headers: Array<[string, string]>,
9
+ url: string,
10
+ ): string => {
11
+ const contentDisposition = headers.find(
12
+ ([key]) => key.toLowerCase() === "content-disposition",
13
+ )?.[1];
14
+
15
+ if (contentDisposition) {
16
+ const filenameMatch = contentDisposition.match(
17
+ /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/,
18
+ );
19
+ if (filenameMatch && filenameMatch[1]) {
20
+ return filenameMatch[1].replace(/['"]/g, "");
21
+ }
22
+ }
23
+
24
+ // Extract filename from URL as fallback
25
+ try {
26
+ const urlPath = new URL(url).pathname;
27
+ const fileName = urlPath.split("/").pop() || "download";
28
+ return fileName.includes(".") ? fileName : "download";
29
+ } catch {
30
+ return "download";
31
+ }
32
+ };
@@ -1,6 +1,7 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
- import { ChevronRightIcon } from "lucide-react";
2
+ import { ChevronRightIcon, DownloadIcon } from "lucide-react";
3
3
  import { Fragment, 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
  Collapsible,
@@ -16,6 +17,7 @@ import {
16
17
  } from "zudoku/ui/Select.js";
17
18
  import { Card } from "../../../../ui/Card.js";
18
19
  import { SyntaxHighlight } from "../../../../ui/SyntaxHighlight.js";
20
+ import { humanFileSize } from "../../../../util/humanFileSize.js";
19
21
  import { convertToTypes } from "./convertToTypes.js";
20
22
 
21
23
  const statusCodeMap: Record<number, string> = {
@@ -31,14 +33,6 @@ const statusCodeMap: Record<number, string> = {
31
33
  500: "Internal Server Error",
32
34
  };
33
35
 
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
-
42
36
  const mimeTypeToLanguage = (mimeType: string) => {
43
37
  const mimeTypeMapping = {
44
38
  "application/json": "json",
@@ -101,6 +95,9 @@ export const ResponseTab = ({
101
95
  time,
102
96
  size,
103
97
  url,
98
+ isBinary = false,
99
+ fileName,
100
+ blob,
104
101
  }: {
105
102
  body?: string;
106
103
  headers: Array<[string, string]>;
@@ -108,6 +105,9 @@ export const ResponseTab = ({
108
105
  time: number;
109
106
  size: number;
110
107
  url: string;
108
+ isBinary?: boolean;
109
+ fileName?: string;
110
+ blob?: Blob;
111
111
  }) => {
112
112
  const detectedLanguage = detectLanguage(headers);
113
113
  const jsonContent = tryParseJson(body);
@@ -121,9 +121,22 @@ export const ResponseTab = ({
121
121
  queryFn: async () => {
122
122
  return convertToTypes(JSON.parse(beautifiedBody));
123
123
  },
124
- enabled: view === "types",
124
+ enabled: view === "types" && !isBinary,
125
125
  });
126
126
 
127
+ const handleDownload = () => {
128
+ if (blob && fileName) {
129
+ const url = URL.createObjectURL(blob);
130
+ const link = document.createElement("a");
131
+ link.href = url;
132
+ link.download = fileName;
133
+ document.body.appendChild(link);
134
+ link.click();
135
+ document.body.removeChild(link);
136
+ URL.revokeObjectURL(url);
137
+ }
138
+ };
139
+
127
140
  const sortedHeaders = sortHeadersByRelevance([...headers]);
128
141
  const shouldDisableHighlighting = size > SYNTAX_HIGHLIGHT_MAX_SIZE_THRESHOLD;
129
142
 
@@ -163,37 +176,59 @@ export const ResponseTab = ({
163
176
  </Collapsible>
164
177
 
165
178
  <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>
179
+ {isBinary ? (
180
+ <div className="p-4 text-center">
181
+ <div className="flex flex-col items-center gap-4">
182
+ <div className="text-lg font-semibold">Binary Content</div>
183
+ <div className="text-sm text-muted-foreground">
184
+ This response contains binary data that cannot be displayed as
185
+ text.
186
+ </div>
187
+ <Button
188
+ onClick={handleDownload}
189
+ className="flex items-center gap-2"
190
+ disabled={!blob}
191
+ >
192
+ <DownloadIcon className="h-4 w-4" />
193
+ Download {fileName || "file"} ({humanFileSize(size)})
194
+ </Button>
195
+ </div>
196
+ </div>
197
+ ) : (
198
+ <>
199
+ {shouldDisableHighlighting && (
200
+ <Callout type="info" className="my-0 p-2">
201
+ Code highlight is disabled for responses larger than{" "}
202
+ {humanFileSize(SYNTAX_HIGHLIGHT_MAX_SIZE_THRESHOLD)}
203
+ </Callout>
204
+ )}
205
+ <SyntaxHighlight
206
+ language={
207
+ view === "types"
208
+ ? "typescript"
209
+ : view === "raw"
210
+ ? jsonContent
211
+ ? "plain"
212
+ : detectedLanguage
213
+ : "json"
214
+ }
215
+ showCopy="always"
216
+ disabled={shouldDisableHighlighting}
217
+ noBackground
218
+ className="overflow-x-auto p-4 text-xs max-h-[calc(83.333vh-180px)]"
219
+ code={
220
+ (view === "raw"
221
+ ? body
222
+ : view === "types"
223
+ ? types.data?.lines.join("\n")
224
+ : beautifiedBody) ?? ""
225
+ }
226
+ />
227
+ </>
171
228
  )}
172
- <SyntaxHighlight
173
- language={
174
- view === "types"
175
- ? "typescript"
176
- : view === "raw"
177
- ? jsonContent
178
- ? "plain"
179
- : detectedLanguage
180
- : "json"
181
- }
182
- showCopy="always"
183
- disabled={shouldDisableHighlighting}
184
- noBackground
185
- className="overflow-x-auto p-4 text-xs max-h-[calc(83.333vh-180px)]"
186
- code={
187
- (view === "raw"
188
- ? body
189
- : view === "types"
190
- ? types.data?.lines.join("\n")
191
- : beautifiedBody) ?? ""
192
- }
193
- />
194
229
  </Card>
195
230
  <div className="flex gap-2 justify-between items-center">
196
- <div className="flex text-xs gap-2 border bg-muted rounded-md p-2 items-center h-8 font-mono divide-x">
231
+ <div className="flex text-xs gap-5 border bg-muted rounded-md p-2 items-center h-8 font-mono">
197
232
  <div>
198
233
  <span className="text-muted-foreground">Status</span> {status}{" "}
199
234
  {statusCodeMap[status] ?? ""}
@@ -207,7 +242,7 @@ export const ResponseTab = ({
207
242
  {humanFileSize(size)}
208
243
  </div>
209
244
  </div>
210
- {jsonContent && (
245
+ {jsonContent && !isBinary && (
211
246
  <div>
212
247
  <Select
213
248
  value={view}