@uploadista/vue 0.0.20-beta.9 → 0.1.0-beta.5

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 (77) hide show
  1. package/dist/components/index.d.mts +3 -3
  2. package/dist/components/index.mjs +1 -1
  3. package/dist/{components-CskPs6sR.css → components-B_L33hsM.css} +33 -33
  4. package/dist/{components-CskPs6sR.css.map → components-B_L33hsM.css.map} +1 -1
  5. package/dist/components-MZ9ETx9c.mjs +2 -0
  6. package/dist/components-MZ9ETx9c.mjs.map +1 -0
  7. package/dist/composables/index.d.mts +1 -1
  8. package/dist/composables/index.mjs +1 -1
  9. package/dist/composables-Dny_9Zrg.mjs +2 -0
  10. package/dist/composables-Dny_9Zrg.mjs.map +1 -0
  11. package/dist/index-6Scxoy1b.d.mts +1289 -0
  12. package/dist/index-6Scxoy1b.d.mts.map +1 -0
  13. package/dist/{index-B2fUTjNP.d.mts → index-BpCRFLJ5.d.mts} +4 -4
  14. package/dist/index-BpCRFLJ5.d.mts.map +1 -0
  15. package/dist/{index-DiRR_Ua6.d.mts → index-RY4FPqAk.d.mts} +431 -432
  16. package/dist/index-RY4FPqAk.d.mts.map +1 -0
  17. package/dist/index.d.mts +5 -5
  18. package/dist/index.mjs +1 -1
  19. package/dist/providers/index.d.mts +1 -1
  20. package/dist/providers/index.mjs +1 -1
  21. package/dist/{providers-fqmOwF71.mjs → providers-CjhEBaQV.mjs} +2 -2
  22. package/dist/providers-CjhEBaQV.mjs.map +1 -0
  23. package/dist/useUploadistaClient-WVuo8jYH.mjs.map +1 -1
  24. package/dist/utils/index.d.mts +62 -2
  25. package/dist/utils/index.d.mts.map +1 -0
  26. package/package.json +11 -9
  27. package/src/__tests__/setup.ts +154 -0
  28. package/src/components/FlowUploadList.vue +25 -24
  29. package/src/components/UploadList.vue +3 -6
  30. package/src/components/UploadZone.vue +2 -5
  31. package/src/components/flow/Flow.vue +16 -4
  32. package/src/components/flow/FlowDropZone.vue +4 -2
  33. package/src/components/flow/FlowInput.vue +14 -8
  34. package/src/components/flow/FlowInputDropZone.vue +4 -2
  35. package/src/components/flow/FlowInputPreview.vue +3 -1
  36. package/src/components/flow/FlowProgress.vue +1 -1
  37. package/src/components/flow/FlowStatus.vue +1 -1
  38. package/src/components/flow/useFlowContext.ts +7 -5
  39. package/src/components/index.ts +4 -2
  40. package/src/components/upload/Upload.vue +146 -0
  41. package/src/components/upload/UploadCancel.vue +22 -0
  42. package/src/components/upload/UploadClearCompleted.vue +24 -0
  43. package/src/components/upload/UploadDropZone.vue +96 -0
  44. package/src/components/upload/UploadError.vue +42 -0
  45. package/src/components/upload/UploadItem.vue +54 -0
  46. package/src/components/upload/UploadItems.vue +33 -0
  47. package/src/components/upload/UploadProgress.vue +35 -0
  48. package/src/components/upload/UploadReset.vue +20 -0
  49. package/src/components/upload/UploadRetry.vue +22 -0
  50. package/src/components/upload/UploadStartAll.vue +30 -0
  51. package/src/components/upload/UploadStatus.vue +65 -0
  52. package/src/components/upload/index.ts +98 -0
  53. package/src/components/upload/useUploadContext.ts +67 -0
  54. package/src/composables/eventUtils.test.ts +267 -0
  55. package/src/composables/eventUtils.ts +5 -4
  56. package/src/composables/index.ts +1 -1
  57. package/src/composables/useDragDrop.test.ts +304 -0
  58. package/src/composables/useFlow.ts +6 -2
  59. package/src/composables/useFlowManagerContext.ts +5 -1
  60. package/src/composables/useUploadEvents.ts +1 -4
  61. package/src/composables/useUploadistaClient.test.ts +152 -0
  62. package/src/index.ts +65 -4
  63. package/src/providers/FlowManagerProvider.vue +5 -2
  64. package/src/utils/index.test.ts +396 -0
  65. package/src/utils/is-browser-file.test.ts +45 -0
  66. package/vitest.config.ts +25 -0
  67. package/dist/components-BxBz_7tS.mjs +0 -2
  68. package/dist/components-BxBz_7tS.mjs.map +0 -1
  69. package/dist/composables-BZ2c_WgI.mjs +0 -2
  70. package/dist/composables-BZ2c_WgI.mjs.map +0 -1
  71. package/dist/index-B2fUTjNP.d.mts.map +0 -1
  72. package/dist/index-BLNNvTVx.d.mts +0 -62
  73. package/dist/index-BLNNvTVx.d.mts.map +0 -1
  74. package/dist/index-D3PNaPGh.d.mts +0 -787
  75. package/dist/index-D3PNaPGh.d.mts.map +0 -1
  76. package/dist/index-DiRR_Ua6.d.mts.map +0 -1
  77. package/dist/providers-fqmOwF71.mjs.map +0 -1
