@eslamdevui/ui 3.3.2-beta.1 → 3.3.2-beta.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.
@@ -50,7 +50,13 @@ export default {
50
50
  "fileWrapper": "flex flex-col min-w-0",
51
51
  "fileName": "text-default truncate",
52
52
  "fileSize": "text-muted truncate",
53
- "fileTrailingButton": ""
53
+ "fileProgress": "flex items-center gap-2 mt-1",
54
+ "fileProgressText": "text-xs text-muted whitespace-nowrap",
55
+ "fileError": "text-xs text-red-500 mt-1",
56
+ "fileActions": "flex items-center gap-1",
57
+ "fileTrailingButton": "",
58
+ "fileUploadButton": "",
59
+ "fileCancelButton": ""
54
60
  },
55
61
  "variants": {
56
62
  "color": {
@@ -104,12 +110,15 @@ export default {
104
110
  "root": "gap-2 items-start",
105
111
  "files": "flex flex-col w-full gap-2",
106
112
  "file": "min-w-0 flex items-center border border-default rounded-md w-full",
107
- "fileTrailingButton": "ms-auto"
113
+ "fileActions": "ms-auto"
108
114
  },
109
115
  "grid": {
110
116
  "fileWrapper": "hidden",
111
117
  "fileLeadingAvatar": "size-full rounded-lg",
112
- "fileTrailingButton": "absolute -top-1.5 -end-1.5 p-0 rounded-full border-2 border-bg"
118
+ "fileActions": "absolute -top-1.5 -end-1.5",
119
+ "fileTrailingButton": "p-0 rounded-full border-2 border-bg",
120
+ "fileUploadButton": "p-0 rounded-full border-2 border-bg",
121
+ "fileCancelButton": "p-0 rounded-full border-2 border-bg"
113
122
  }
114
123
  },
115
124
  "position": {
@@ -130,6 +139,9 @@ export default {
130
139
  },
131
140
  "disabled": {
132
141
  "true": "cursor-not-allowed opacity-75"
142
+ },
143
+ "showProgress": {
144
+ "true": ""
133
145
  }
134
146
  },
135
147
  "compoundVariants": [
@@ -200,35 +212,35 @@ export default {
200
212
  "size": "xs" as typeof size[number],
201
213
  "layout": "list" as typeof layout[number],
202
214
  "class": {
203
- "fileTrailingButton": "-me-1"
215
+ "fileActions": "-me-1"
204
216
  }
205
217
  },
206
218
  {
207
219
  "size": "sm" as typeof size[number],
208
220
  "layout": "list" as typeof layout[number],
209
221
  "class": {
210
- "fileTrailingButton": "-me-1.5"
222
+ "fileActions": "-me-1.5"
211
223
  }
212
224
  },
213
225
  {
214
226
  "size": "md" as typeof size[number],
215
227
  "layout": "list" as typeof layout[number],
216
228
  "class": {
217
- "fileTrailingButton": "-me-1.5"
229
+ "fileActions": "-me-1.5"
218
230
  }
219
231
  },
220
232
  {
221
233
  "size": "lg" as typeof size[number],
222
234
  "layout": "list" as typeof layout[number],
223
235
  "class": {
224
- "fileTrailingButton": "-me-2"
236
+ "fileActions": "-me-2"
225
237
  }
226
238
  },
227
239
  {
228
240
  "size": "xl" as typeof size[number],
229
241
  "layout": "list" as typeof layout[number],
230
242
  "class": {
231
- "fileTrailingButton": "-me-2"
243
+ "fileActions": "-me-2"
232
244
  }
233
245
  },
234
246
  {
@@ -285,6 +297,23 @@ export default {
285
297
  "interactive": true,
286
298
  "disabled": false,
287
299
  "class": "hover:bg-elevated/25"
300
+ },
301
+ {
302
+ "showProgress": true,
303
+ "layout": "list" as typeof layout[number],
304
+ "class": {
305
+ "fileWrapper": "flex-col",
306
+ "fileProgress": "w-full"
307
+ }
308
+ },
309
+ {
310
+ "showProgress": true,
311
+ "layout": "grid" as typeof layout[number],
312
+ "class": {
313
+ "fileWrapper": "absolute inset-0 bg-black/50 flex flex-col items-center justify-center rounded-lg",
314
+ "fileProgress": "w-3/4",
315
+ "fileProgressText": "text-white font-medium"
316
+ }
288
317
  }
289
318
  ],
290
319
  "defaultVariants": {
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslamdevui/ui",
3
- "version": "3.3.2-beta.1",
3
+ "version": "3.3.2-beta.2",
4
4
  "docs": "https://ui.nuxt.com/getting-started/installation/nuxt",
5
5
  "configKey": "ui",
6
6
  "compatibility": {
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { defineNuxtModule, createResolver, addVitePlugin, addPlugin, hasNuxtModule, addComponentsDir, addImportsDir, installModule } from '@nuxt/kit';
2
2
  import { defu } from 'defu';
3
- import { d as defaultOptions, r as resolveColors, a as getDefaultUiConfig, b as addTemplates } from './shared/ui.CgUuvRXi.mjs';
3
+ import { d as defaultOptions, r as resolveColors, a as getDefaultUiConfig, b as addTemplates } from './shared/ui.DdcmBFzX.mjs';
4
4
  import '../dist/runtime/utils/index.js';
5
5
  import 'node:url';
6
6
  import 'scule';
@@ -8,7 +8,7 @@ import 'tailwindcss/colors';
8
8
  import 'knitwork';
9
9
 
10
10
  const name = "@eslamdevui/ui";
11
- const version = "3.3.2-beta.1";
11
+ const version = "3.3.2-beta.2";
12
12
 
13
13
  function generateProseComponentMap(components) {
14
14
  return components.reduce((map, component) => {
@@ -3,7 +3,7 @@ import theme from "#build/ui/file-upload";
3
3
  </script>
4
4
 
5
5
  <script setup>
6
- import { computed, watch } from "vue";
6
+ import { computed, watch, ref, nextTick, readonly } from "vue";
7
7
  import { Primitive } from "reka-ui";
8
8
  import { createReusableTemplate } from "@vueuse/core";
9
9
  import { useAppConfig } from "#imports";
@@ -13,6 +13,7 @@ import { tv } from "../utils/tv";
13
13
  import UAvatar from "./Avatar.vue";
14
14
  import UButton from "./Button.vue";
15
15
  import UIcon from "./Icon.vue";
16
+ import UProgress from "./Progress.vue";
16
17
  defineOptions({ inheritAttrs: false });
17
18
  const props = defineProps({
18
19
  as: { type: null, required: false },
@@ -37,20 +38,28 @@ const props = defineProps({
37
38
  fileIcon: { type: String, required: false },
38
39
  fileDelete: { type: [Boolean, Object], required: false },
39
40
  fileDeleteIcon: { type: String, required: false },
41
+ showProgress: { type: Boolean, required: false, default: false },
42
+ allowCancel: { type: Boolean, required: false, default: true },
43
+ uploadFn: { type: Function, required: false },
44
+ autoUpload: { type: Boolean, required: false, default: false },
45
+ uploadTimeout: { type: Number, required: false, default: 3e4 },
46
+ maxConcurrentUploads: { type: Number, required: false, default: 3 },
40
47
  class: { type: null, required: false },
41
48
  ui: { type: null, required: false }
42
49
  });
43
- const emits = defineEmits(["update:modelValue", "change"]);
50
+ const emits = defineEmits(["update:modelValue", "change", "upload:start", "upload:progress", "upload:success", "upload:error", "upload:cancel", "upload:complete"]);
44
51
  const slots = defineSlots();
45
52
  const modelValue = defineModel({ type: null });
46
53
  const appConfig = useAppConfig();
54
+ const activeUploads = ref(0);
55
+ const uploadQueue = ref([]);
47
56
  const [DefineFilesTemplate, ReuseFilesTemplate] = createReusableTemplate();
48
57
  const { isDragging, open, inputRef, dropzoneRef } = useFileUpload({
49
58
  accept: props.accept,
50
59
  reset: props.reset,
51
60
  multiple: props.multiple,
52
61
  dropzone: props.dropzone,
53
- onUpdate
62
+ onUpdate: onFileUpdate
54
63
  });
55
64
  const { emitFormInput, emitFormChange, id, name, disabled, ariaAttrs } = useFormField(props);
56
65
  const variant = computed(() => props.multiple ? "area" : props.variant);
@@ -64,6 +73,9 @@ const position = computed(() => {
64
73
  }
65
74
  return props.position;
66
75
  });
76
+ const shouldShowProgress = computed(() => {
77
+ return props.showProgress || props.autoUpload;
78
+ });
67
79
  const ui = computed(() => tv({ extend: tv(theme), ...appConfig.ui?.fileUpload || {} })({
68
80
  dropzone: props.dropzone,
69
81
  interactive: props.interactive,
@@ -74,7 +86,8 @@ const ui = computed(() => tv({ extend: tv(theme), ...appConfig.ui?.fileUpload ||
74
86
  position: position.value,
75
87
  multiple: props.multiple,
76
88
  highlight: props.highlight,
77
- disabled: props.disabled
89
+ disabled: props.disabled,
90
+ showProgress: shouldShowProgress.value
78
91
  }));
79
92
  function createObjectUrl(file) {
80
93
  return URL.createObjectURL(file);
@@ -90,77 +103,265 @@ function formatFileSize(bytes) {
90
103
  const formattedSize = i === 0 ? size.toString() : size.toFixed(0);
91
104
  return `${formattedSize}${sizes[i]}`;
92
105
  }
93
- function onUpdate(files, reset = false) {
106
+ function createFileUploadObject(file) {
107
+ return {
108
+ file,
109
+ progress: 0,
110
+ uploading: false,
111
+ error: void 0,
112
+ abortController: void 0
113
+ };
114
+ }
115
+ async function onFileUpdate(files, reset = false) {
116
+ const fileUploads = files.map(createFileUploadObject);
94
117
  if (props.multiple) {
95
118
  if (reset) {
96
- modelValue.value = files;
119
+ modelValue.value = fileUploads;
97
120
  } else {
98
121
  const existingFiles = modelValue.value || [];
99
- modelValue.value = [...existingFiles, ...files || []];
122
+ modelValue.value = [...existingFiles, ...fileUploads];
100
123
  }
101
124
  } else {
102
- modelValue.value = files?.[0];
125
+ modelValue.value = fileUploads?.[0];
103
126
  }
104
127
  const event = new Event("change", { target: { value: modelValue.value } });
105
128
  emits("change", event);
106
129
  emitFormChange();
107
130
  emitFormInput();
131
+ if (props.autoUpload && props.uploadFn) {
132
+ await nextTick();
133
+ const newFiles = props.multiple ? fileUploads : fileUploads[0] ? [fileUploads[0]] : [];
134
+ for (let i = 0; i < newFiles.length; i++) {
135
+ const actualIndex = props.multiple && !reset ? (modelValue.value?.length || 0) - newFiles.length + i : i;
136
+ if (props.multiple) {
137
+ if (newFiles[i]) {
138
+ queueUpload(actualIndex, newFiles[i]);
139
+ }
140
+ } else {
141
+ uploadFile(actualIndex);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ function queueUpload(index, fileUpload) {
147
+ if (!props.multiple) {
148
+ uploadFile(index);
149
+ return;
150
+ }
151
+ if (activeUploads.value < props.maxConcurrentUploads) {
152
+ uploadFile(index);
153
+ } else {
154
+ uploadQueue.value.push({ index, fileUpload });
155
+ }
156
+ }
157
+ function processUploadQueue() {
158
+ while (uploadQueue.value.length > 0 && activeUploads.value < props.maxConcurrentUploads) {
159
+ const queueItem = uploadQueue.value.shift();
160
+ if (queueItem) {
161
+ uploadFile(queueItem.index);
162
+ }
163
+ }
108
164
  }
109
165
  function removeFile(index) {
110
166
  if (!modelValue.value) {
111
167
  return;
112
168
  }
113
169
  if (!props.multiple || index === void 0) {
114
- onUpdate([], true);
170
+ const singleFile = modelValue.value;
171
+ if (singleFile.abortController) {
172
+ singleFile.abortController.abort();
173
+ }
174
+ onFileUpdate([], true);
115
175
  return;
116
176
  }
117
177
  const files = [...modelValue.value];
178
+ const file = files[index];
179
+ if (file?.abortController) {
180
+ file.abortController.abort();
181
+ }
182
+ uploadQueue.value = uploadQueue.value.filter((item) => item.index !== index);
118
183
  files.splice(index, 1);
119
- onUpdate(files, true);
184
+ onFileUpdate(files.map((f) => f.file), true);
185
+ }
186
+ async function uploadFile(index) {
187
+ if (!props.uploadFn || !modelValue.value) return;
188
+ const files = props.multiple ? modelValue.value : [modelValue.value];
189
+ const fileUpload = files[index];
190
+ if (!fileUpload || fileUpload.uploading) return;
191
+ activeUploads.value++;
192
+ const abortController = new AbortController();
193
+ fileUpload.abortController = abortController;
194
+ fileUpload.uploading = true;
195
+ fileUpload.progress = 0;
196
+ fileUpload.error = void 0;
197
+ emits("upload:start", fileUpload, index);
198
+ const timeoutId = setTimeout(() => {
199
+ if (!abortController.signal.aborted) {
200
+ abortController.abort();
201
+ }
202
+ }, props.uploadTimeout);
203
+ try {
204
+ const result = await props.uploadFn(
205
+ fileUpload.file,
206
+ (progress) => {
207
+ fileUpload.progress = Math.min(100, Math.max(0, progress));
208
+ emits("upload:progress", fileUpload, fileUpload.progress, index);
209
+ },
210
+ abortController.signal
211
+ );
212
+ clearTimeout(timeoutId);
213
+ fileUpload.uploading = false;
214
+ fileUpload.progress = 100;
215
+ emits("upload:success", fileUpload, result, index);
216
+ } catch (error) {
217
+ clearTimeout(timeoutId);
218
+ fileUpload.uploading = false;
219
+ if (abortController.signal.aborted) {
220
+ emits("upload:cancel", fileUpload, index);
221
+ } else {
222
+ fileUpload.error = error instanceof Error ? error.message : "Upload failed";
223
+ emits("upload:error", fileUpload, fileUpload.error, index);
224
+ }
225
+ } finally {
226
+ fileUpload.abortController = void 0;
227
+ activeUploads.value--;
228
+ if (props.multiple) {
229
+ processUploadQueue();
230
+ }
231
+ if (props.multiple && activeUploads.value === 0 && uploadQueue.value.length === 0) {
232
+ emits("upload:complete", files);
233
+ }
234
+ }
235
+ }
236
+ function cancelUpload(index) {
237
+ if (!modelValue.value) return;
238
+ const files = props.multiple ? modelValue.value : [modelValue.value];
239
+ const fileUpload = files[index];
240
+ if (fileUpload?.abortController) {
241
+ fileUpload.abortController.abort();
242
+ }
243
+ uploadQueue.value = uploadQueue.value.filter((item) => item.index !== index);
244
+ }
245
+ function shouldShowFileProgress(fileUpload) {
246
+ return shouldShowProgress.value && (fileUpload.uploading || (fileUpload.progress ?? 0) > 0 || !!fileUpload.error);
120
247
  }
121
248
  watch(modelValue, (newValue) => {
122
249
  const hasModelReset = !Array.isArray(newValue) || !newValue.length;
123
250
  if (hasModelReset && inputRef.value) {
124
251
  inputRef.value.value = "";
252
+ uploadQueue.value = [];
253
+ activeUploads.value = 0;
125
254
  }
126
255
  });
127
256
  defineExpose({
128
257
  inputRef,
129
- dropzoneRef
258
+ dropzoneRef,
259
+ uploadFile,
260
+ cancelUpload,
261
+ activeUploads: readonly(activeUploads),
262
+ uploadQueue: readonly(uploadQueue)
130
263
  });
131
264
  </script>
132
265
 
133
266
  <template>
134
267
  <DefineFilesTemplate>
135
268
  <template v-if="modelValue && (Array.isArray(modelValue) ? modelValue.length : true)">
136
- <slot name="files-top" :files="modelValue" :open="open" :remove-file="removeFile" />
269
+ <slot
270
+ name="files-top"
271
+ :files="modelValue"
272
+ :open="open"
273
+ :remove-file="removeFile"
274
+ :upload-file="uploadFile"
275
+ :cancel-upload="cancelUpload"
276
+ />
137
277
 
138
278
  <div :class="ui.files({ class: props.ui?.files })">
139
279
  <slot name="files" :files="modelValue">
140
- <div v-for="(file, index) in Array.isArray(modelValue) ? modelValue : [modelValue]" :key="file.name" :class="ui.file({ class: props.ui?.file })">
141
- <slot name="file" :file="file" :index="index">
142
- <slot name="file-leading" :file="file" :index="index">
143
- <UAvatar :src="createObjectUrl(file)" :icon="fileIcon || appConfig.ui.icons.file" :size="props.size" :class="ui.fileLeadingAvatar({ class: props.ui?.fileLeadingAvatar })" />
280
+ <div
281
+ v-for="(fileUpload, index) in Array.isArray(modelValue) ? modelValue : [modelValue]"
282
+ :key="fileUpload.file.name + index"
283
+ :class="ui.file({ class: props.ui?.file })"
284
+ >
285
+ <slot name="file" :file-upload="fileUpload" :index="index">
286
+ <slot name="file-leading" :file-upload="fileUpload" :index="index">
287
+ <UAvatar
288
+ :src="createObjectUrl(fileUpload.file)"
289
+ :icon="fileIcon || appConfig.ui.icons.file"
290
+ :size="props.size"
291
+ :class="ui.fileLeadingAvatar({ class: props.ui?.fileLeadingAvatar })"
292
+ />
144
293
  </slot>
145
294
 
146
295
  <div :class="ui.fileWrapper({ class: props.ui?.fileWrapper })">
147
296
  <span :class="ui.fileName({ class: props.ui?.fileName })">
148
- <slot name="file-name" :file="file" :index="index">
149
- {{ file.name }}
297
+ <slot name="file-name" :file-upload="fileUpload" :index="index">
298
+ {{ fileUpload.file.name }}
150
299
  </slot>
151
300
  </span>
152
301
 
153
302
  <span :class="ui.fileSize({ class: props.ui?.fileSize })">
154
- <slot name="file-size" :file="file" :index="index">
155
- {{ formatFileSize(file.size) }}
303
+ <slot name="file-size" :file-upload="fileUpload" :index="index">
304
+ {{ formatFileSize(fileUpload.file.size) }}
156
305
  </slot>
157
306
  </span>
307
+
308
+ <!-- Progress bar - Now shows automatically when autoUpload is true -->
309
+ <div
310
+ v-if="shouldShowFileProgress(fileUpload)"
311
+ :class="ui.fileProgress({ class: props.ui?.fileProgress })"
312
+ >
313
+ <slot name="file-progress" :file-upload="fileUpload" :index="index">
314
+ <UProgress
315
+ :model-value="fileUpload.progress || 0"
316
+ :color="fileUpload.error ? 'error' : color"
317
+ :size="size === 'xs' ? 'xs' : 'sm'"
318
+ />
319
+ <span :class="ui.fileProgressText({ class: props.ui?.fileProgressText })">
320
+ {{ Math.round(fileUpload.progress || 0) }}%
321
+ </span>
322
+ </slot>
323
+ </div>
324
+
325
+ <!-- Error message -->
326
+ <div v-if="fileUpload.error" :class="ui.fileError({ class: props.ui?.fileError })">
327
+ {{ fileUpload.error }}
328
+ </div>
329
+
330
+ <!-- Upload status indicator -->
331
+ <div v-if="autoUpload && fileUpload.uploading" class="text-xs text-gray-500 mt-1">
332
+ Uploading...
333
+ </div>
158
334
  </div>
159
335
 
160
- <slot name="file-trailing" :file="file" :index="index">
161
- <UButton
162
- color="neutral"
163
- v-bind="{
336
+ <slot name="file-trailing" :file-upload="fileUpload" :index="index">
337
+ <div :class="ui.fileActions({ class: props.ui?.fileActions })">
338
+ <!-- Upload button (if not auto-upload and uploadFn provided) -->
339
+ <UButton
340
+ v-if="uploadFn && !autoUpload && !fileUpload.uploading && !(fileUpload.progress ?? 0)"
341
+ color="primary"
342
+ variant="ghost"
343
+ :size="layout === 'grid' ? 'xs' : size"
344
+ :trailing-icon="appConfig.ui.icons.upload || 'i-lucide-upload'"
345
+ :class="ui.fileUploadButton({ class: props.ui?.fileUploadButton })"
346
+ @click.stop.prevent="uploadFile(index)"
347
+ />
348
+
349
+ <!-- Cancel button (during upload) -->
350
+ <UButton
351
+ v-if="allowCancel && fileUpload.uploading"
352
+ color="neutral"
353
+ variant="ghost"
354
+ :size="layout === 'grid' ? 'xs' : size"
355
+ :trailing-icon="appConfig.ui.icons.close || 'i-lucide-x'"
356
+ :class="ui.fileCancelButton({ class: props.ui?.fileCancelButton })"
357
+ @click.stop.prevent="cancelUpload(index)"
358
+ />
359
+
360
+ <!-- Delete button -->
361
+ <UButton
362
+ v-if="!fileUpload.uploading"
363
+ color="neutral"
364
+ v-bind="{
164
365
  ...layout === 'grid' ? {
165
366
  variant: 'solid',
166
367
  size: 'xs'
@@ -170,22 +371,30 @@ defineExpose({
170
371
  },
171
372
  ...typeof fileDelete === 'object' ? fileDelete : void 0
172
373
  }"
173
- :trailing-icon="fileDeleteIcon || appConfig.ui.icons.close"
174
- :class="ui.fileTrailingButton({ class: props.ui?.fileTrailingButton })"
175
- @click.stop.prevent="removeFile(index)"
176
- />
374
+ :trailing-icon="fileDeleteIcon || appConfig.ui.icons.close"
375
+ :class="ui.fileTrailingButton({ class: props.ui?.fileTrailingButton })"
376
+ @click.stop.prevent="removeFile(index)"
377
+ />
378
+ </div>
177
379
  </slot>
178
380
  </slot>
179
381
  </div>
180
382
  </slot>
181
383
  </div>
182
384
 
183
- <slot name="files-bottom" :files="modelValue" :open="open" :remove-file="removeFile" />
385
+ <slot
386
+ name="files-bottom"
387
+ :files="modelValue"
388
+ :open="open"
389
+ :remove-file="removeFile"
390
+ :upload-file="uploadFile"
391
+ :cancel-upload="cancelUpload"
392
+ />
184
393
  </template>
185
394
  </DefineFilesTemplate>
186
395
 
187
396
  <Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
188
- <slot :open="open" :remove-file="removeFile">
397
+ <slot :open="open" :remove-file="removeFile" :upload-file="uploadFile" :cancel-upload="cancelUpload">
189
398
  <component
190
399
  :is="variant === 'button' ? 'button' : 'div'"
191
400
  ref="dropzoneRef"
@@ -197,10 +406,22 @@ defineExpose({
197
406
  >
198
407
  <ReuseFilesTemplate v-if="position === 'inside'" />
199
408
 
200
- <div v-if="position === 'inside' ? multiple ? !modelValue?.length : !modelValue : true" :class="ui.wrapper({ class: props.ui?.wrapper })">
409
+ <div
410
+ v-if="position === 'inside' ? multiple ? !modelValue?.length : !modelValue : true"
411
+ :class="ui.wrapper({ class: props.ui?.wrapper })"
412
+ >
201
413
  <slot name="leading">
202
- <UIcon v-if="variant === 'button'" :name="icon || appConfig.ui.icons.upload" :class="ui.icon({ class: props.ui?.icon })" />
203
- <UAvatar v-else :icon="icon || appConfig.ui.icons.upload" :size="props.size" :class="ui.avatar({ class: props.ui?.avatar })" />
414
+ <UIcon
415
+ v-if="variant === 'button'"
416
+ :name="icon || appConfig.ui.icons.upload"
417
+ :class="ui.icon({ class: props.ui?.icon })"
418
+ />
419
+ <UAvatar
420
+ v-else
421
+ :icon="icon || appConfig.ui.icons.upload"
422
+ :size="props.size"
423
+ :class="ui.avatar({ class: props.ui?.avatar })"
424
+ />
204
425
  </slot>
205
426
 
206
427
  <template v-if="variant !== 'button'">
@@ -216,7 +437,14 @@ defineExpose({
216
437
  </div>
217
438
 
218
439
  <div v-if="!!slots.actions" :class="ui.actions({ class: props.ui?.actions })">
219
- <slot name="actions" :files="modelValue" :open="open" :remove-file="removeFile" />
440
+ <slot
441
+ name="actions"
442
+ :files="modelValue"
443
+ :open="open"
444
+ :remove-file="removeFile"
445
+ :upload-file="uploadFile"
446
+ :cancel-upload="cancelUpload"
447
+ />
220
448
  </div>
221
449
  </template>
222
450
  </div>
@@ -4,6 +4,13 @@ import theme from '#build/ui/file-upload';
4
4
  import type { ButtonProps } from '../types';
5
5
  import type { ComponentConfig } from '../types/utils';
6
6
  type FileUpload = ComponentConfig<typeof theme, AppConfig, 'fileUpload'>;
7
+ export interface FileUploadFile {
8
+ file: File;
9
+ progress?: number;
10
+ uploading?: boolean;
11
+ error?: string;
12
+ abortController?: AbortController;
13
+ }
7
14
  export interface FileUploadProps<M extends boolean = false> {
8
15
  /**
9
16
  * The element or component this component should render as.
@@ -89,18 +96,55 @@ export interface FileUploadProps<M extends boolean = false> {
89
96
  * @IconifyIcon
90
97
  */
91
98
  fileDeleteIcon?: string;
99
+ /**
100
+ * Enable upload progress tracking
101
+ * @defaultValue false
102
+ */
103
+ showProgress?: boolean;
104
+ /**
105
+ * Allow cancelling uploads in progress
106
+ * @defaultValue true
107
+ */
108
+ allowCancel?: boolean;
109
+ /**
110
+ * Upload function that returns a promise with progress callback
111
+ */
112
+ uploadFn?: (file: File, onProgress: (progress: number) => void, signal?: AbortSignal) => Promise<any>;
113
+ /**
114
+ * Auto-start upload when files are selected
115
+ * @defaultValue false
116
+ */
117
+ autoUpload?: boolean;
118
+ /**
119
+ * Upload timeout in milliseconds
120
+ * @defaultValue 30000
121
+ */
122
+ uploadTimeout?: number;
123
+ /**
124
+ * Max concurrent uploads when multiple is true
125
+ * @defaultValue 3
126
+ */
127
+ maxConcurrentUploads?: number;
92
128
  class?: any;
93
129
  ui?: FileUpload['slots'];
94
130
  }
95
131
  export interface FileUploadEmits<M extends boolean = false> {
96
- 'update:modelValue': [payload: M extends true ? File[] : File | null];
132
+ 'update:modelValue': [payload: M extends true ? FileUploadFile[] : FileUploadFile | null];
97
133
  'change': [event: Event];
134
+ 'upload:start': [file: FileUploadFile, index: number];
135
+ 'upload:progress': [file: FileUploadFile, progress: number, index: number];
136
+ 'upload:success': [file: FileUploadFile, result: any, index: number];
137
+ 'upload:error': [file: FileUploadFile, error: string, index: number];
138
+ 'upload:cancel': [file: FileUploadFile, index: number];
139
+ 'upload:complete': [files: FileUploadFile[]];
98
140
  }
99
- type FileUploadFiles<M> = (M extends true ? File[] : File) | null;
141
+ type FileUploadFiles<M> = (M extends true ? FileUploadFile[] : FileUploadFile) | null;
100
142
  export interface FileUploadSlots<M extends boolean = false> {
101
143
  'default'(props: {
102
144
  open: UseFileDialogReturn['open'];
103
145
  removeFile: (index?: number) => void;
146
+ uploadFile: (index: number) => void;
147
+ cancelUpload: (index: number) => void;
104
148
  }): any;
105
149
  'leading'(props?: {}): any;
106
150
  'label'(props?: {}): any;
@@ -109,6 +153,8 @@ export interface FileUploadSlots<M extends boolean = false> {
109
153
  files?: FileUploadFiles<M>;
110
154
  open: UseFileDialogReturn['open'];
111
155
  removeFile: (index?: number) => void;
156
+ uploadFile: (index: number) => void;
157
+ cancelUpload: (index: number) => void;
112
158
  }): any;
113
159
  'files'(props: {
114
160
  files?: FileUploadFiles<M>;
@@ -117,30 +163,38 @@ export interface FileUploadSlots<M extends boolean = false> {
117
163
  files?: FileUploadFiles<M>;
118
164
  open: UseFileDialogReturn['open'];
119
165
  removeFile: (index?: number) => void;
166
+ uploadFile: (index: number) => void;
167
+ cancelUpload: (index: number) => void;
120
168
  }): any;
121
169
  'files-bottom'(props: {
122
170
  files?: FileUploadFiles<M>;
123
171
  open: UseFileDialogReturn['open'];
124
172
  removeFile: (index?: number) => void;
173
+ uploadFile: (index: number) => void;
174
+ cancelUpload: (index: number) => void;
125
175
  }): any;
126
176
  'file'(props: {
127
- file: File;
177
+ fileUpload: FileUploadFile;
128
178
  index: number;
129
179
  }): any;
130
180
  'file-leading'(props: {
131
- file: File;
181
+ fileUpload: FileUploadFile;
132
182
  index: number;
133
183
  }): any;
134
184
  'file-name'(props: {
135
- file: File;
185
+ fileUpload: FileUploadFile;
136
186
  index: number;
137
187
  }): any;
138
188
  'file-size'(props: {
139
- file: File;
189
+ fileUpload: FileUploadFile;
190
+ index: number;
191
+ }): any;
192
+ 'file-progress'(props: {
193
+ fileUpload: FileUploadFile;
140
194
  index: number;
141
195
  }): any;
142
196
  'file-trailing'(props: {
143
- file: File;
197
+ fileUpload: FileUploadFile;
144
198
  index: number;
145
199
  }): any;
146
200
  }
@@ -148,16 +202,160 @@ declare const _default: <M extends boolean = false>(__VLS_props: NonNullable<Awa
148
202
  props: __VLS_PrettifyLocal<Pick<Partial<{}> & Omit<{
149
203
  readonly onChange?: ((event: Event) => any) | undefined;
150
204
  readonly "onUpdate:modelValue"?: ((...args: unknown[]) => any) | undefined;
151
- } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onChange" | "onUpdate:modelValue"> & (FileUploadProps<M> & {
152
- modelValue?: (M extends true ? File[] : File) | null;
205
+ readonly "onUpload:start"?: ((file: FileUploadFile, index: number) => any) | undefined;
206
+ readonly "onUpload:progress"?: ((file: FileUploadFile, progress: number, index: number) => any) | undefined;
207
+ readonly "onUpload:success"?: ((file: FileUploadFile, result: any, index: number) => any) | undefined;
208
+ readonly "onUpload:error"?: ((file: FileUploadFile, error: string, index: number) => any) | undefined;
209
+ readonly "onUpload:cancel"?: ((file: FileUploadFile, index: number) => any) | undefined;
210
+ readonly "onUpload:complete"?: ((files: FileUploadFile[]) => any) | undefined;
211
+ } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onChange" | "onUpdate:modelValue" | "onUpload:start" | "onUpload:progress" | "onUpload:success" | "onUpload:error" | "onUpload:cancel" | "onUpload:complete"> & (FileUploadProps<M> & {
212
+ modelValue?: (M extends true ? FileUploadFile[] : FileUploadFile) | null;
153
213
  }) & Partial<{}>> & import("vue").PublicProps;
154
214
  expose(exposed: import("vue").ShallowUnwrapRef<{
155
215
  inputRef: import("vue").Ref<HTMLInputElement | undefined, HTMLInputElement | undefined>;
156
216
  dropzoneRef: import("vue").Ref<HTMLDivElement | undefined, HTMLDivElement | undefined>;
217
+ uploadFile: (index: number) => Promise<void>;
218
+ cancelUpload: (index: number) => void;
219
+ activeUploads: Readonly<import("vue").Ref<number, number>>;
220
+ uploadQueue: Readonly<import("vue").Ref<readonly {
221
+ readonly index: number;
222
+ readonly fileUpload: {
223
+ readonly file: {
224
+ readonly lastModified: number;
225
+ readonly name: string;
226
+ readonly webkitRelativePath: string;
227
+ readonly size: number;
228
+ readonly type: string;
229
+ readonly arrayBuffer: {
230
+ (): Promise<ArrayBuffer>;
231
+ (): Promise<ArrayBuffer>;
232
+ };
233
+ readonly bytes: {
234
+ (): Promise<Uint8Array>;
235
+ (): Promise<Uint8Array>;
236
+ };
237
+ readonly slice: {
238
+ (start?: number, end?: number, contentType?: string): Blob;
239
+ (start?: number, end?: number, contentType?: string): Blob;
240
+ };
241
+ readonly stream: {
242
+ (): ReadableStream<Uint8Array>;
243
+ (): ReadableStream<Uint8Array>;
244
+ };
245
+ readonly text: {
246
+ (): Promise<string>;
247
+ (): Promise<string>;
248
+ };
249
+ };
250
+ readonly progress?: number | undefined;
251
+ readonly uploading?: boolean | undefined;
252
+ readonly error?: string | undefined;
253
+ readonly abortController?: {
254
+ readonly signal: {
255
+ readonly aborted: boolean;
256
+ readonly onabort: ((this: AbortSignal, ev: Event) => any) | null;
257
+ readonly reason: any;
258
+ readonly throwIfAborted: {
259
+ (): void;
260
+ (): void;
261
+ (): void;
262
+ };
263
+ readonly addEventListener: {
264
+ <K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
265
+ (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
266
+ <K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
267
+ (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
268
+ };
269
+ readonly removeEventListener: {
270
+ <K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
271
+ (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
272
+ <K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
273
+ (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
274
+ };
275
+ readonly dispatchEvent: {
276
+ (event: Event): boolean;
277
+ (event: Event): boolean;
278
+ };
279
+ };
280
+ readonly abort: {
281
+ (reason?: any): void;
282
+ (reason?: any): void;
283
+ (reason?: any): void;
284
+ };
285
+ } | undefined;
286
+ };
287
+ }[], readonly {
288
+ readonly index: number;
289
+ readonly fileUpload: {
290
+ readonly file: {
291
+ readonly lastModified: number;
292
+ readonly name: string;
293
+ readonly webkitRelativePath: string;
294
+ readonly size: number;
295
+ readonly type: string;
296
+ readonly arrayBuffer: {
297
+ (): Promise<ArrayBuffer>;
298
+ (): Promise<ArrayBuffer>;
299
+ };
300
+ readonly bytes: {
301
+ (): Promise<Uint8Array>;
302
+ (): Promise<Uint8Array>;
303
+ };
304
+ readonly slice: {
305
+ (start?: number, end?: number, contentType?: string): Blob;
306
+ (start?: number, end?: number, contentType?: string): Blob;
307
+ };
308
+ readonly stream: {
309
+ (): ReadableStream<Uint8Array>;
310
+ (): ReadableStream<Uint8Array>;
311
+ };
312
+ readonly text: {
313
+ (): Promise<string>;
314
+ (): Promise<string>;
315
+ };
316
+ };
317
+ readonly progress?: number | undefined;
318
+ readonly uploading?: boolean | undefined;
319
+ readonly error?: string | undefined;
320
+ readonly abortController?: {
321
+ readonly signal: {
322
+ readonly aborted: boolean;
323
+ readonly onabort: ((this: AbortSignal, ev: Event) => any) | null;
324
+ readonly reason: any;
325
+ readonly throwIfAborted: {
326
+ (): void;
327
+ (): void;
328
+ (): void;
329
+ };
330
+ readonly addEventListener: {
331
+ <K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
332
+ (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
333
+ <K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
334
+ (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
335
+ };
336
+ readonly removeEventListener: {
337
+ <K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
338
+ (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
339
+ <K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
340
+ (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
341
+ };
342
+ readonly dispatchEvent: {
343
+ (event: Event): boolean;
344
+ (event: Event): boolean;
345
+ };
346
+ };
347
+ readonly abort: {
348
+ (reason?: any): void;
349
+ (reason?: any): void;
350
+ (reason?: any): void;
351
+ };
352
+ } | undefined;
353
+ };
354
+ }[]>>;
157
355
  }>): void;
158
356
  attrs: any;
159
357
  slots: FileUploadSlots<M>;
160
- emit: (((evt: "change", event: Event) => void) & ((evt: "update:modelValue", payload: M extends true ? File[] : File | null) => void)) & ((evt: "update:modelValue", value: (M extends true ? File[] : File) | null | undefined) => void);
358
+ emit: (((evt: "change", event: Event) => void) & ((evt: "update:modelValue", payload: M extends true ? FileUploadFile[] : FileUploadFile | null) => void) & ((evt: "upload:start", file: FileUploadFile, index: number) => void) & ((evt: "upload:progress", file: FileUploadFile, progress: number, index: number) => void) & ((evt: "upload:success", file: FileUploadFile, result: any, index: number) => void) & ((evt: "upload:error", file: FileUploadFile, error: string, index: number) => void) & ((evt: "upload:cancel", file: FileUploadFile, index: number) => void) & ((evt: "upload:complete", files: FileUploadFile[]) => void)) & ((evt: "update:modelValue", value: (M extends true ? FileUploadFile[] : FileUploadFile) | null | undefined) => void);
161
359
  }>) => import("vue").VNode & {
162
360
  __ctx?: Awaited<typeof __VLS_setup>;
163
361
  };
@@ -2367,7 +2367,13 @@ const fileUpload = (options) => ({
2367
2367
  fileWrapper: "flex flex-col min-w-0",
2368
2368
  fileName: "text-default truncate",
2369
2369
  fileSize: "text-muted truncate",
2370
- fileTrailingButton: ""
2370
+ fileProgress: "flex items-center gap-2 mt-1",
2371
+ fileProgressText: "text-xs text-muted whitespace-nowrap",
2372
+ fileError: "text-xs text-red-500 mt-1",
2373
+ fileActions: "flex items-center gap-1",
2374
+ fileTrailingButton: "",
2375
+ fileUploadButton: "",
2376
+ fileCancelButton: ""
2371
2377
  },
2372
2378
  variants: {
2373
2379
  color: {
@@ -2416,12 +2422,15 @@ const fileUpload = (options) => ({
2416
2422
  root: "gap-2 items-start",
2417
2423
  files: "flex flex-col w-full gap-2",
2418
2424
  file: "min-w-0 flex items-center border border-default rounded-md w-full",
2419
- fileTrailingButton: "ms-auto"
2425
+ fileActions: "ms-auto"
2420
2426
  },
2421
2427
  grid: {
2422
2428
  fileWrapper: "hidden",
2423
2429
  fileLeadingAvatar: "size-full rounded-lg",
2424
- fileTrailingButton: "absolute -top-1.5 -end-1.5 p-0 rounded-full border-2 border-bg"
2430
+ fileActions: "absolute -top-1.5 -end-1.5",
2431
+ fileTrailingButton: "p-0 rounded-full border-2 border-bg",
2432
+ fileUploadButton: "p-0 rounded-full border-2 border-bg",
2433
+ fileCancelButton: "p-0 rounded-full border-2 border-bg"
2425
2434
  }
2426
2435
  },
2427
2436
  position: {
@@ -2442,6 +2451,9 @@ const fileUpload = (options) => ({
2442
2451
  },
2443
2452
  disabled: {
2444
2453
  true: "cursor-not-allowed opacity-75"
2454
+ },
2455
+ showProgress: {
2456
+ true: ""
2445
2457
  }
2446
2458
  },
2447
2459
  compoundVariants: [...(options.theme.colors || []).map((color) => ({
@@ -2462,31 +2474,31 @@ const fileUpload = (options) => ({
2462
2474
  size: "xs",
2463
2475
  layout: "list",
2464
2476
  class: {
2465
- fileTrailingButton: "-me-1"
2477
+ fileActions: "-me-1"
2466
2478
  }
2467
2479
  }, {
2468
2480
  size: "sm",
2469
2481
  layout: "list",
2470
2482
  class: {
2471
- fileTrailingButton: "-me-1.5"
2483
+ fileActions: "-me-1.5"
2472
2484
  }
2473
2485
  }, {
2474
2486
  size: "md",
2475
2487
  layout: "list",
2476
2488
  class: {
2477
- fileTrailingButton: "-me-1.5"
2489
+ fileActions: "-me-1.5"
2478
2490
  }
2479
2491
  }, {
2480
2492
  size: "lg",
2481
2493
  layout: "list",
2482
2494
  class: {
2483
- fileTrailingButton: "-me-2"
2495
+ fileActions: "-me-2"
2484
2496
  }
2485
2497
  }, {
2486
2498
  size: "xl",
2487
2499
  layout: "list",
2488
2500
  class: {
2489
- fileTrailingButton: "-me-2"
2501
+ fileActions: "-me-2"
2490
2502
  }
2491
2503
  }, {
2492
2504
  variant: "button",
@@ -2535,6 +2547,21 @@ const fileUpload = (options) => ({
2535
2547
  interactive: true,
2536
2548
  disabled: false,
2537
2549
  class: "hover:bg-elevated/25"
2550
+ }, {
2551
+ showProgress: true,
2552
+ layout: "list",
2553
+ class: {
2554
+ fileWrapper: "flex-col",
2555
+ fileProgress: "w-full"
2556
+ }
2557
+ }, {
2558
+ showProgress: true,
2559
+ layout: "grid",
2560
+ class: {
2561
+ fileWrapper: "absolute inset-0 bg-black/50 flex flex-col items-center justify-center rounded-lg",
2562
+ fileProgress: "w-3/4",
2563
+ fileProgressText: "text-white font-medium"
2564
+ }
2538
2565
  }],
2539
2566
  defaultVariants: {
2540
2567
  color: "primary",
package/dist/unplugin.mjs CHANGED
@@ -3,7 +3,7 @@ import { join, normalize } from 'pathe';
3
3
  import { createUnplugin } from 'unplugin';
4
4
  import { defu } from 'defu';
5
5
  import tailwind from '@tailwindcss/vite';
6
- import { g as getTemplates, d as defaultOptions, r as resolveColors, a as getDefaultUiConfig } from './shared/ui.CgUuvRXi.mjs';
6
+ import { g as getTemplates, d as defaultOptions, r as resolveColors, a as getDefaultUiConfig } from './shared/ui.DdcmBFzX.mjs';
7
7
  import { globSync } from 'tinyglobby';
8
8
  import { genSafeVariableName } from 'knitwork';
9
9
  import MagicString from 'magic-string';
package/dist/vite.mjs CHANGED
@@ -4,7 +4,7 @@ import 'pathe';
4
4
  import 'unplugin';
5
5
  import 'defu';
6
6
  import '@tailwindcss/vite';
7
- import './shared/ui.CgUuvRXi.mjs';
7
+ import './shared/ui.DdcmBFzX.mjs';
8
8
  import '../dist/runtime/utils/index.js';
9
9
  import 'scule';
10
10
  import '@nuxt/kit';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eslamdevui/ui",
3
3
  "description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
4
- "version": "3.3.2-beta.1",
4
+ "version": "3.3.2-beta.2",
5
5
  "packageManager": "pnpm@10.13.1",
6
6
  "repository": {
7
7
  "type": "git",