@uploadista/vue 0.0.3

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.
@@ -0,0 +1,303 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * UploadList - Display a list of uploads with customizable status grouping and sorting
4
+ *
5
+ * Shows the progress and status of multiple file uploads. Supports filtering, sorting,
6
+ * and status-based grouping. Provides flexible slot-based customization for rendering each item.
7
+ *
8
+ * @component
9
+ * @example
10
+ * // Basic upload list
11
+ * <UploadList :uploads="uploads" />
12
+ *
13
+ * @example
14
+ * // Custom item rendering with status indicators
15
+ * <UploadList :uploads="uploads">
16
+ * <template #item="{ item, isSuccess, isError }">
17
+ * <div class="upload-item">
18
+ * <span>{{ item.filename }}</span>
19
+ * <progress :value="item.progress" max="100"></progress>
20
+ * <span :class="{ success: isSuccess, error: isError }">
21
+ * {{ item.state.status }}
22
+ * </span>
23
+ * </div>
24
+ * </template>
25
+ * </UploadList>
26
+ *
27
+ * @example
28
+ * // With filtering and sorting
29
+ * <UploadList
30
+ * :uploads="uploads"
31
+ * :filter="item => item.state.status === 'success'"
32
+ * :sort-by="(a, b) => a.uploadedAt - b.uploadedAt"
33
+ * />
34
+ */
35
+ import { computed } from "vue";
36
+ import type { UploadItem } from "../composables";
37
+ import { isBrowserFile } from "../utils";
38
+
39
+ /**
40
+ * Props for the UploadList component
41
+ * @property {UploadItem[]} uploads - Array of upload items to display
42
+ * @property {Function} filter - Optional filter for which items to display
43
+ * @property {Function} sortBy - Optional sorting function for items (a, b) => number
44
+ */
45
+ export interface UploadListProps {
46
+ /**
47
+ * Array of upload items to display
48
+ */
49
+ uploads: UploadItem[];
50
+
51
+ /**
52
+ * Optional filter for which items to display
53
+ */
54
+ filter?: (item: UploadItem) => boolean;
55
+
56
+ /**
57
+ * Optional sorting function for items
58
+ */
59
+ sortBy?: (a: UploadItem, b: UploadItem) => number;
60
+ }
61
+
62
+ const props = defineProps<UploadListProps>();
63
+
64
+ defineSlots<{
65
+ item(props: {
66
+ item: UploadItem;
67
+ index: number;
68
+ isUploading: boolean;
69
+ isSuccess: boolean;
70
+ isError: boolean;
71
+ formatFileSize: (bytes: number) => string;
72
+ }): any;
73
+ default?(props: {
74
+ items: UploadItem[];
75
+ itemsByStatus: {
76
+ idle: UploadItem[];
77
+ uploading: UploadItem[];
78
+ success: UploadItem[];
79
+ error: UploadItem[];
80
+ aborted: UploadItem[];
81
+ };
82
+ }): any;
83
+ }>();
84
+
85
+ // Apply filtering and sorting
86
+ const filteredItems = computed(() => {
87
+ let items = props.uploads;
88
+
89
+ if (props.filter) {
90
+ items = items.filter(props.filter);
91
+ }
92
+
93
+ if (props.sortBy) {
94
+ items = [...items].sort(props.sortBy);
95
+ }
96
+
97
+ return items;
98
+ });
99
+
100
+ // Group items by status
101
+ // biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
102
+ const itemsByStatus = computed(() => ({
103
+ idle: filteredItems.value.filter((item) => item.state.status === "idle"),
104
+ uploading: filteredItems.value.filter(
105
+ (item) => item.state.status === "uploading",
106
+ ),
107
+ success: filteredItems.value.filter(
108
+ (item) => item.state.status === "success",
109
+ ),
110
+ error: filteredItems.value.filter((item) => item.state.status === "error"),
111
+ aborted: filteredItems.value.filter(
112
+ (item) => item.state.status === "aborted",
113
+ ),
114
+ }));
115
+
116
+ // Helper function to format file sizes
117
+ // biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
118
+ const formatFileSize = (bytes: number): string => {
119
+ if (bytes === 0) return "0 Bytes";
120
+ const k = 1024;
121
+ const sizes = ["Bytes", "KB", "MB", "GB"];
122
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
123
+ return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
124
+ };
125
+
126
+ // Helper function to get status icon
127
+ // biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
128
+ const getStatusIcon = (status: string): string => {
129
+ switch (status) {
130
+ case "idle":
131
+ return "⏳";
132
+ case "uploading":
133
+ return "📤";
134
+ case "success":
135
+ return "✅";
136
+ case "error":
137
+ return "❌";
138
+ case "aborted":
139
+ return "⏹️";
140
+ default:
141
+ return "❓";
142
+ }
143
+ };
144
+
145
+ // Helper function to get status color
146
+ // biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
147
+ const getStatusColor = (status: string): string => {
148
+ switch (status) {
149
+ case "idle":
150
+ return "#6c757d";
151
+ case "uploading":
152
+ return "#007bff";
153
+ case "success":
154
+ return "#28a745";
155
+ case "error":
156
+ return "#dc3545";
157
+ case "aborted":
158
+ return "#6c757d";
159
+ default:
160
+ return "#6c757d";
161
+ }
162
+ };
163
+ </script>
164
+
165
+ <template>
166
+ <div class="upload-list">
167
+ <slot :items="filteredItems" :items-by-status="itemsByStatus">
168
+ <!-- Default rendering: simple list of upload items -->
169
+ <div
170
+ v-for="(item, index) in filteredItems"
171
+ :key="item.id"
172
+ class="upload-list__item"
173
+ :class="`upload-list__item--${item.state.status}`"
174
+ >
175
+ <slot
176
+ name="item"
177
+ :item="item"
178
+ :index="index"
179
+ :is-uploading="item.state.status === 'uploading'"
180
+ :is-success="item.state.status === 'success'"
181
+ :is-error="item.state.status === 'error'"
182
+ :format-file-size="formatFileSize"
183
+ >
184
+ <!-- Default item template -->
185
+ <div class="upload-list__item-header">
186
+ <span class="upload-list__item-icon">
187
+ {{ getStatusIcon(item.state.status) }}
188
+ </span>
189
+ <span class="upload-list__item-name">
190
+ {{ isBrowserFile(item.file) ? item.file.name : 'File' }}
191
+ </span>
192
+ <span
193
+ class="upload-list__item-status"
194
+ :style="{ color: getStatusColor(item.state.status) }"
195
+ >
196
+ {{ item.state.status.toUpperCase() }}
197
+ </span>
198
+ </div>
199
+
200
+ <div v-if="item.state.totalBytes" class="upload-list__item-size">
201
+ {{ formatFileSize(item.state.totalBytes) }}
202
+ </div>
203
+
204
+ <div v-if="item.state.status === 'uploading'" class="upload-list__item-progress">
205
+ <div class="upload-list__progress-bar">
206
+ <div
207
+ class="upload-list__progress-fill"
208
+ :style="{ width: `${item.state.progress}%` }"
209
+ />
210
+ </div>
211
+ <span class="upload-list__progress-text">{{ item.state.progress }}%</span>
212
+ </div>
213
+
214
+ <div v-if="item.state.status === 'error' && item.state.error" class="upload-list__item-error">
215
+ {{ item.state.error.message }}
216
+ </div>
217
+ </slot>
218
+ </div>
219
+ </slot>
220
+ </div>
221
+ </template>
222
+
223
+ <style scoped>
224
+ .upload-list {
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: 0.5rem;
228
+ }
229
+
230
+ .upload-list__item {
231
+ padding: 0.75rem;
232
+ border: 1px solid #e0e0e0;
233
+ border-radius: 0.375rem;
234
+ background-color: #fff;
235
+ }
236
+
237
+ .upload-list__item-header {
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 0.5rem;
241
+ margin-bottom: 0.5rem;
242
+ }
243
+
244
+ .upload-list__item-icon {
245
+ font-size: 1rem;
246
+ }
247
+
248
+ .upload-list__item-name {
249
+ flex: 1;
250
+ font-weight: 500;
251
+ overflow: hidden;
252
+ text-overflow: ellipsis;
253
+ white-space: nowrap;
254
+ }
255
+
256
+ .upload-list__item-status {
257
+ font-size: 0.75rem;
258
+ font-weight: 600;
259
+ text-transform: uppercase;
260
+ }
261
+
262
+ .upload-list__item-size {
263
+ font-size: 0.75rem;
264
+ color: #666;
265
+ margin-bottom: 0.5rem;
266
+ }
267
+
268
+ .upload-list__item-progress {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 0.5rem;
272
+ }
273
+
274
+ .upload-list__progress-bar {
275
+ flex: 1;
276
+ height: 0.375rem;
277
+ background-color: #e0e0e0;
278
+ border-radius: 0.1875rem;
279
+ overflow: hidden;
280
+ }
281
+
282
+ .upload-list__progress-fill {
283
+ height: 100%;
284
+ background-color: #007bff;
285
+ transition: width 0.2s ease;
286
+ }
287
+
288
+ .upload-list__progress-text {
289
+ font-size: 0.75rem;
290
+ color: #666;
291
+ min-width: 3rem;
292
+ text-align: right;
293
+ }
294
+
295
+ .upload-list__item-error {
296
+ margin-top: 0.5rem;
297
+ padding: 0.5rem;
298
+ background-color: #f8d7da;
299
+ color: #721c24;
300
+ font-size: 0.75rem;
301
+ border-radius: 0.25rem;
302
+ }
303
+ </style>
@@ -0,0 +1,254 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * UploadZone - A flexible file upload component with drag-and-drop support
4
+ *
5
+ * Provides a drag-and-drop zone and file picker for uploading files. Supports both single
6
+ * and multiple file uploads with validation. Emits events for file selection and upload events.
7
+ *
8
+ * @component
9
+ * @example
10
+ * // Basic single file upload
11
+ * <UploadZone @file-select="handleFiles" />
12
+ *
13
+ * @example
14
+ * // Multiple files with validation
15
+ * <UploadZone
16
+ * multiple
17
+ * accept={["image/*"]}
18
+ * :max-file-size="10 * 1024 * 1024"
19
+ * @file-select="handleFiles"
20
+ * @validation-error="handleErrors"
21
+ * >
22
+ * <template #default="{ isDragging, errors, openFilePicker }">
23
+ * <div :class="{ dragging: isDragging }" @click="openFilePicker">
24
+ * <p>{{ isDragging ? 'Drop files here' : 'Click or drag files here' }}</p>
25
+ * <div v-if="errors.length">
26
+ * <p v-for="error in errors" :key="error">{{ error }}</p>
27
+ * </div>
28
+ * </div>
29
+ * </template>
30
+ * </UploadZone>
31
+ *
32
+ * @emits file-select - When files are selected/dropped
33
+ * @emits upload-start - When upload begins
34
+ * @emits validation-error - When validation fails
35
+ */
36
+ import type { UploadOptions } from "@uploadista/client-browser";
37
+ import { computed, ref } from "vue";
38
+ import type { MultiUploadOptions } from "../composables";
39
+ import { useDragDrop, useMultiUpload, useUpload } from "../composables";
40
+
41
+ /**
42
+ * Props for the UploadZone component
43
+ * @property {string[]} accept - Accepted file types (MIME types or file extensions)
44
+ * @property {boolean} multiple - Whether to allow multiple files (default: true)
45
+ * @property {boolean} disabled - Whether the upload zone is disabled (default: false)
46
+ * @property {number} maxFileSize - Maximum file size in bytes
47
+ * @property {Function} validator - Custom validation function for files
48
+ * @property {MultiUploadOptions} multiUploadOptions - Multi-upload options (only used when multiple=true)
49
+ * @property {UploadOptions} uploadOptions - Single upload options (only used when multiple=false)
50
+ */
51
+ export interface UploadZoneProps {
52
+ /**
53
+ * Accepted file types (MIME types or file extensions)
54
+ */
55
+ accept?: string[];
56
+
57
+ /**
58
+ * Whether to allow multiple files
59
+ */
60
+ multiple?: boolean;
61
+
62
+ /**
63
+ * Whether the upload zone is disabled
64
+ */
65
+ disabled?: boolean;
66
+
67
+ /**
68
+ * Maximum file size in bytes
69
+ */
70
+ maxFileSize?: number;
71
+
72
+ /**
73
+ * Custom validation function for files
74
+ */
75
+ validator?: (files: File[]) => string[] | null;
76
+
77
+ /**
78
+ * Multi-upload options (only used when multiple=true)
79
+ */
80
+ multiUploadOptions?: MultiUploadOptions;
81
+
82
+ /**
83
+ * Single upload options (only used when multiple=false)
84
+ */
85
+ uploadOptions?: UploadOptions;
86
+ }
87
+
88
+ const props = withDefaults(defineProps<UploadZoneProps>(), {
89
+ multiple: true,
90
+ disabled: false,
91
+ });
92
+
93
+ const emit = defineEmits<{
94
+ "file-select": [files: File[]];
95
+ "upload-start": [files: File[]];
96
+ "validation-error": [errors: string[]];
97
+ }>();
98
+
99
+ defineSlots<{
100
+ // biome-ignore lint/suspicious/noExplicitAny: Vue slot definition requires any
101
+ default(props: {
102
+ isDragging: boolean;
103
+ isOver: boolean;
104
+ isUploading: boolean;
105
+ errors: string[];
106
+ openFilePicker: () => void;
107
+ }): any;
108
+ }>();
109
+
110
+ // Initialize upload composables
111
+ const singleUpload = props.multiple
112
+ ? null
113
+ : useUpload(props.uploadOptions || {});
114
+ const multiUpload = props.multiple
115
+ ? useMultiUpload(props.multiUploadOptions || {})
116
+ : null;
117
+
118
+ // Handle files received from drag-drop or file picker
119
+ const handleFilesReceived = (files: File[]) => {
120
+ emit("file-select", files);
121
+ emit("upload-start", files);
122
+
123
+ if (props.multiple && multiUpload) {
124
+ multiUpload.addFiles(files);
125
+ setTimeout(() => multiUpload.startAll(), 0);
126
+ } else if (!props.multiple && singleUpload && files[0]) {
127
+ singleUpload.upload(files[0]);
128
+ }
129
+ };
130
+
131
+ // Handle validation errors
132
+ const handleValidationError = (errors: string[]) => {
133
+ emit("validation-error", errors);
134
+ };
135
+
136
+ // Initialize drag-drop
137
+ const dragDrop = useDragDrop({
138
+ accept: props.accept,
139
+ multiple: props.multiple,
140
+ maxFileSize: props.maxFileSize,
141
+ validator: props.validator,
142
+ onFilesReceived: handleFilesReceived,
143
+ onValidationError: handleValidationError,
144
+ });
145
+
146
+ // File input ref
147
+ const fileInputRef = ref<HTMLInputElement>();
148
+
149
+ // Open file picker
150
+ // biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
151
+ const openFilePicker = () => {
152
+ if (!props.disabled) {
153
+ fileInputRef.value?.click();
154
+ }
155
+ };
156
+
157
+ // Computed states
158
+ // biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
159
+ const isActive = computed(
160
+ () => dragDrop.state.value.isDragging || dragDrop.state.value.isOver,
161
+ );
162
+
163
+ // biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
164
+ const isUploading = computed(() => {
165
+ if (props.multiple && multiUpload) {
166
+ return multiUpload.state.value.isUploading;
167
+ } else if (!props.multiple && singleUpload) {
168
+ return singleUpload.state.value.status === "uploading";
169
+ }
170
+ return false;
171
+ });
172
+ </script>
173
+
174
+ <template>
175
+ <div
176
+ class="upload-zone"
177
+ :class="{ 'upload-zone--active': isActive, 'upload-zone--disabled': disabled }"
178
+ @dragenter="!disabled && dragDrop.onDragEnter"
179
+ @dragover="!disabled && dragDrop.onDragOver"
180
+ @dragleave="!disabled && dragDrop.onDragLeave"
181
+ @drop="!disabled && dragDrop.onDrop"
182
+ @click="openFilePicker"
183
+ role="button"
184
+ :tabindex="disabled ? -1 : 0"
185
+ :aria-disabled="disabled"
186
+ :aria-label="multiple ? 'Upload multiple files' : 'Upload a file'"
187
+ @keydown.enter="openFilePicker"
188
+ @keydown.space.prevent="openFilePicker"
189
+ >
190
+ <slot
191
+ :is-dragging="dragDrop.state.value.isDragging"
192
+ :is-over="dragDrop.state.value.isOver"
193
+ :is-uploading="isUploading"
194
+ :errors="[...dragDrop.state.value.errors]"
195
+ :open-file-picker="openFilePicker"
196
+ >
197
+ <!-- Default slot content -->
198
+ <div class="upload-zone__content">
199
+ <p v-if="dragDrop.state.value.isDragging">
200
+ {{ multiple ? 'Drop files here...' : 'Drop file here...' }}
201
+ </p>
202
+ <p v-else>
203
+ {{ multiple ? 'Drag files here or click to select' : 'Drag a file here or click to select' }}
204
+ </p>
205
+
206
+ <div v-if="dragDrop.state.value.errors.length > 0" class="upload-zone__errors">
207
+ <p v-for="(error, index) in dragDrop.state.value.errors" :key="index">
208
+ {{ error }}
209
+ </p>
210
+ </div>
211
+ </div>
212
+ </slot>
213
+
214
+ <input
215
+ ref="fileInputRef"
216
+ type="file"
217
+ :multiple="dragDrop.inputProps.value.multiple"
218
+ :accept="dragDrop.inputProps.value.accept"
219
+ :disabled="disabled"
220
+ @change="dragDrop.onInputChange"
221
+ style="display: none"
222
+ aria-hidden="true"
223
+ />
224
+ </div>
225
+ </template>
226
+
227
+ <style scoped>
228
+ .upload-zone {
229
+ cursor: pointer;
230
+ user-select: none;
231
+ }
232
+
233
+ .upload-zone--disabled {
234
+ cursor: not-allowed;
235
+ opacity: 0.6;
236
+ }
237
+
238
+ .upload-zone__content {
239
+ display: flex;
240
+ flex-direction: column;
241
+ align-items: center;
242
+ justify-content: center;
243
+ }
244
+
245
+ .upload-zone__errors {
246
+ margin-top: 0.5rem;
247
+ color: #dc3545;
248
+ font-size: 0.875rem;
249
+ }
250
+
251
+ .upload-zone__errors p {
252
+ margin: 0.25rem 0;
253
+ }
254
+ </style>
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Vue 3 Components for Uploadista
3
+ *
4
+ * These components provide ready-to-use UI elements for file uploads and flow processing.
5
+ * They integrate with the Uploadista composables and are designed to be flexible and customizable.
6
+ */
7
+
8
+ export { default as FlowUploadList } from "./FlowUploadList.vue";
9
+ export { default as FlowUploadZone } from "./FlowUploadZone.vue";
10
+ export { default as UploadList } from "./UploadList.vue";
11
+ export { default as UploadZone } from "./UploadZone.vue";
@@ -0,0 +1,44 @@
1
+ // Plugin
2
+
3
+ export type { UploadistaPluginOptions } from "./plugin";
4
+ export { createUploadistaPlugin, UPLOADISTA_CLIENT_KEY } from "./plugin";
5
+ export type {
6
+ DragDropOptions,
7
+ DragDropState,
8
+ } from "./useDragDrop";
9
+ // Drag and drop composable
10
+ export { useDragDrop } from "./useDragDrop";
11
+
12
+ export type {
13
+ FlowUploadState,
14
+ FlowUploadStatus,
15
+ } from "./useFlowUpload";
16
+ // Flow upload composables
17
+ export { useFlowUpload } from "./useFlowUpload";
18
+ export { useMultiFlowUpload } from "./useMultiFlowUpload";
19
+ export type {
20
+ MultiUploadOptions,
21
+ MultiUploadState,
22
+ UploadItem,
23
+ } from "./useMultiUpload";
24
+ export { useMultiUpload } from "./useMultiUpload";
25
+ export type {
26
+ ChunkMetrics,
27
+ PerformanceInsights,
28
+ UploadInput,
29
+ UploadMetrics,
30
+ UploadSessionMetrics,
31
+ UploadState,
32
+ UploadStatus,
33
+ } from "./useUpload";
34
+ // Upload composables
35
+ export { useUpload } from "./useUpload";
36
+ export type { UseUploadistaClientReturn } from "./useUploadistaClient";
37
+ // Core client composable
38
+ export { useUploadistaClient } from "./useUploadistaClient";
39
+ export type {
40
+ FileUploadMetrics,
41
+ UseUploadMetricsOptions,
42
+ } from "./useUploadMetrics";
43
+ // Metrics composable
44
+ export { useUploadMetrics } from "./useUploadMetrics";
@@ -0,0 +1,76 @@
1
+ import {
2
+ createUploadistaClient,
3
+ type UploadistaClientOptions,
4
+ type UploadistaEvent,
5
+ } from "@uploadista/client-browser";
6
+ import type { App, InjectionKey, Ref } from "vue";
7
+ import { ref } from "vue";
8
+
9
+ export interface UploadistaPluginOptions extends UploadistaClientOptions {
10
+ /**
11
+ * Global event handler for all upload and flow events from this client
12
+ */
13
+ onEvent?: UploadistaClientOptions["onEvent"];
14
+ }
15
+
16
+ export const UPLOADISTA_CLIENT_KEY: InjectionKey<
17
+ ReturnType<typeof createUploadistaClient>
18
+ > = Symbol("uploadista-client");
19
+
20
+ export const UPLOADISTA_EVENT_SUBSCRIBERS_KEY: InjectionKey<
21
+ Ref<Set<(event: UploadistaEvent) => void>>
22
+ > = Symbol("uploadista-event-subscribers");
23
+
24
+ /**
25
+ * Vue plugin for providing Uploadista client instance globally.
26
+ * Uses Vue's provide/inject pattern to make the client available
27
+ * throughout the component tree.
28
+ *
29
+ * @param options - Uploadista client configuration options
30
+ * @returns Vue plugin object
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { createApp } from 'vue';
35
+ * import { createUploadistaPlugin } from '@uploadista/vue';
36
+ * import App from './App.vue';
37
+ *
38
+ * const app = createApp(App);
39
+ *
40
+ * app.use(createUploadistaPlugin({
41
+ * baseUrl: 'https://api.example.com',
42
+ * storageId: 'my-storage',
43
+ * chunkSize: 1024 * 1024, // 1MB
44
+ * storeFingerprintForResuming: true,
45
+ * onEvent: (event) => {
46
+ * console.log('Upload event:', event);
47
+ * }
48
+ * }));
49
+ *
50
+ * app.mount('#app');
51
+ * ```
52
+ */
53
+ export function createUploadistaPlugin(options: UploadistaPluginOptions) {
54
+ return {
55
+ install(app: App) {
56
+ // Create a shared set of event subscribers
57
+ const eventSubscribers = ref(new Set<(event: UploadistaEvent) => void>());
58
+
59
+ const client = createUploadistaClient({
60
+ ...options,
61
+ onEvent: (event) => {
62
+ // Dispatch to all subscribers registered via subscribeToEvents
63
+ eventSubscribers.value.forEach((subscriber) => {
64
+ subscriber(event);
65
+ });
66
+
67
+ // Call the original onEvent handler if provided
68
+ options.onEvent?.(event);
69
+ },
70
+ });
71
+
72
+ app.provide(UPLOADISTA_CLIENT_KEY, client);
73
+ app.provide(UPLOADISTA_EVENT_SUBSCRIBERS_KEY, eventSubscribers);
74
+ },
75
+ };
76
+ }