@superleapai/flow-ui 1.0.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 (40) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +451 -0
  4. package/components/alert.js +282 -0
  5. package/components/avatar.js +195 -0
  6. package/components/badge.js +135 -0
  7. package/components/button.js +201 -0
  8. package/components/checkbox.js +254 -0
  9. package/components/currency.js +227 -0
  10. package/components/date-time-picker/date-time-picker-utils.js +253 -0
  11. package/components/date-time-picker/date-time-picker.js +532 -0
  12. package/components/duration/duration-constants.js +46 -0
  13. package/components/duration/duration-utils.js +164 -0
  14. package/components/duration/duration.js +448 -0
  15. package/components/enum-multiselect.js +869 -0
  16. package/components/enum-select.js +831 -0
  17. package/components/enumeration.js +213 -0
  18. package/components/file-input.js +533 -0
  19. package/components/icon.js +200 -0
  20. package/components/input.js +259 -0
  21. package/components/label.js +111 -0
  22. package/components/multiselect.js +351 -0
  23. package/components/phone-input/phone-input.js +392 -0
  24. package/components/phone-input/phone-utils.js +157 -0
  25. package/components/popover.js +240 -0
  26. package/components/radio-group.js +435 -0
  27. package/components/record-multiselect.js +956 -0
  28. package/components/record-select.js +930 -0
  29. package/components/select.js +544 -0
  30. package/components/spinner.js +136 -0
  31. package/components/table.js +335 -0
  32. package/components/textarea.js +114 -0
  33. package/components/time-picker.js +357 -0
  34. package/components/toast.js +343 -0
  35. package/core/flow.js +1729 -0
  36. package/core/superleapClient.js +146 -0
  37. package/dist/output.css +2 -0
  38. package/index.d.ts +458 -0
  39. package/index.js +253 -0
  40. package/package.json +70 -0