@@ -1,9 +1,17 @@
1
1
  <script setup lang="ts">
2
2
  import type { FlowUploadOptions } from "@uploadista/client-browser";
3
+ import type {
4
+ FlowUploadState,
5
+ FlowUploadStatus,
6
+ InputExecutionState,
7
+ } from "@uploadista/client-core";
3
8
  import type { TypedOutput } from "@uploadista/core/flow";
4
- import { provide, computed, toRefs } from "vue";
5
- import { useFlow, type UseFlowReturn, type FlowInputMetadata } from "../../composables/useFlow";
6
- import type { FlowUploadState, FlowUploadStatus, InputExecutionState } from "@uploadista/client-core";
9
+ import { provide } from "vue";
10
+ import {
11
+ type FlowInputMetadata,
12
+ type UseFlowReturn,
13
+ useFlow,
14
+ } from "../../composables/useFlow";
7
15
 
8
16
  /**
9
17
  * Props for the Flow root component.
@@ -27,7 +35,11 @@ const emit = defineEmits<{
27
35
  /** Called when flow fails */
28
36
  error: [error: Error];
29
37
  /** Called on upload progress */
30
- progress: [uploadId: string, bytesUploaded: number, totalBytes: number | null];
38
+ progress: [
39
+ uploadId: string,
40
+ bytesUploaded: number,
41
+ totalBytes: number | null,
42
+ ];
31
43
  /** Called when flow completes with all outputs */
32
44
  flowComplete: [outputs: TypedOutput[]];
33
45
  /** Called when upload is aborted */
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } from "vue";
3
- import { useDragDrop, type DragDropState } from "../../composables/useDragDrop";
3
+ import { type DragDropState, useDragDrop } from "../../composables/useDragDrop";
4
4
  import { useFlowContext } from "./useFlowContext";
5
5
 
