@lukoweb/apitogo 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/cli/cli.js +37 -147
  2. package/dist/declarations/lib/ui/Command.d.ts +1 -1
  3. package/dist/declarations/lib/util/MdxComponents.d.ts +1 -1
  4. package/package.json +1 -1
  5. package/src/app/main.tsx +150 -150
  6. package/src/config/loader.ts +245 -245
  7. package/src/index.ts +33 -33
  8. package/src/lib/authentication/components/CallbackHandler.tsx +45 -45
  9. package/src/lib/authentication/components/SignIn.tsx +46 -46
  10. package/src/lib/authentication/components/SignUp.tsx +48 -48
  11. package/src/lib/authentication/providers/clerk.tsx +224 -224
  12. package/src/lib/authentication/ui/AuthCard.tsx +7 -7
  13. package/src/lib/authentication/ui/EmailLinkCallbackUi.tsx +129 -125
  14. package/src/lib/authentication/ui/EmailLinkSentUi.tsx +105 -101
  15. package/src/lib/authentication/ui/EmailLinkSignInUi.tsx +100 -96
  16. package/src/lib/authentication/ui/EmailVerificationUi.tsx +128 -124
  17. package/src/lib/authentication/ui/ZudokuAuthUi.tsx +620 -616
  18. package/src/lib/components/AiAssistantMenuItems.tsx +102 -102
  19. package/src/lib/components/Autocomplete.tsx +123 -123
  20. package/src/lib/components/DeveloperHint.tsx +39 -35
  21. package/src/lib/components/Header.tsx +260 -260
  22. package/src/lib/components/Layout.tsx +53 -53
  23. package/src/lib/components/Main.tsx +51 -51
  24. package/src/lib/components/Mermaid.tsx +74 -70
  25. package/src/lib/components/MobileTopNavigation.tsx +276 -276
  26. package/src/lib/components/MultiSelect.tsx +86 -82
  27. package/src/lib/components/TopNavigation.tsx +103 -103
  28. package/src/lib/components/index.ts +26 -26
  29. package/src/lib/components/navigation/NavigationCategory.tsx +157 -157
  30. package/src/lib/components/navigation/NavigationFilterInput.tsx +35 -35
  31. package/src/lib/components/navigation/NavigationItem.tsx +181 -177
  32. package/src/lib/core/RouteGuard.tsx +193 -193
  33. package/src/lib/errors/ErrorMessage.tsx +50 -46
  34. package/src/lib/plugins/api-catalog/Catalog.tsx +80 -80
  35. package/src/lib/plugins/api-keys/CreateApiKey.tsx +128 -124
  36. package/src/lib/plugins/api-keys/CreateApiKeyDialog.tsx +49 -49
  37. package/src/lib/plugins/api-keys/SettingsApiKeys.tsx +72 -72
  38. package/src/lib/plugins/api-keys/settings/ApiKeyItem.tsx +342 -342
  39. package/src/lib/plugins/api-keys/settings/RevealApiKey.tsx +127 -127
  40. package/src/lib/plugins/markdown/MdxPage.tsx +285 -285
  41. package/src/lib/plugins/openapi/ApiHeader.tsx +96 -96
  42. package/src/lib/plugins/openapi/CollapsibleCode.tsx +88 -88
  43. package/src/lib/plugins/openapi/DownloadSchemaButton.tsx +94 -94
  44. package/src/lib/plugins/openapi/GeneratedExampleSidecarBox.tsx +52 -52
  45. package/src/lib/plugins/openapi/OperationListItem.tsx +183 -183
  46. package/src/lib/plugins/openapi/RequestBodySidecarBox.tsx +63 -63
  47. package/src/lib/plugins/openapi/ResponsesSidecarBox.tsx +120 -117
  48. package/src/lib/plugins/openapi/SchemaInfo.tsx +344 -344
  49. package/src/lib/plugins/openapi/SchemaList.tsx +113 -113
  50. package/src/lib/plugins/openapi/Sidecar.tsx +312 -309
  51. package/src/lib/plugins/openapi/SidecarExamples.tsx +177 -174
  52. package/src/lib/plugins/openapi/components/EnumValues.tsx +58 -58
  53. package/src/lib/plugins/openapi/components/ResponseContent.tsx +117 -114
  54. package/src/lib/plugins/openapi/playground/BodyPanel.tsx +274 -271
  55. package/src/lib/plugins/openapi/playground/CollapsibleHeader.tsx +51 -51
  56. package/src/lib/plugins/openapi/playground/ExamplesDropdown.tsx +60 -60
  57. package/src/lib/plugins/openapi/playground/Headers.tsx +188 -181
  58. package/src/lib/plugins/openapi/playground/IdentityDialog.tsx +75 -75
  59. package/src/lib/plugins/openapi/playground/IdentitySelector.tsx +38 -38
  60. package/src/lib/plugins/openapi/playground/ParamsGrid.tsx +45 -45
  61. package/src/lib/plugins/openapi/playground/Playground.tsx +602 -599
  62. package/src/lib/plugins/openapi/playground/PlaygroundDialog.tsx +56 -56
  63. package/src/lib/plugins/openapi/playground/QueryParams.tsx +151 -148
  64. package/src/lib/plugins/openapi/playground/RequestLoginDialog.tsx +70 -70
  65. package/src/lib/plugins/openapi/playground/request-panel/MultipartField.tsx +91 -91
  66. package/src/lib/plugins/openapi/playground/result-panel/AudioPlayer.tsx +50 -50
  67. package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +355 -355
  68. package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +106 -102
  69. package/src/lib/plugins/openapi/schema/SchemaPropertyItem.tsx +189 -184
  70. package/src/lib/plugins/openapi/schema/SchemaView.tsx +299 -299
  71. package/src/lib/plugins/search-pagefind/IndexingDialog.tsx +162 -162
  72. package/src/lib/plugins/search-pagefind/PagefindSearch.tsx +202 -202
  73. package/src/lib/plugins/search-pagefind/ResultList.tsx +122 -118
  74. package/src/lib/ui/ActionButton.tsx +21 -21
  75. package/src/lib/ui/Command.tsx +191 -191
  76. package/src/lib/util/MdxComponents.tsx +150 -150
  77. package/src/vite/config.ts +234 -231
  78. package/src/vite/dev-server.ts +291 -291
  79. package/src/vite/plugin-api-keys.ts +50 -50
  80. package/src/vite/plugin-api.ts +320 -318
  81. package/src/vite/plugin-auth.ts +38 -38
  82. package/src/vite/plugin-component.ts +45 -39
  83. package/src/vite/plugin-config.ts +75 -75
  84. package/src/vite/plugin-custom-pages.ts +36 -36
  85. package/src/vite/plugin-docs.ts +202 -202
  86. package/src/vite/plugin-markdown-export.ts +214 -214
  87. package/src/vite/plugin-mdx.ts +149 -149
  88. package/src/vite/plugin-navigation.ts +106 -106
  89. package/src/vite/plugin-search.ts +47 -47
  90. package/src/vite/plugin.ts +42 -42
  91. package/src/vite/prerender/prerender.ts +233 -233