@@ -0,0 +1,533 @@
1
+ /**
2
+ * S3 File Upload Component
3
+ * Uploads files to S3 and stores URLs in FlowUI state as an array.
4
+ * UI aligned with React PublicFileUploadInput / FileUploadLoader / UploadedFilePreviewer.
5
+ */
6
+
7
+ (function (global) {
8
+ "use strict";
9
+
10
+ // Tabler-style inline SVGs (no external deps) – match React IconFile, IconCopy, IconEye, IconX
11
+ var ICONS = {
12
+ file: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>',
13
+ fileImage: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>',
14
+ filePdf: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M9 12h6"/><path d="M9 16h6"/><path d="M9 8h1"/></svg>',
15
+ fileVideo: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2" ry="2"/></svg>',
16
+ fileAudio: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>',
17
+ fileSpreadsheet: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M8 12h8"/><path d="M8 16h8"/><path d="M8 8h8"/></svg>',
18
+ fileText: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>',
19
+ copy: '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>',
20
+ eye: '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>',
21
+ x: '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
22
+ loader: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>',
23
+ };
24
+
25
+ var IMAGE_EXT = /\.(jpe?g|png|gif|webp|bmp|svg|ico|avif)(\?|$)/i;
26
+ var PDF_EXT = /\.(pdf)(\?|$)/i;
27
+ var VIDEO_EXT = /\.(mp4|webm|mov|avi|mkv|m4v|ogv)(\?|$)/i;
28
+ var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac|flac|webm)(\?|$)/i;
29
+ var SPREADSHEET_EXT = /\.(xlsx?|xls|csv|ods)(\?|$)/i;
30
+ var TEXT_DOC_EXT = /\.(docx?|txt|rtf|odt|pptx?|ppt)(\?|$)/i;
31
+
32
+ function getFileTypeIcon(nameOrUrl) {
33
+ var str = (nameOrUrl || "").toLowerCase();
34
+ if (IMAGE_EXT.test(str)) return ICONS.fileImage;
35
+ if (PDF_EXT.test(str)) return ICONS.filePdf;
36
+ if (VIDEO_EXT.test(str)) return ICONS.fileVideo;
37
+ if (AUDIO_EXT.test(str)) return ICONS.fileAudio;
38
+ if (SPREADSHEET_EXT.test(str)) return ICONS.fileSpreadsheet;
39
+ if (TEXT_DOC_EXT.test(str)) return ICONS.fileText;
40
+ return ICONS.file;
41
+ }
42
+
43
+ /**
44
+ * Upload file to S3
45
+ * @param {File} file - File to upload
46
+ * @param {boolean} isPrivate - Whether file should be private
47
+ * @returns {Promise<Object>} Upload response with URLs
48
+ */
49
+ async function uploadFileToS3(file, isPrivate = false) {
50
+ const formData = new FormData();
51
+ formData.append("file", file, file.name);
52
+ formData.append("is_private", String(!!isPrivate));
53
+
54
+ // Get upload URL - can be configured via global.S3_UPLOAD_URL
55
+ const uploadUrl = global.S3_UPLOAD_URL || "/org/file/upload";
56
+ const baseUrl = global.SUPERLEAP_BASE_URL || "https://app.superleap.com/api/v1";
57
+ const fullUrl = uploadUrl.startsWith("http") ? uploadUrl : `${baseUrl}${uploadUrl}`;
58
+
59
+ // Get API key: use FlowUI._getComponent when globals were captured (index.js), else global.superleapClient
60
+ let apiKey = null;
61
+ try {
62
+ const client =
63
+ global.FlowUI && typeof global.FlowUI._getComponent === "function"
64
+ ? global.FlowUI._getComponent("superleapClient")
65
+ : global.superleapClient;
66
+ if (client && typeof client.getSdk === "function") {
67
+ const sdk = client.getSdk();
68
+ apiKey = sdk?.apiKey;
69
+ }
70
+ } catch (e) {
71
+ console.warn("[S3FileUpload] Could not get API key:", e);
72
+ }
73
+
74
+ const headers = {};
75
+ if (apiKey) {
76
+ headers.Authorization = `Bearer ${apiKey}`;
77
+ }
78
+
79
+ const response = await fetch(fullUrl, {
80
+ method: "POST",
81
+ body: formData,
82
+ headers,
83
+ });
84
+
85
+ if (!response.ok) {
86
+ const errorText = await response.text();
87
+ throw new Error(`File upload failed (${response.status}): ${errorText}`);
88
+ }
89
+
90
+ const result = await response.json();
91
+ console.log("[S3FileUpload] Upload response:");
92
+
93
+ const url = result?.data?.url;
94
+
95
+ if (!url) {
96
+ console.error("[S3FileUpload] No URL found in response:", result);
97
+ throw new Error("Upload response did not contain a file URL");
98
+ }
99
+
100
+ return {
101
+ url: url,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Create S3 file upload component
107
+ * @param {Object} config - Configuration object
108
+ * @param {string} config.label - Field label
109
+ * @param {string} config.fieldId - State key for this field (stores array of URLs)
110
+ * @param {boolean} config.multiple - Allow multiple files
111
+ * @param {string} config.accept - Accepted file types (e.g., ".pdf,.jpg,.jpeg")
112
+ * @param {boolean} config.required - Whether field is required
113
+ * @param {string} config.helpText - Optional help text for tooltip
114
+ * @param {boolean} config.isPrivate - Whether files should be private
115
+ * @param {number} config.maxFiles - Maximum number of files (for multiple mode)
116
+ * @param {number} config.maxFileSize - Maximum file size in bytes
117
+ * @returns {HTMLElement} Field element
118
+ */
119
+ function create(config) {
120
+ const {
121
+ label,
122
+ fieldId,
123
+ multiple = true,
124
+ accept = "*",
125
+ required = false,
126
+ helpText = null,
127
+ isPrivate = false,
128
+ maxFiles = null,
129
+ maxFileSize = 10 * 1024 * 1024, // 10MB default
130
+ } = config;
131
+
132
+ if (!global.FlowUI) {
133
+ throw new Error("FlowUI not available");
134
+ }
135
+
136
+ const FlowUI = global.FlowUI;
137
+
138
+ // Create field wrapper
139
+ const field = FlowUI.createFieldWrapper(label, required, helpText);
140
+ field.setAttribute("data-field-id", fieldId);
141
+
142
+ // Container (match React: space-y-4 → gap-16)
143
+ const uploadContainer = document.createElement("div");
144
+ uploadContainer.className = "w-full flex flex-col gap-8";
145
+
146
+ // Upload row: button + status + optional end spinner (match Select/MultiSelect trigger)
147
+ const uploadWrapper = document.createElement("div");
148
+ uploadWrapper.className = "relative flex w-full items-center justify-between rounded-4 border-1/2 border-border-primary bg-fill-quarternary-fill-white px-8 py-4 hover:border-primary-border focus-within:outline-none focus-within:border-1/2 focus-within:border-primary-border";
149
+
150
+ // Left content (button + status) – pointer-events-none so overlay input receives clicks
151
+ const leftContent = document.createElement("div");
152
+ leftContent.className = "pointer-events-none flex min-w-0 flex-1 items-center gap-8 truncate";
153
+
154
+ const useButtonComponent = global.Button && typeof global.Button.create === "function";
155
+ const initialButtonText = multiple ? "Choose files" : "Choose a file";
156
+ const btn = useButtonComponent
157
+ ? global.Button.create({
158
+ variant: "outline",
159
+ size: "small",
160
+ text: initialButtonText,
161
+ className: "shrink-0 truncate",
162
+ })
163
+ : (function () {
164
+ const el = document.createElement("div");
165
+ el.className = "shrink-0 truncate rounded-2 border-1/2 border-border-primary bg-fill-tertiary-fill-light-gray px-8 py-1 text-reg-13 text-typography-primary-text transition-colors duration-150 hover:bg-fill-secondary-fill-gray";
166
+ el.textContent = initialButtonText;
167
+ return el;
168
+ })();
169
+
170
+ // Status text: "No files chosen" (quaternary) or "X file(s) selected"
171
+ const statusText = document.createElement("p");
172
+ statusText.className = "text-reg-13 min-w-0 flex-1 truncate text-typography-quaternary-text";
173
+
174
+ // Hidden file input – overlays row, high z-index so it receives clicks
175
+ const input = document.createElement("input");
176
+ input.type = "file";
177
+ input.className = "absolute inset-0 z-10 w-full cursor-pointer opacity-0";
178
+ input.multiple = multiple;
179
+ if (accept !== "*") {
180
+ input.accept = accept;
181
+ }
182
+
183
+ // End icon slot: spinner when uploading (match Select chevron position)
184
+ const endIconSlot = document.createElement("div");
185
+ endIconSlot.className = "ml-4 flex size-16 items-center justify-center shrink-0 text-typography-quaternary-text";
186
+ endIconSlot.style.display = "none";
187
+
188
+ // Uploaded files: badge list (match UploadedFilePreviewer – flex-wrap gap-2)
189
+ const filesList = document.createElement("div");
190
+ filesList.className = "flex flex-wrap gap-8";
191
+
192
+ // Uploading files: badge list (match FileUploadLoader – flex-wrap gap-2)
193
+ const uploadingList = document.createElement("div");
194
+ uploadingList.className = "flex flex-wrap gap-8";
195
+ uploadingList.style.display = "none";
196
+
197
+ // State
198
+ let uploadedFiles = [];
199
+ let uploadingFiles = [];
200
+
201
+ // Load existing URLs from state
202
+ function loadExistingFiles() {
203
+ const urls = FlowUI.get(fieldId);
204
+ if (Array.isArray(urls) && urls.length > 0) {
205
+ uploadedFiles = urls.map((url) => ({
206
+ url: url,
207
+ name: url.split("/").pop() || "File",
208
+ }));
209
+ renderFilesList();
210
+ updateStatus();
211
+ }
212
+ }
213
+
214
+ function makeCopyBtn(fileUrl) {
215
+ const btn = document.createElement("button");
216
+ btn.type = "button";
217
+ btn.className = "shrink-0 rounded-2 p-4 text-typography-quaternary-text transition-colors hover:text-typography-primary-text focus:outline-none";
218
+ btn.innerHTML = ICONS.copy;
219
+ btn.setAttribute("aria-label", "Copy URL");
220
+ btn.addEventListener("click", function (e) {
221
+ e.stopPropagation();
222
+ navigator.clipboard.writeText(fileUrl).then(
223
+ function () {
224
+ if (global.FlowUI && global.FlowUI.renderAlerts) {
225
+ const ac = document.getElementById("alerts");
226
+ if (ac) global.FlowUI.renderAlerts(ac, ["Link copied"], "success");
227
+ }
228
+ },
229
+ function () {
230
+ if (global.FlowUI && global.FlowUI.renderAlerts) {
231
+ const ac = document.getElementById("alerts");
232
+ if (ac) global.FlowUI.renderAlerts(ac, ["Copy failed"], "error");
233
+ }
234
+ }
235
+ );
236
+ });
237
+ return btn;
238
+ }
239
+
240
+ function makeViewBtn(fileUrl) {
241
+ const btn = document.createElement("button");
242
+ btn.type = "button";
243
+ btn.className = "shrink-0 rounded-2 p-4 text-typography-quaternary-text transition-colors hover:text-typography-primary-text focus:outline-none";
244
+ btn.innerHTML = ICONS.eye;
245
+ btn.setAttribute("aria-label", "View file");
246
+ btn.addEventListener("click", function (e) {
247
+ e.stopPropagation();
248
+ window.open(fileUrl, "_blank", "noopener");
249
+ });
250
+ return btn;
251
+ }
252
+
253
+ function makeRemoveBtn(onRemove) {
254
+ const btn = document.createElement("button");
255
+ btn.type = "button";
256
+ btn.className = "shrink-0 rounded-2 p-4 text-error-text-base transition-colors hover:bg-error-surface focus:outline-none";
257
+ btn.innerHTML = ICONS.x;
258
+ btn.setAttribute("aria-label", "Remove file");
259
+ btn.addEventListener("click", function (e) {
260
+ e.stopPropagation();
261
+ onRemove();
262
+ });
263
+ return btn;
264
+ }
265
+
266
+ // Render uploaded files as badges (uses Badge component when available)
267
+ function renderFilesList() {
268
+ filesList.innerHTML = "";
269
+ var useBadge = global.Badge && typeof global.Badge.create === "function";
270
+
271
+ uploadedFiles.forEach(function (file, index) {
272
+ var nameEl = document.createElement("p");
273
+ nameEl.className = "max-w-[100px] truncate text-reg-10 text-typography-secondary-text";
274
+ nameEl.textContent = file.name;
275
+ nameEl.title = file.url;
276
+
277
+ var actionsWrap = document.createElement("div");
278
+ actionsWrap.className = "flex items-center gap-2 shrink-0";
279
+ actionsWrap.appendChild(makeCopyBtn(file.url));
280
+ actionsWrap.appendChild(makeViewBtn(file.url));
281
+ actionsWrap.appendChild(makeRemoveBtn(function () {
282
+ uploadedFiles.splice(index, 1);
283
+ saveToState();
284
+ renderFilesList();
285
+ updateStatus();
286
+ }));
287
+
288
+ var badge;
289
+ if (useBadge) {
290
+ badge = global.Badge.create({
291
+ size: "small",
292
+ startIcon: getFileTypeIcon(file.name || file.url),
293
+ content: nameEl,
294
+ endIcon: actionsWrap,
295
+ className: "text-typography-primary-text",
296
+ });
297
+ var iconWraps = badge.querySelectorAll(".flex.size-16");
298
+ if (iconWraps.length >= 1) iconWraps[0].classList.add("text-typography-secondary-text");
299
+ } else {
300
+ badge = document.createElement("div");
301
+ badge.className = "inline-flex items-center gap-4 rounded-4 border-1/2 border-border-primary bg-fill-tertiary-fill-light-gray px-8 py-2 text-med-12 text-typography-primary-text";
302
+ var iconWrap = document.createElement("span");
303
+ iconWrap.className = "flex size-16 shrink-0 items-center justify-center text-typography-secondary-text";
304
+ iconWrap.innerHTML = getFileTypeIcon(file.name || file.url);
305
+ badge.appendChild(iconWrap);
306
+ badge.appendChild(nameEl);
307
+ badge.appendChild(actionsWrap);
308
+ }
309
+
310
+ filesList.appendChild(badge);
311
+ });
312
+ }
313
+
314
+ // Render uploading files as badges (uses Badge component when available)
315
+ function renderUploadingList() {
316
+ uploadingList.innerHTML = "";
317
+ var useBadge = global.Badge && typeof global.Badge.create === "function";
318
+
319
+ uploadingFiles.forEach(function (item) {
320
+ var nameEl = document.createElement("p");
321
+ nameEl.className = "max-w-[100px] truncate text-reg-10 text-typography-secondary-text";
322
+ nameEl.textContent = item.name;
323
+
324
+ var badge;
325
+ if (useBadge) {
326
+ badge = global.Badge.create({
327
+ size: "small",
328
+ startIcon: ICONS.loader,
329
+ content: nameEl,
330
+ className: "text-typography-primary-text",
331
+ });
332
+ var iconWraps = badge.querySelectorAll(".flex.size-16");
333
+ if (iconWraps.length >= 1) iconWraps[0].classList.add("text-typography-secondary-text");
334
+ } else {
335
+ badge = document.createElement("div");
336
+ badge.className = "inline-flex items-center gap-4 rounded-4 border-1/2 border-border-primary bg-fill-tertiary-fill-light-gray px-8 py-2 text-med-12";
337
+ var iconWrap = document.createElement("span");
338
+ iconWrap.className = "flex size-16 shrink-0 items-center justify-center text-typography-secondary-text";
339
+ iconWrap.innerHTML = ICONS.loader;
340
+ badge.appendChild(iconWrap);
341
+ badge.appendChild(nameEl);
342
+ }
343
+
344
+ uploadingList.appendChild(badge);
345
+ });
346
+
347
+ uploadingList.style.display = uploadingFiles.length > 0 ? "flex" : "none";
348
+ }
349
+
350
+ // Update button label (match React: "Add more files" / "Replace file" when files chosen)
351
+ function updateButtonLabel() {
352
+ const filesChosen = uploadedFiles.length > 0 || uploadingFiles.length > 0;
353
+ if (multiple) {
354
+ btn.textContent = filesChosen ? "Add more files" : "Choose files";
355
+ } else {
356
+ btn.textContent = filesChosen ? "Replace file" : "Choose a file";
357
+ }
358
+ }
359
+
360
+ // Update status text and end spinner (match React: Skeleton when uploading, else "X file(s) selected")
361
+ function updateStatus() {
362
+ const uploadedCount = uploadedFiles.length;
363
+ const uploadingCount = uploadingFiles.length;
364
+ const filesChosen = uploadedCount > 0 || uploadingCount > 0;
365
+
366
+ if (!filesChosen) {
367
+ statusText.textContent = "No files chosen";
368
+ statusText.className = "text-reg-13 min-w-0 flex-1 truncate text-typography-quaternary-text";
369
+ } else if (uploadingCount > 0) {
370
+ statusText.textContent = "Uploading…";
371
+ statusText.className = "text-reg-13 min-w-0 flex-1 truncate text-typography-quaternary-text";
372
+ } else {
373
+ statusText.textContent = `${uploadedCount} file${uploadedCount !== 1 ? "s" : ""} selected`;
374
+ statusText.className = "text-reg-13 min-w-0 flex-1 truncate text-typography-primary-text";
375
+ }
376
+
377
+ endIconSlot.style.display = uploadingCount > 0 ? "flex" : "none";
378
+ if (uploadingCount > 0 && endIconSlot.children.length === 0) {
379
+ const spinnerWrap = document.createElement("span");
380
+ spinnerWrap.className = "inline-block animate-spin";
381
+ spinnerWrap.innerHTML = ICONS.loader;
382
+ endIconSlot.appendChild(spinnerWrap);
383
+ }
384
+ updateButtonLabel();
385
+ }
386
+
387
+ // Save URLs to FlowUI state
388
+ function saveToState() {
389
+ const urls = uploadedFiles.map((f) => f.url).filter((url) => url);
390
+ const valueToSave = urls.length > 0 ? urls : null;
391
+ FlowUI.set(fieldId, valueToSave);
392
+ console.log(`[S3FileUpload] Saved to ${fieldId}:`, valueToSave);
393
+ }
394
+
395
+ // Validate file
396
+ function validateFile(file) {
397
+ if (maxFileSize && file.size > maxFileSize) {
398
+ return `File "${file.name}" is too large. Maximum size is ${(maxFileSize / 1024 / 1024).toFixed(2)}MB`;
399
+ }
400
+
401
+ if (accept !== "*") {
402
+ const acceptedTypes = accept.split(",").map((t) => t.trim());
403
+ const fileExtension = "." + file.name.split(".").pop().toLowerCase();
404
+ const fileType = file.type;
405
+
406
+ const isAccepted = acceptedTypes.some((type) => {
407
+ if (type.startsWith(".")) {
408
+ return fileExtension === type.toLowerCase();
409
+ }
410
+ if (type.includes("/")) {
411
+ return fileType === type || fileType.startsWith(type.split("/")[0] + "/");
412
+ }
413
+ return false;
414
+ });
415
+
416
+ if (!isAccepted) {
417
+ return `File "${file.name}" type is not supported. Accepted: ${accept}`;
418
+ }
419
+ }
420
+
421
+ return null;
422
+ }
423
+
424
+ // Upload a single file
425
+ async function uploadFile(file) {
426
+ const error = validateFile(file);
427
+ if (error) {
428
+ if (global.FlowUI && global.FlowUI.renderAlerts) {
429
+ const alertsContainer = document.getElementById("alerts");
430
+ if (alertsContainer) {
431
+ global.FlowUI.renderAlerts(alertsContainer, [error], "error");
432
+ }
433
+ }
434
+ return;
435
+ }
436
+
437
+ uploadingFiles.push({ file, name: file.name });
438
+ renderUploadingList();
439
+ updateStatus();
440
+
441
+ try {
442
+ const result = await uploadFileToS3(file, isPrivate);
443
+ const fileUrl = result?.url;
444
+ if (!fileUrl) {
445
+ throw new Error(`No URL returned from upload for file "${file.name}"`);
446
+ }
447
+ console.log(`[S3FileUpload] File uploaded successfully: ${file.name} -> ${fileUrl}`);
448
+
449
+ uploadingFiles = uploadingFiles.filter((f) => f.file !== file);
450
+ uploadedFiles.push({ url: fileUrl, name: file.name });
451
+ saveToState();
452
+ renderFilesList();
453
+ renderUploadingList();
454
+ updateStatus();
455
+ } catch (error) {
456
+ console.error("[S3FileUpload] Upload error:", error);
457
+ uploadingFiles = uploadingFiles.filter((f) => f.file !== file);
458
+ renderUploadingList();
459
+ updateStatus();
460
+ if (global.FlowUI && global.FlowUI.renderAlerts) {
461
+ const alertsContainer = document.getElementById("alerts");
462
+ if (alertsContainer) {
463
+ global.FlowUI.renderAlerts(
464
+ alertsContainer,
465
+ [`Failed to upload "${file.name}": ${error.message}`],
466
+ "error"
467
+ );
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ // Handle file selection
474
+ input.addEventListener("change", async function (e) {
475
+ const files = Array.from(e.target.files || []);
476
+ if (files.length === 0) {return;}
477
+
478
+ // Check max files limit
479
+ if (multiple && maxFiles && uploadedFiles.length + files.length > maxFiles) {
480
+ const message = `You can only upload ${maxFiles} file${maxFiles !== 1 ? "s" : ""}`;
481
+ if (global.FlowUI && global.FlowUI.renderAlerts) {
482
+ const alertsContainer = document.getElementById("alerts");
483
+ if (alertsContainer) {
484
+ global.FlowUI.renderAlerts(alertsContainer, [message], "error");
485
+ }
486
+ }
487
+ input.value = "";
488
+ return;
489
+ }
490
+
491
+ // For single file mode, clear existing and re-render
492
+ if (!multiple) {
493
+ uploadedFiles = [];
494
+ uploadingFiles = [];
495
+ renderFilesList();
496
+ renderUploadingList();
497
+ updateStatus();
498
+ }
499
+
500
+ // Upload all files
501
+ for (const file of files) {
502
+ await uploadFile(file);
503
+ }
504
+
505
+ // Reset input
506
+ input.value = "";
507
+ });
508
+
509
+ // Assemble upload row
510
+ leftContent.appendChild(btn);
511
+ leftContent.appendChild(statusText);
512
+ uploadWrapper.appendChild(leftContent);
513
+ uploadWrapper.appendChild(endIconSlot);
514
+ uploadWrapper.appendChild(input);
515
+ uploadContainer.appendChild(uploadWrapper);
516
+ uploadContainer.appendChild(uploadingList);
517
+ uploadContainer.appendChild(filesList);
518
+ field.appendChild(uploadContainer);
519
+
520
+ // Initialize
521
+ loadExistingFiles();
522
+ updateStatus();
523
+
524
+ return field;
525
+ }
526
+
527
+ // Export API (shadcn name: FileInput)
528
+ global.FileInput = {
529
+ create,
530
+ };
531
+
532
+ console.log("[FileInput] Module loaded successfully");
533
+ })(typeof window !== "undefined" ? window : this);