6
6
  /**
@@ -28,7 +28,9 @@ const dragDrop = useDragDrop({
28
28
  flow.upload(file);
29
29
  }
30
30
  },
31
- accept: props.accept ? props.accept.split(",").map((t) => t.trim()) : undefined,
31
+ accept: props.accept
32
+ ? props.accept.split(",").map((t) => t.trim())
33
+ : undefined,
32
34
  maxFileSize: props.maxFileSize,
33
35
  multiple: false,
34
36
  });
@@ -1,6 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, provide } from "vue";
3
- import { useFlowContext, FLOW_INPUT_CONTEXT_KEY, type FlowInputContextValue } from "./useFlowContext";
3
+ import {
4
+ FLOW_INPUT_CONTEXT_KEY,
5
+ type FlowInputContextValue,
6
+ useFlowContext,
7
+ } from "./useFlowContext";
4
8
 
5
9
  /**
6
10
  * Props for FlowInput component.
@@ -15,7 +19,7 @@ const flow = useFlowContext();
15
19
 
16
20
  // Find metadata for this input
17
21
  const metadata = computed(() =>
18
- flow.inputMetadata.value?.find((m) => m.nodeId === props.nodeId)
22
+ flow.inputMetadata.value?.find((m) => m.nodeId === props.nodeId),
19
23
  );
20
24
 
21
25
  // Get current value for this input
@@ -36,12 +40,14 @@ const contextValue: FlowInputContextValue = {
36
40
  return props.nodeId;
37
41
  },
38
42
  get metadata() {
39
- return metadata.value ?? {
40
- nodeId: props.nodeId,
41
- nodeName: "",
42
- nodeDescription: "",
43
- required: false,
44
- };
43
+ return (
44
+ metadata.value ?? {
45
+ nodeId: props.nodeId,
46
+ nodeName: "",
47
+ nodeDescription: "",
48
+ required: false,
49
+ }
50
+ );
45
51
  },
46
52
  get value() {
47
53
  return value.value;
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } from "vue";
3
- import { useDragDrop, type DragDropState } from "../../composables/useDragDrop";
3
+ import { type DragDropState, useDragDrop } from "../../composables/useDragDrop";
4
4
  import { useFlowInputContext } from "./useFlowContext";
5
5
 
6
6
  // Helper function to check if value is a File (for template use)
@@ -32,7 +32,9 @@ const dragDrop = useDragDrop({
32
32
  input.setValue(file);
33
33
  }
34
34
  },
35
- accept: props.accept ? props.accept.split(",").map((t) => t.trim()) : undefined,
35
+ accept: props.accept
36
+ ? props.accept.split(",").map((t) => t.trim())
37
+ : undefined,
36
38
  maxFileSize: props.maxFileSize,
37
39
  multiple: false,
38
40
  });
@@ -5,7 +5,9 @@ import { useFlowInputContext } from "./useFlowContext";
5
5
  const input = useFlowInputContext();
6
6
 
7
7
  const isFile = computed(() => input.value instanceof File);
8
- const isUrl = computed(() => typeof input.value === "string" && (input.value as string).length > 0);
8
+ const isUrl = computed(
9
+ () => typeof input.value === "string" && (input.value as string).length > 0,
10
+ );
9
11
 
10
12
  const clear = () => {
11
13
  input.setValue(undefined);
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { computed } from "vue";
3
2
  import type { FlowUploadStatus } from "@uploadista/client-core";
3
+ import { computed } from "vue";
4
4
  import { useFlowContext } from "./useFlowContext";
5
5
 
6
6
  const flow = useFlowContext();
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { computed } from "vue";
3
2
  import type { FlowUploadStatus } from "@uploadista/client-core";
4
3
  import type { TypedOutput } from "@uploadista/core/flow";
4
+ import { computed } from "vue";
5
5
  import { useFlowContext } from "./useFlowContext";
6
6
 
7
7
  const flow = useFlowContext();
@@ -30,11 +30,13 @@ export interface FlowInputContextValue {
30
30
  /** Set the value for this input */
31
31
  setValue: (value: unknown) => void;
32
32
  /** Per-input execution state (if available) */
33
- state: {
34
- status: string;
35
- progress: number;
36
- error: Error | null;
37
- } | undefined;
33
+ state:
34
+ | {
35
+ status: string;
36
+ progress: number;
37
+ error: Error | null;
38
+ }
39
+ | undefined;
38
40
  }
39
41
 
40
42
  /**
@@ -6,8 +6,10 @@
6
6
  */
7
7
 
8
8
  export { default as FlowUploadList } from "./FlowUploadList.vue";
9
+ // Flow compound components
10
+ export * from "./flow";
9
11
  export { default as UploadList } from "./UploadList.vue";
10
12
  export { default as UploadZone } from "./UploadZone.vue";
11
13
 