@@ -1,271 +1,274 @@
1
- import {
2
- ChevronDownIcon,
3
- FileInput,
4
- Grid2x2PlusIcon,
5
- PaperclipIcon,
6
- ScanTextIcon,
7
- XIcon,
8
- } from "lucide-react";
9
- import { useRef, useState } from "react";
10
- import { useFormContext } from "react-hook-form";
11
- import { Button } from "@lukoweb/apitogo/components";
12
- import { Collapsible, CollapsibleContent } from "@lukoweb/apitogo/ui/Collapsible.js";
13
- import {
14
- DropdownMenu,
15
- DropdownMenuContent,
16
- DropdownMenuItem,
17
- DropdownMenuTrigger,
18
- } from "@lukoweb/apitogo/ui/DropdownMenu.js";
19
- import { Textarea } from "@lukoweb/apitogo/ui/Textarea.js";
20
- import { cn } from "../../../util/cn.js";
21
- import { humanFileSize } from "../../../util/humanFileSize.js";
22
- import type { MediaTypeObject } from "../graphql/graphql.js";
23
- import {
24
- CollapsibleHeader,
25
- CollapsibleHeaderTrigger,
26
- } from "./CollapsibleHeader.js";
27
- import ExamplesDropdown from "./ExamplesDropdown.js";
28
- import ParamsGrid from "./ParamsGrid.js";
29
- import type { PlaygroundForm } from "./Playground.js";
30
- import { MultipartField } from "./request-panel/MultipartField.js";
31
- import { useKeyValueFieldManager } from "./request-panel/useKeyValueFieldManager.js";
32
-
33
- export const BodyPanel = ({ content }: { content?: MediaTypeObject[] }) => {
34
- const { register, setValue, watch, control } =
35
- useFormContext<PlaygroundForm>();
36
- const examples = (content ?? []).flatMap((e) => e.examples);
37
- const [headers, file, bodyMode, body, multipartFormFields] = watch([
38
- "headers",
39
- "file",
40
- "bodyMode",
41
- "body",
42
- "multipartFormFields",
43
- ]);
44
- const fileInputRef = useRef<HTMLInputElement>(null);
45
- const [isDragging, setIsDragging] = useState(false);
46
-
47
- const handleFileSelect = (selectedFile: File | null) => {
48
- setValue("file", selectedFile);
49
- if (!selectedFile) return;
50
- setValue(
51
- "headers",
52
- headers.filter(
53
- (h) => h.name.toLowerCase() !== "content-type" || !h.active,
54
- ),
55
- );
56
- };
57
-
58
- const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
59
- const selectedFile = e.target.files?.[0] ?? null;
60
- handleFileSelect(selectedFile);
61
- };
62
-
63
- const handleDragOver = (e: React.DragEvent<HTMLElement>) => {
64
- e.preventDefault();
65
- e.stopPropagation();
66
- setIsDragging(true);
67
- };
68
-
69
- const handleDragLeave = (e: React.DragEvent<HTMLElement>) => {
70
- e.preventDefault();
71
- e.stopPropagation();
72
- setIsDragging(false);
73
- };
74
-
75
- const handleDrop = (e: React.DragEvent<HTMLElement>) => {
76
- e.preventDefault();
77
- e.stopPropagation();
78
- setIsDragging(false);
79
-
80
- const droppedFile = e.dataTransfer.files?.[0] ?? null;
81
- handleFileSelect(droppedFile);
82
- };
83
-
84
- const manager = useKeyValueFieldManager<
85
- PlaygroundForm,
86
- "multipartFormFields"
87
- >({
88
- control,
89
- name: "multipartFormFields",
90
- defaultValue: { name: "", value: "", active: false },
91
- isEmpty: (item) => {
92
- if (item.value instanceof File) return false;
93
- return !item.name && !item.value;
94
- },
95
- });
96
-
97
- return (
98
- <Collapsible defaultOpen>
99
- <CollapsibleHeaderTrigger className="items-center">
100
- <FileInput size={16} />
101
- <CollapsibleHeader className="flex items-center justify-between">
102
- Body
103
- <div className="flex items-center">
104
- <DropdownMenu>
105
- <DropdownMenuTrigger asChild>
106
- <Button
107
- variant="ghost"
108
- size="sm"
109
- className="hover:bg-accent hover:brightness-95 gap-2"
110
- >
111
- {bodyMode === "text" ? (
112
- <>
113
- <ScanTextIcon size={14} />
114
- Text
115
- </>
116
- ) : bodyMode === "file" ? (
117
- <>
118
- <PaperclipIcon size={14} />
119
- File
120
- </>
121
- ) : (
122
- <>
123
- <Grid2x2PlusIcon size={14} />
124
- Multipart
125
- </>
126
- )}
127
- <ChevronDownIcon size={14} />
128
- </Button>
129
- </DropdownMenuTrigger>
130
- <DropdownMenuContent className="min-w-40">
131
- <DropdownMenuItem
132
- onSelect={() => setValue("bodyMode", "text")}
133
- className="gap-2"
134
- >
135
- <ScanTextIcon size={14} />
136
- <span className="flex-1">Text</span>
137
- <span>
138
- {body.length > 0 && (
139
- <div className="w-1.5 h-1.5 bg-primary rounded-full" />
140
- )}
141
- </span>
142
- </DropdownMenuItem>
143
- <DropdownMenuItem
144
- onSelect={() => setValue("bodyMode", "file")}
145
- className="gap-2"
146
- >
147
- <PaperclipIcon size={14} />
148
- <span className="flex-1">File</span>
149
- <span>
150
- {file && (
151
- <div className="w-1.5 h-1.5 bg-primary rounded-full" />
152
- )}
153
- </span>
154
- </DropdownMenuItem>
155
- <DropdownMenuItem
156
- onSelect={() => setValue("bodyMode", "multipart")}
157
- className="gap-2"
158
- >
159
- <Grid2x2PlusIcon size={14} strokeWidth={1.5} />
160
- <span className="flex-1">Multipart</span>
161
- <span>
162
- {multipartFormFields?.some((field) => field.active) && (
163
- <div className="w-1.5 h-1.5 bg-primary rounded-full" />
164
- )}
165
- </span>
166
- </DropdownMenuItem>
167
- </DropdownMenuContent>
168
- </DropdownMenu>
169
- <input
170
- ref={fileInputRef}
171
- type="file"
172
- className="hidden"
173
- onChange={handleFileInputChange}
174
- />
175
- <div className="w-px mx-1 h-5 bg-border" />
176
- {content && examples.length > 0 ? (
177
- <ExamplesDropdown
178
- examples={content}
179
- onSelect={(example, mediaType) => {
180
- setValue("body", JSON.stringify(example.value, null, 2));
181
- setValue("headers", [
182
- ...headers.filter((h) => h.name !== "Content-Type"),
183
- {
184
- name: "Content-Type",
185
- value: mediaType,
186
- active: true,
187
- },
188
- ]);
189
- }}
190
- />
191
- ) : (
192
- <div />
193
- )}
194
- </div>
195
- </CollapsibleHeader>
196
- </CollapsibleHeaderTrigger>
197
- <CollapsibleContent className="CollapsibleContent flex flex-col gap-2">
198
- {bodyMode === "text" && (
199
- <Textarea
200
- {...register("body")}
201
- className={cn(
202
- "w-full px-4 py-2.5 h-64 font-mono md:text-xs border-none rounded-none focus-visible:ring-0 transition-colors",
203
- )}
204
- placeholder="Body content"
205
- />
206
- )}
207
- {bodyMode === "file" && (
208
- <div
209
- role="region"
210
- aria-label="File upload drop zone"
211
- className={cn(
212
- "flex flex-col items-center justify-center gap-4 min-h-[300px]",
213
- )}
214
- onDragOver={handleDragOver}
215
- onDragLeave={handleDragLeave}
216
- onDrop={handleDrop}
217
- >
218
- <button
219
- type="button"
220
- onClick={() => fileInputRef.current?.click()}
221
- className={cn(
222
- "flex items-center justify-center gap-2 rounded-full size-20 p-0 border border-dashed border-muted-foreground/50 hover:bg-accent/75 transition-colors",
223
- (file || isDragging) && "border-solid",
224
- isDragging && "bg-accent border-primary",
225
- )}
226
- >
227
- <PaperclipIcon
228
- className={cn(
229
- "text-muted-foreground",
230
- isDragging && "text-primary",
231
- )}
232
- size={30}
233
- />
234
- </button>
235
- {file ? (
236
- <div className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-md border">
237
- <span className="text-sm truncate" title={file.name}>
238
- {file.name}{" "}
239
- <span className="text-muted-foreground">
240
- ({humanFileSize(file.size)})
241
- </span>
242
- </span>
243
- <Button
244
- type="button"
245
- variant="ghost"
246
- size="icon-xs"
247
- onClick={() => handleFileSelect(null)}
248
- >
249
- <XIcon size={14} />
250
- </Button>
251
- </div>
252
- ) : (
253
- <span className="text-lg font-semibold text-muted-foreground">
254
- Select or drop a file
255
- </span>
256
- )}
257
- </div>
258
- )}
259
- {bodyMode === "multipart" && (
260
- <ParamsGrid>
261
- {manager.fields.map((field, index) => (
262
- <MultipartField key={field.id} index={index} manager={manager} />
263
- ))}
264
- </ParamsGrid>
265
- )}
266
- </CollapsibleContent>
267
- </Collapsible>
268
- );
269
- };
270
-
271
- export default BodyPanel;
1
+ import { Button } from "@lukoweb/apitogo/components";
2
+ import {
3
+ Collapsible,
4
+ CollapsibleContent,
5
+ } from "@lukoweb/apitogo/ui/Collapsible.js";
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ } from "@lukoweb/apitogo/ui/DropdownMenu.js";
12
+ import { Textarea } from "@lukoweb/apitogo/ui/Textarea.js";
13
+ import {
14
+ ChevronDownIcon,
15
+ FileInput,
16
+ Grid2x2PlusIcon,
17
+ PaperclipIcon,
18
+ ScanTextIcon,
19
+ XIcon,
20
+ } from "lucide-react";
21
+ import { useRef, useState } from "react";
22
+ import { useFormContext } from "react-hook-form";
23
+ import { cn } from "../../../util/cn.js";
24
+ import { humanFileSize } from "../../../util/humanFileSize.js";
25
+ import type { MediaTypeObject } from "../graphql/graphql.js";
26
+ import {
27
+ CollapsibleHeader,
28
+ CollapsibleHeaderTrigger,
29
+ } from "./CollapsibleHeader.js";
30
+ import ExamplesDropdown from "./ExamplesDropdown.js";
31
+ import ParamsGrid from "./ParamsGrid.js";
32
+ import type { PlaygroundForm } from "./Playground.js";
33
+ import { MultipartField } from "./request-panel/MultipartField.js";
34
+ import { useKeyValueFieldManager } from "./request-panel/useKeyValueFieldManager.js";
35
+
36
+ export const BodyPanel = ({ content }: { content?: MediaTypeObject[] }) => {
37
+ const { register, setValue, watch, control } =
38
+ useFormContext<PlaygroundForm>();
39
+ const examples = (content ?? []).flatMap((e) => e.examples);
40
+ const [headers, file, bodyMode, body, multipartFormFields] = watch([
41
+ "headers",
42
+ "file",
43
+ "bodyMode",
44
+ "body",
45
+ "multipartFormFields",
46
+ ]);
47
+ const fileInputRef = useRef<HTMLInputElement>(null);
48
+ const [isDragging, setIsDragging] = useState(false);
49
+
50
+ const handleFileSelect = (selectedFile: File | null) => {
51
+ setValue("file", selectedFile);
52
+ if (!selectedFile) return;
53
+ setValue(
54
+ "headers",
55
+ headers.filter(
56
+ (h) => h.name.toLowerCase() !== "content-type" || !h.active,
57
+ ),
58
+ );
59
+ };
60
+
61
+ const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
62
+ const selectedFile = e.target.files?.[0] ?? null;
63
+ handleFileSelect(selectedFile);
64
+ };
65
+
66
+ const handleDragOver = (e: React.DragEvent<HTMLElement>) => {
67
+ e.preventDefault();
68
+ e.stopPropagation();
69
+ setIsDragging(true);
70
+ };
71
+
72
+ const handleDragLeave = (e: React.DragEvent<HTMLElement>) => {
73
+ e.preventDefault();
74
+ e.stopPropagation();
75
+ setIsDragging(false);
76
+ };
77
+
78
+ const handleDrop = (e: React.DragEvent<HTMLElement>) => {
79
+ e.preventDefault();
80
+ e.stopPropagation();
81
+ setIsDragging(false);
82
+
83
+ const droppedFile = e.dataTransfer.files?.[0] ?? null;
84
+ handleFileSelect(droppedFile);
85
+ };
86
+
87
+ const manager = useKeyValueFieldManager<
88
+ PlaygroundForm,
89
+ "multipartFormFields"
90
+ >({
91
+ control,
92
+ name: "multipartFormFields",
93
+ defaultValue: { name: "", value: "", active: false },
94
+ isEmpty: (item) => {
95
+ if (item.value instanceof File) return false;
96
+ return !item.name && !item.value;
97
+ },
98
+ });
99
+
100
+ return (
101
+ <Collapsible defaultOpen>
102
+ <CollapsibleHeaderTrigger className="items-center">
103
+ <FileInput size={16} />
104
+ <CollapsibleHeader className="flex items-center justify-between">
105
+ Body
106
+ <div className="flex items-center">
107
+ <DropdownMenu>
108
+ <DropdownMenuTrigger asChild>
109
+ <Button
110
+ variant="ghost"
111
+ size="sm"
112
+ className="hover:bg-accent hover:brightness-95 gap-2"
113
+ >
114
+ {bodyMode === "text" ? (
115
+ <>
116
+ <ScanTextIcon size={14} />
117
+ Text
118
+ </>
119
+ ) : bodyMode === "file" ? (
120
+ <>
121
+ <PaperclipIcon size={14} />
122
+ File
123
+ </>
124
+ ) : (
125
+ <>
126
+ <Grid2x2PlusIcon size={14} />
127
+ Multipart
128
+ </>
129
+ )}
130
+ <ChevronDownIcon size={14} />
131
+ </Button>
132
+ </DropdownMenuTrigger>
133
+ <DropdownMenuContent className="min-w-40">
134
+ <DropdownMenuItem
135
+ onSelect={() => setValue("bodyMode", "text")}
136
+ className="gap-2"
137
+ >
138
+ <ScanTextIcon size={14} />
139
+ <span className="flex-1">Text</span>
140
+ <span>
141
+ {body.length > 0 && (
142
+ <div className="w-1.5 h-1.5 bg-primary rounded-full" />
143
+ )}
144
+ </span>
145
+ </DropdownMenuItem>
146
+ <DropdownMenuItem
147
+ onSelect={() => setValue("bodyMode", "file")}
148
+ className="gap-2"
149
+ >
150
+ <PaperclipIcon size={14} />
151
+ <span className="flex-1">File</span>
152
+ <span>
153
+ {file && (
154
+ <div className="w-1.5 h-1.5 bg-primary rounded-full" />
155
+ )}
156
+ </span>
157
+ </DropdownMenuItem>
158
+ <DropdownMenuItem
159
+ onSelect={() => setValue("bodyMode", "multipart")}
160
+ className="gap-2"
161
+ >
162
+ <Grid2x2PlusIcon size={14} strokeWidth={1.5} />
163
+ <span className="flex-1">Multipart</span>
164
+ <span>
165
+ {multipartFormFields?.some((field) => field.active) && (
166
+ <div className="w-1.5 h-1.5 bg-primary rounded-full" />
167
+ )}
168
+ </span>
169
+ </DropdownMenuItem>
170
+ </DropdownMenuContent>
171
+ </DropdownMenu>
172
+ <input
173
+ ref={fileInputRef}
174
+ type="file"
175
+ className="hidden"
176
+ onChange={handleFileInputChange}
177
+ />
178
+ <div className="w-px mx-1 h-5 bg-border" />
179
+ {content && examples.length > 0 ? (
180
+ <ExamplesDropdown
181
+ examples={content}
182
+ onSelect={(example, mediaType) => {
183
+ setValue("body", JSON.stringify(example.value, null, 2));
184
+ setValue("headers", [
185
+ ...headers.filter((h) => h.name !== "Content-Type"),
186
+ {
187
+ name: "Content-Type",
188
+ value: mediaType,
189
+ active: true,
190
+ },
191
+ ]);
192
+ }}
193
+ />
194
+ ) : (
195
+ <div />
196
+ )}
197
+ </div>
198
+ </CollapsibleHeader>
199
+ </CollapsibleHeaderTrigger>
200
+ <CollapsibleContent className="CollapsibleContent flex flex-col gap-2">
201
+ {bodyMode === "text" && (
202
+ <Textarea
203
+ {...register("body")}
204
+ className={cn(
205
+ "w-full px-4 py-2.5 h-64 font-mono md:text-xs border-none rounded-none focus-visible:ring-0 transition-colors",
206
+ )}
207
+ placeholder="Body content"
208
+ />
209
+ )}
210
+ {bodyMode === "file" && (
211
+ <div
212
+ role="region"
213
+ aria-label="File upload drop zone"
214
+ className={cn(
215
+ "flex flex-col items-center justify-center gap-4 min-h-[300px]",
216
+ )}
217
+ onDragOver={handleDragOver}
218
+ onDragLeave={handleDragLeave}
219
+ onDrop={handleDrop}
220
+ >
221
+ <button
222
+ type="button"
223
+ onClick={() => fileInputRef.current?.click()}
224
+ className={cn(
225
+ "flex items-center justify-center gap-2 rounded-full size-20 p-0 border border-dashed border-muted-foreground/50 hover:bg-accent/75 transition-colors",
226
+ (file || isDragging) && "border-solid",
227
+ isDragging && "bg-accent border-primary",
228
+ )}
229
+ >
230
+ <PaperclipIcon
231
+ className={cn(
232
+ "text-muted-foreground",
233
+ isDragging && "text-primary",
234
+ )}
235
+ size={30}
236
+ />
237
+ </button>
238
+ {file ? (
239
+ <div className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-md border">
240
+ <span className="text-sm truncate" title={file.name}>
241
+ {file.name}{" "}
242
+ <span className="text-muted-foreground">
243
+ ({humanFileSize(file.size)})
244
+ </span>
245
+ </span>
246
+ <Button
247
+ type="button"
248
+ variant="ghost"
249
+ size="icon-xs"
250
+ onClick={() => handleFileSelect(null)}
251
+ >
252
+ <XIcon size={14} />
253
+ </Button>
254
+ </div>
255
+ ) : (
256
+ <span className="text-lg font-semibold text-muted-foreground">
257
+ Select or drop a file
258
+ </span>
259
+ )}
260
+ </div>
261
+ )}
262
+ {bodyMode === "multipart" && (
263
+ <ParamsGrid>
264
+ {manager.fields.map((field, index) => (
265
+ <MultipartField key={field.id} index={index} manager={manager} />
266
+ ))}
267
+ </ParamsGrid>
268
+ )}
269
+ </CollapsibleContent>
270
+ </Collapsible>
271
+ );
272
+ };
273
+
274
+ export default BodyPanel;