12
- // Flow compound components
13
- export * from "./flow";
14
+ // Upload compound components
15
+ export * from "./upload";
@@ -0,0 +1,146 @@
1
+ <script setup lang="ts">
2
+ import type { UploadFile } from "@uploadista/core/types";
3
+ import { computed, provide } from "vue";
4
+ import {
5
+ type MultiUploadState,
6
+ type UploadItem,
7
+ useMultiUpload,
8
+ } from "../../composables/useMultiUpload";
9
+ import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
10
+
11
+ /**
12
+ * Props for the Upload root component.
13
+ */
14
+ export interface UploadProps {
15
+ /** Whether to allow multiple file uploads (default: false) */
16
+ multiple?: boolean;
17
+ /** Maximum concurrent uploads (default: 3, only used in multi mode) */
18
+ maxConcurrent?: number;
19
+ /** Whether to auto-start uploads when files are received (default: true) */
20
+ autoStart?: boolean;
21
+ /** Metadata to attach to uploads */
22
+ metadata?: Record<string, string>;
23
+ }
24
+
25
+ const props = withDefaults(defineProps<UploadProps>(), {
26
+ multiple: false,
27
+ maxConcurrent: 3,
28
+ autoStart: true,
29
+ });
30
+
31
+ const emit = defineEmits<{
32
+ /** Called when a single file upload succeeds (single mode) */
33
+ success: [result: UploadFile];
34
+ /** Called when an upload fails */
35
+ error: [error: Error, item?: UploadItem];
36
+ /** Called when all uploads complete (multi mode) */
37
+ complete: [
38
+ results: { successful: UploadItem[]; failed: UploadItem[]; total: number },
39
+ ];
40
+ /** Called when an individual upload starts */
41
+ uploadStart: [item: UploadItem];
42
+ /** Called on upload progress */
43
+ progress: [
44
+ item: UploadItem,
45
+ progress: number,
46
+ bytesUploaded: number,
47
+ totalBytes: number | null,
48
+ ];
49
+ }>();
50
+
51
+ const multiUpload = useMultiUpload({
52
+ maxConcurrent: props.maxConcurrent,
53
+ metadata: props.metadata,
54
+ onUploadStart: (item) => emit("uploadStart", item),
55
+ onUploadProgress: (item, progress, bytesUploaded, totalBytes) =>
56
+ emit("progress", item, progress, bytesUploaded, totalBytes),
57
+ onUploadSuccess: (_item, result) => {
58
+ // In single mode, call success directly
59
+ if (!props.multiple) {
60
+ emit("success", result);
61
+ }
62
+ },
63
+ onUploadError: (item, error) => {
64
+ emit("error", error, item);
65
+ },
66
+ onComplete: (results) => emit("complete", results),
67
+ });
68
+
69
+ const handleFilesReceived = (files: File[]) => {
70
+ if (!props.multiple) {
71
+ // Single mode: clear existing and add new file
72
+ multiUpload.clearAll();
73
+ }
74
+ multiUpload.addFiles(files);
75
+ if (props.autoStart) {
76
+ // Use setTimeout to ensure state is updated before starting
77
+ setTimeout(() => multiUpload.startAll(), 0);
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Context value provided by the Upload component root.
83
+ * Contains all upload state and actions.
84
+ */
85
+ export interface UploadContextValue {
86
+ /** Whether in multi-file mode */
87
+ mode: "single" | "multi";
88
+ /** Current multi-upload state (aggregate) */
89
+ state: MultiUploadState;
90
+ /** All upload items */
91
+ items: readonly UploadItem[];
92
+ /** Whether auto-start is enabled */
93
+ autoStart: boolean;
94
+
95
+ /** Add files to the upload queue */
96
+ addFiles: (files: File[]) => void;
97
+ /** Remove an item from the queue */
98
+ removeItem: (id: string) => void;
99
+ /** Start all pending uploads */
100
+ startAll: () => void;
101
+ /** Abort a specific upload by ID */
102
+ abortUpload: (id: string) => void;
103
+ /** Abort all active uploads */
104
+ abortAll: () => void;
105
+ /** Retry a specific failed upload by ID */
106
+ retryUpload: (id: string) => void;
107
+ /** Retry all failed uploads */
108
+ retryFailed: () => void;
109
+ /** Clear all completed uploads */
110
+ clearCompleted: () => void;
111
+ /** Clear all items and reset state */
112
+ clearAll: () => void;
113
+
114
+ /** Internal handler for files received from drop zone */
115
+ handleFilesReceived: (files: File[]) => void;
116
+ }
117
+
118
+ // Create computed context value that updates reactively
119
+ // Cast items to mutable array for context (the readonly is enforced at the composable level)
120
+ const contextValue = computed<UploadContextValue>(() => ({
121
+ mode: props.multiple ? "multi" : "single",
122
+ state: multiUpload.state.value,
123
+ items: multiUpload.items.value as UploadItem[],
124
+ autoStart: props.autoStart,
125
+ addFiles: multiUpload.addFiles,
126
+ removeItem: multiUpload.removeItem,
127
+ startAll: multiUpload.startAll,
128
+ abortUpload: multiUpload.abortUpload,
129
+ abortAll: multiUpload.abortAll,
130
+ retryUpload: multiUpload.retryUpload,
131
+ retryFailed: multiUpload.retryFailed,
132
+ clearCompleted: multiUpload.clearCompleted,
133
+ clearAll: multiUpload.clearAll,
134
+ handleFilesReceived,
135
+ }));
136
+
137
+ // Provide context for child components
138
+ provide(UPLOAD_CONTEXT_KEY, contextValue);
139
+
140
+ // Expose to parent via defineExpose for programmatic access
141
+ defineExpose(contextValue);
142
+ </script>
143
+
144
+ <template>
145
+ <slot />
146
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from "vue";
3
+ import type { UploadContextValue } from "./Upload.vue";
4
+ import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
5
+
6
+ const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
7
+ if (!uploadContext) {
8
+ throw new Error("UploadCancel must be used within an <Upload> component.");
9
+ }
10
+
11
+ const isDisabled = computed(() => !uploadContext.value.state.isUploading);
12
+
13
+ const handleClick = () => {
14
+ uploadContext.value.abortAll();
15
+ };
16
+ </script>
17
+
18
+ <template>
19
+ <button type="button" :disabled="isDisabled" @click="handleClick">
20
+ <slot />
21
+ </button>
22
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from "vue";
3
+ import type { UploadContextValue } from "./Upload.vue";
4
+ import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
5
+
6
+ const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
7
+ if (!uploadContext) {
8
+ throw new Error(
9
+ "UploadClearCompleted must be used within an <Upload> component.",
10
+ );
11
+ }
12
+
13
+ const isDisabled = computed(() => uploadContext.value.state.completed === 0);
14
+
15
+ const handleClick = () => {
16
+ uploadContext.value.clearCompleted();
17
+ };
18
+ </script>
19
+
20
+ <template>
21
+ <button type="button" :disabled="isDisabled" @click="handleClick">
22
+ <slot />
23
+ </button>
24
+ </template>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, ref } from "vue";
3
+ import { type DragDropState, useDragDrop } from "../../composables/useDragDrop";
4
+ import type { UploadContextValue } from "./Upload.vue";
5
+ import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
6
+
7
+ /**
8
+ * Props for UploadDropZone component.
9
+ */
10
+ export interface UploadDropZoneProps {
11
+ /** Accepted file types (e.g., "image/*", ".pdf") */
12
+ accept?: string;
13
+ /** Maximum file size in bytes */
14
+ maxFileSize?: number;
15
+ /** Maximum number of files (only in multi mode) */
16
+ maxFiles?: number;
17
+ }
18
+
19
+ /**
20
+ * Slot props for UploadDropZone component.
21
+ */
22
+ export interface UploadDropZoneSlotProps {
23
+ /** Whether files are being dragged over */
24
+ isDragging: boolean;
25
+ /** Whether drag is over the zone */
26
+ isOver: boolean;
27
+ /** Validation errors */
28
+ errors: readonly string[];
29
+ /** Drag event handlers to bind to the drop zone element */
30
+ dragHandlers: {
31
+ onDragenter: (event: DragEvent) => void;
32
+ onDragover: (event: DragEvent) => void;
33
+ onDragleave: (event: DragEvent) => void;
34
+ onDrop: (event: DragEvent) => void;
35
+ };
36
+ /** Input props for the hidden file input */
37
+ inputProps: {
38
+ type: "file";
39
+ multiple: boolean;
40
+ accept: string | undefined;
41
+ };
42
+ /** Handler for input change event */
43
+ onInputChange: (event: Event) => void;
44
+ /** Open file picker programmatically */
45
+ openFilePicker: () => void;
46
+ /** Current drag-drop state */
47
+ dragDropState: DragDropState;
48
+ }
49
+
50
+ const props = defineProps<UploadDropZoneProps>();
51
+
52
+ const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
53
+ if (!uploadContext) {
54
+ throw new Error("UploadDropZone must be used within an <Upload> component.");
55
+ }
56
+
57
+ const inputRef = ref<HTMLInputElement>();
58
+
59
+ const dragDrop = useDragDrop({
60
+ onFilesReceived: (files) => uploadContext.value.handleFilesReceived(files),
61
+ accept: props.accept
62
+ ? props.accept.split(",").map((t) => t.trim())
63
+ : undefined,
64
+ maxFileSize: props.maxFileSize,
65
+ maxFiles: uploadContext.value.mode === "multi" ? props.maxFiles : 1,
66
+ multiple: uploadContext.value.mode === "multi",
67
+ });
68
+
69
+ const openFilePicker = () => {
70
+ inputRef.value?.click();
71
+ };
72
+
73
+ const slotProps = computed<UploadDropZoneSlotProps>(() => ({
74
+ isDragging: dragDrop.state.value.isDragging,
75
+ isOver: dragDrop.state.value.isOver,
76
+ errors: dragDrop.state.value.errors,
77
+ dragHandlers: {
78
+ onDragenter: dragDrop.onDragEnter,
79
+ onDragover: dragDrop.onDragOver,
80
+ onDragleave: dragDrop.onDragLeave,
81
+ onDrop: dragDrop.onDrop,
82
+ },
83
+ inputProps: dragDrop.inputProps.value,
84
+ onInputChange: dragDrop.onInputChange,
85
+ openFilePicker,
86
+ dragDropState: dragDrop.state.value,
87
+ }));
88
+
89
+ defineExpose({ inputRef });
90
+ </script>
91
+
92
+ <template>
93
+ <slot v-bind="slotProps">
94
+ <!-- Default slot content if none provided -->
95
+ </slot>
96
+ </template>
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from "vue";
3
+ import type { UploadItem } from "../../composables/useMultiUpload";
4
+ import type { UploadContextValue } from "./Upload.vue";
5
+ import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
6
+
7
+ /**
8
+ * Slot props for UploadError component.
9
+ */
10
+ export interface UploadErrorSlotProps {
11
+ /** Whether there are any errors */
12
+ hasError: boolean;
13
+ /** Number of failed uploads */
14
+ failedCount: number;
15
+ /** Failed items */
16
+ failedItems: readonly UploadItem[];
17
+ /** Reset/clear all errors */
18
+ reset: () => void;
19
+ }
20
+
21
+ const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
22
+ if (!uploadContext) {
23
+ throw new Error("UploadError must be used within an <Upload> component.");
24
+ }
25
+
26
+ const slotProps = computed<UploadErrorSlotProps>(() => {
27
+ const failedItems = uploadContext.value.items.filter((item) =>
28
+ ["error", "aborted"].includes(item.state.status),
29
+ );
30
+
31
+ return {
32
+ hasError: failedItems.length > 0,
33
+ failedCount: failedItems.length,
34
+ failedItems,
35
+ reset: uploadContext.value.clearCompleted,
36
+ };
37
+ });
38
+ </script>
39
+
40
+ <template>
41
+ <slot v-bind="slotProps" />
42
+ </template>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, provide } from "vue";
3
+ import type { UploadContextValue } from "./Upload.vue";
4
+ import {
5
+ UPLOAD_CONTEXT_KEY,
6
+ UPLOAD_ITEM_CONTEXT_KEY,
7
+ type UploadItemContextValue,
8
+ } from "./useUploadContext";
9
+
10
+ /**
11
+ * Props for UploadItem component.
12
+ */
13
+ export interface UploadItemProps {
14
+ /** Item ID */
15
+ id: string;
16
+ }
17
+
18
+ /**
19
+ * Slot props for UploadItem component.
20
+ */
21
+ export interface UploadItemSlotProps extends UploadItemContextValue {}
22
+
23
+ const props = defineProps<UploadItemProps>();
24
+
25
+ const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
26
+ if (!uploadContext) {
27
+ throw new Error("UploadItem must be used within an <Upload> component.");
28
+ }
29
+
30
+ const item = computed(() =>
31
+ uploadContext.value.items.find((i) => i.id === props.id),
32
+ );
33
+
34
+ const itemContext = computed<UploadItemContextValue | null>(() => {
35
+ const currentItem = item.value;
36
+ if (!currentItem) return null;
37
+
38
+ return {
39
+ id: props.id,
40
+ file: currentItem.file,
41
+ state: currentItem.state,
42
+ abort: () => uploadContext.value.abortUpload(props.id),
43
+ retry: () => uploadContext.value.retryUpload(props.id),
44
+ remove: () => uploadContext.value.removeItem(props.id),
45
+ };
46
+ });
47
+
48
+ // Provide item context for nested components
49
+ provide(UPLOAD_ITEM_CONTEXT_KEY, itemContext);
50
+ </script>
51
+
52
+ <template>
53
+ <slot v-if="itemContext" v-bind="itemContext" />
54
+ </template>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from "vue";
3
+ import type { UploadItem } from "../../composables/useMultiUpload";
4
+ import type { UploadContextValue } from "./Upload.vue";
5
+ import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
6
+
7
+ /**
8
+ * Slot props for UploadItems component.
9
+ */
10
+ export interface UploadItemsSlotProps {
11
+ /** All upload items */
12
+ items: readonly UploadItem[];
13
+ /** Whether there are any items */
14
+ hasItems: boolean;
15
+ /** Whether items array is empty */
16
+ isEmpty: boolean;
17
+ }
18
+
19
+ const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
20
+ if (!uploadContext) {
21
+ throw new Error("UploadItems must be used within an <Upload> component.");
22
+ }
23
+
24
+ const slotProps = computed<UploadItemsSlotProps>(() => ({
25
+ items: uploadContext.value.items,
26
+ hasItems: uploadContext.value.items.length > 0,
27
+ isEmpty: uploadContext.value.items.length === 0,
28
+ }));
29
+ </script>
30
+
31
+ <template>
32
+ <slot v-bind="slotProps" />
33
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from "vue";
3
+ import type { UploadContextValue } from "./Upload.vue";
4
+ import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
5
+
6
+ /**
7
+ * Slot props for UploadProgress component.
8
+ */
9
+ export interface UploadProgressSlotProps {
10
+ /** Progress percentage (0-100) */
11
+ progress: number;
12
+ /** Bytes uploaded so far */
13
+ bytesUploaded: number;
14
+ /** Total bytes to upload */
15
+ totalBytes: number;
16
+ /** Whether any uploads are active */
17
+ isUploading: boolean;
18
+ }
19
+
20
+ const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
21
+ if (!uploadContext) {
22
+ throw new Error("UploadProgress must be used within an <Upload> component.");
23
+ }
24
+
25
+ const slotProps = computed<UploadProgressSlotProps>(() => ({
26
+ progress: uploadContext.value.state.progress,
27
+ bytesUploaded: uploadContext.value.state.totalBytesUploaded,
28
+ totalBytes: uploadContext.value.state.totalBytes,
29
+ isUploading: uploadContext.value.state.isUploading,
30
+ }));
31
+ </script>
32
+
33
+ <template>
34
+ <slot v-bind="slotProps" />
35
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import { inject } from "vue";
3
+ import type { UploadContextValue } from "./Upload.vue";
4
+ import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
5
+
6
+ const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
7
+ if (!uploadContext) {
8
+ throw new Error("UploadReset must be used within an <Upload> component.");
9
+ }
10
+
11
+ const handleClick = () => {
12
+ uploadContext.value.clearAll();
13
+ };
14
+ </script>
15
+
16
+ <template>
17
+ <button type="button" @click="handleClick">
18
+ <slot />
19
+ </button>
20
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from "vue";
3
+ import type { UploadContextValue } from "./Upload.vue";
4
+ import { UPLOAD_CONTEXT_KEY } from "./useUploadContext";
5
+
6
+ const uploadContext = inject<{ value: UploadContextValue }>(UPLOAD_CONTEXT_KEY);
7
+ if (!uploadContext) {
8
+ throw new Error("UploadRetry must be used within an <Upload> component.");
9
+ }
10
+
11
+ const isDisabled = computed(() => uploadContext.value.state.failed === 0);
12
+
13
+ const handleClick = () => {
14
+ uploadContext.value.retryFailed();
15
+ };
16
+ </script>
17
+
18
+ <template>
19
+ <button type="button" :disabled="isDisabled" @click="handleClick">
20
+ <slot />
21
+ </button>
22
+ </template>