@makolabs/ripple 1.13.1 → 2.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.
- package/dist/elements/file-upload/FileUpload.svelte +278 -41
- package/dist/elements/file-upload/FileUpload.svelte.d.ts +1 -1
- package/dist/elements/file-upload/file-upload-types.d.ts +97 -16
- package/dist/elements/file-upload/file-upload.d.ts +169 -0
- package/dist/elements/file-upload/file-upload.js +64 -0
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,32 +1,111 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../../helper/cls.js';
|
|
3
|
-
import
|
|
3
|
+
import { fileUpload } from './file-upload.js';
|
|
4
|
+
import Button from '../../button/Button.svelte';
|
|
5
|
+
import Spinner from '../spinner/Spinner.svelte';
|
|
6
|
+
import { Size, Color } from '../../variants.js';
|
|
7
|
+
import type { FileUploadProps, StagedFile } from '../../index.js';
|
|
4
8
|
|
|
5
9
|
let {
|
|
6
10
|
allowedMimeTypes = [],
|
|
7
|
-
maxFiles =
|
|
11
|
+
maxFiles = 1,
|
|
8
12
|
maxSize,
|
|
13
|
+
size = 'xl',
|
|
9
14
|
class: className = '',
|
|
10
15
|
dropzoneClass = '',
|
|
11
16
|
id = 'file-upload',
|
|
12
17
|
onfiles,
|
|
13
|
-
uploadContent
|
|
18
|
+
uploadContent,
|
|
19
|
+
files = $bindable<StagedFile[]>([]),
|
|
20
|
+
clearAfterUpload = true,
|
|
21
|
+
uploadButtonLabel = 'Upload All',
|
|
22
|
+
clearButtonLabel = 'Clear All',
|
|
23
|
+
filesListLabel = 'Selected files',
|
|
24
|
+
listMaxHeight = 'max-h-64',
|
|
25
|
+
header
|
|
14
26
|
}: FileUploadProps = $props();
|
|
15
27
|
|
|
28
|
+
const slots = $derived(fileUpload({ size }));
|
|
29
|
+
|
|
30
|
+
/** True when the component operates as a multi-file staging uploader. */
|
|
31
|
+
const isMulti = $derived(maxFiles > 1);
|
|
32
|
+
|
|
33
|
+
/** Hard-disabled dropzone (maxFiles <= 0). */
|
|
16
34
|
const disabled = $derived(maxFiles <= 0);
|
|
17
35
|
|
|
36
|
+
/** Soft-disabled dropzone when the staging list is at capacity. */
|
|
37
|
+
const dropzoneFull = $derived(isMulti && files.length >= maxFiles);
|
|
38
|
+
|
|
39
|
+
/** Effective "can drop files" flag. */
|
|
40
|
+
const dropzoneEnabled = $derived(!disabled && !dropzoneFull);
|
|
41
|
+
|
|
18
42
|
let isDragging = $state(false);
|
|
19
43
|
let inputRef: HTMLInputElement;
|
|
20
44
|
|
|
21
|
-
function
|
|
22
|
-
if (
|
|
23
|
-
|
|
45
|
+
function makeId(): string {
|
|
46
|
+
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
|
47
|
+
return crypto.randomUUID();
|
|
48
|
+
}
|
|
49
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleIncoming(newFiles: FileList | File[]) {
|
|
53
|
+
if (!dropzoneEnabled) return;
|
|
54
|
+
const arr = Array.from(newFiles);
|
|
55
|
+
if (!isMulti) {
|
|
56
|
+
// Single-file mode: fire-and-forget, same as pre-v2 FileUpload.
|
|
57
|
+
onfiles?.(arr);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Multi-file mode: cap and stage
|
|
61
|
+
const room = Math.max(0, maxFiles - files.length);
|
|
62
|
+
const accepted = arr.slice(0, room);
|
|
63
|
+
const toAdd: StagedFile[] = accepted.map((file) => ({
|
|
64
|
+
id: makeId(),
|
|
65
|
+
file,
|
|
66
|
+
status: 'ready' as const
|
|
67
|
+
}));
|
|
68
|
+
files = [...files, ...toAdd];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleRemove(fileId: string) {
|
|
72
|
+
files = files.filter((f) => f.id !== fileId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handleClearAll() {
|
|
76
|
+
files = [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const readyCount = $derived(files.filter((f) => !f.status || f.status === 'ready').length);
|
|
80
|
+
const hasAnyFile = $derived(files.length > 0);
|
|
81
|
+
const anyUploading = $derived(files.some((f) => f.status === 'uploading'));
|
|
82
|
+
const canUpload = $derived(readyCount > 0 && !anyUploading && !!onfiles);
|
|
83
|
+
|
|
84
|
+
function handleUploadAll() {
|
|
85
|
+
if (!onfiles) return;
|
|
86
|
+
const rawFiles = files.filter((f) => !f.status || f.status === 'ready').map((f) => f.file);
|
|
87
|
+
if (rawFiles.length === 0) return;
|
|
88
|
+
onfiles(rawFiles);
|
|
89
|
+
if (clearAfterUpload) {
|
|
90
|
+
files = [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleRetry(fileId: string) {
|
|
95
|
+
const target = files.find((f) => f.id === fileId);
|
|
96
|
+
if (!target || !onfiles) return;
|
|
97
|
+
files = files.map((f) =>
|
|
98
|
+
f.id === fileId
|
|
99
|
+
? { ...f, status: 'ready' as const, error: undefined, progress: undefined }
|
|
100
|
+
: f
|
|
101
|
+
);
|
|
102
|
+
onfiles([target.file]);
|
|
24
103
|
}
|
|
25
104
|
|
|
26
105
|
function handleDragEnter(e: DragEvent) {
|
|
27
106
|
e.preventDefault();
|
|
28
107
|
e.stopPropagation();
|
|
29
|
-
if (
|
|
108
|
+
if (dropzoneEnabled) isDragging = true;
|
|
30
109
|
}
|
|
31
110
|
|
|
32
111
|
function handleDragLeave(e: DragEvent) {
|
|
@@ -39,15 +118,15 @@
|
|
|
39
118
|
e.preventDefault();
|
|
40
119
|
e.stopPropagation();
|
|
41
120
|
isDragging = false;
|
|
42
|
-
if (
|
|
43
|
-
|
|
121
|
+
if (dropzoneEnabled && e.dataTransfer?.files) {
|
|
122
|
+
handleIncoming(e.dataTransfer.files);
|
|
44
123
|
}
|
|
45
124
|
}
|
|
46
125
|
|
|
47
126
|
function handleInputChange(e: Event) {
|
|
48
127
|
const input = e.target as HTMLInputElement;
|
|
49
128
|
if (input.files) {
|
|
50
|
-
|
|
129
|
+
handleIncoming(input.files);
|
|
51
130
|
input.value = '';
|
|
52
131
|
}
|
|
53
132
|
}
|
|
@@ -55,7 +134,7 @@
|
|
|
55
134
|
function handleDragOver(e: DragEvent) {
|
|
56
135
|
e.preventDefault();
|
|
57
136
|
e.stopPropagation();
|
|
58
|
-
if (
|
|
137
|
+
if (dropzoneEnabled) isDragging = true;
|
|
59
138
|
}
|
|
60
139
|
|
|
61
140
|
function formatFileSize(bytes: number): string {
|
|
@@ -67,15 +146,19 @@
|
|
|
67
146
|
}
|
|
68
147
|
</script>
|
|
69
148
|
|
|
70
|
-
<div class={cn('w-full', className)}>
|
|
149
|
+
<div class={cn('flex w-full flex-col gap-4', className)}>
|
|
150
|
+
{#if header}
|
|
151
|
+
{@render header()}
|
|
152
|
+
{/if}
|
|
153
|
+
|
|
71
154
|
<label
|
|
72
155
|
class={cn(
|
|
73
|
-
|
|
156
|
+
slots.dropzone(),
|
|
74
157
|
{
|
|
75
158
|
'border-primary-400 bg-primary-50': isDragging,
|
|
76
159
|
'border-default-200 bg-white': !isDragging,
|
|
77
|
-
'cursor-not-allowed opacity-50':
|
|
78
|
-
'hover:bg-default-50 cursor-pointer':
|
|
160
|
+
'cursor-not-allowed opacity-50': !dropzoneEnabled,
|
|
161
|
+
'hover:bg-default-50 cursor-pointer': dropzoneEnabled
|
|
79
162
|
},
|
|
80
163
|
dropzoneClass
|
|
81
164
|
)}
|
|
@@ -89,21 +172,16 @@
|
|
|
89
172
|
type="file"
|
|
90
173
|
bind:this={inputRef}
|
|
91
174
|
accept={allowedMimeTypes.join(',')}
|
|
92
|
-
multiple={maxFiles
|
|
93
|
-
{
|
|
175
|
+
multiple={maxFiles > 1}
|
|
176
|
+
disabled={!dropzoneEnabled}
|
|
94
177
|
class="hidden"
|
|
95
178
|
onchange={handleInputChange}
|
|
96
179
|
{id}
|
|
97
180
|
/>
|
|
98
181
|
|
|
99
|
-
<div class=
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<svg
|
|
103
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
104
|
-
class="text-primary-500 size-12"
|
|
105
|
-
viewBox="0 0 24 24"
|
|
106
|
-
>
|
|
182
|
+
<div class={slots.content()}>
|
|
183
|
+
<div class={slots.iconWrapper()} data-fileupload-icon-wrapper="">
|
|
184
|
+
<svg xmlns="http://www.w3.org/2000/svg" class={slots.icon()} viewBox="0 0 24 24">
|
|
107
185
|
<path
|
|
108
186
|
fill="currentColor"
|
|
109
187
|
d="M11 14.2V6.8l-3.7 3.7L6 9l6-6l6 6l-1.3 1.4L13 6.8v7.4zm-5 4.3h12v2H6z"
|
|
@@ -111,25 +189,184 @@
|
|
|
111
189
|
</svg>
|
|
112
190
|
</div>
|
|
113
191
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{:else}
|
|
121
|
-
{@render uploadContent()}
|
|
122
|
-
{/if}
|
|
123
|
-
|
|
124
|
-
<!-- File Type Info -->
|
|
125
|
-
<div class="text-default-500 text-xs">
|
|
126
|
-
Supported Format: {allowedMimeTypes.length ? allowedMimeTypes.join(', ') : 'SVG, JPG, PNG'}
|
|
127
|
-
{#if maxSize}
|
|
128
|
-
({formatFileSize(maxSize)} each)
|
|
192
|
+
<div class={slots.textBlock()}>
|
|
193
|
+
{#if !uploadContent}
|
|
194
|
+
<div class={slots.mainText()}>
|
|
195
|
+
<span class="text-primary-500 font-medium">Click here</span>
|
|
196
|
+
<span class="text-default-600"> to upload your file or drag and drop.</span>
|
|
197
|
+
</div>
|
|
129
198
|
{:else}
|
|
130
|
-
(
|
|
199
|
+
{@render uploadContent()}
|
|
131
200
|
{/if}
|
|
201
|
+
|
|
202
|
+
<div class={slots.hintText()}>
|
|
203
|
+
Supported Format: {allowedMimeTypes.length
|
|
204
|
+
? allowedMimeTypes.join(', ')
|
|
205
|
+
: 'SVG, JPG, PNG'}
|
|
206
|
+
{#if maxSize}
|
|
207
|
+
({formatFileSize(maxSize)} each)
|
|
208
|
+
{:else}
|
|
209
|
+
(10MB each)
|
|
210
|
+
{/if}
|
|
211
|
+
</div>
|
|
132
212
|
</div>
|
|
133
213
|
</div>
|
|
134
214
|
</label>
|
|
215
|
+
|
|
216
|
+
{#if isMulti && hasAnyFile}
|
|
217
|
+
<div class="flex flex-col gap-2">
|
|
218
|
+
<div class="flex items-center justify-between">
|
|
219
|
+
<span class="text-default-700 text-sm font-medium">
|
|
220
|
+
{filesListLabel} ({files.length})
|
|
221
|
+
</span>
|
|
222
|
+
<div class="flex gap-2">
|
|
223
|
+
<Button
|
|
224
|
+
size={Size.XS}
|
|
225
|
+
variant="outline"
|
|
226
|
+
color={Color.DEFAULT}
|
|
227
|
+
onclick={handleClearAll}
|
|
228
|
+
disabled={anyUploading}
|
|
229
|
+
>
|
|
230
|
+
{clearButtonLabel}
|
|
231
|
+
</Button>
|
|
232
|
+
<Button
|
|
233
|
+
size={Size.XS}
|
|
234
|
+
color={Color.PRIMARY}
|
|
235
|
+
onclick={handleUploadAll}
|
|
236
|
+
disabled={!canUpload}
|
|
237
|
+
>
|
|
238
|
+
{uploadButtonLabel}{readyCount > 0 ? ` (${readyCount})` : ''}
|
|
239
|
+
</Button>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<ul
|
|
244
|
+
class={cn(
|
|
245
|
+
'border-default-200 divide-default-100 divide-y overflow-y-auto rounded-lg border',
|
|
246
|
+
listMaxHeight
|
|
247
|
+
)}
|
|
248
|
+
>
|
|
249
|
+
{#each files as stagedFile (stagedFile.id)}
|
|
250
|
+
{@const status = stagedFile.status ?? 'ready'}
|
|
251
|
+
<li
|
|
252
|
+
class={cn('flex items-center gap-3 px-3 py-2', status === 'success' && 'opacity-60')}
|
|
253
|
+
data-fileupload-row=""
|
|
254
|
+
data-status={status}
|
|
255
|
+
>
|
|
256
|
+
{#if status === 'ready'}
|
|
257
|
+
<span
|
|
258
|
+
class="bg-default-100 text-default-500 flex size-7 shrink-0 items-center justify-center rounded-full"
|
|
259
|
+
aria-hidden="true"
|
|
260
|
+
>
|
|
261
|
+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
262
|
+
<path
|
|
263
|
+
stroke-linecap="round"
|
|
264
|
+
stroke-linejoin="round"
|
|
265
|
+
stroke-width="2"
|
|
266
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
267
|
+
/>
|
|
268
|
+
</svg>
|
|
269
|
+
</span>
|
|
270
|
+
{:else if status === 'uploading'}
|
|
271
|
+
<div class="flex size-7 shrink-0 items-center justify-center">
|
|
272
|
+
<Spinner size={Size.SM} color={Color.PRIMARY} />
|
|
273
|
+
</div>
|
|
274
|
+
{:else if status === 'success'}
|
|
275
|
+
<span
|
|
276
|
+
class="bg-success-100 text-success-600 flex size-7 shrink-0 items-center justify-center rounded-full"
|
|
277
|
+
aria-hidden="true"
|
|
278
|
+
>
|
|
279
|
+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
280
|
+
<path
|
|
281
|
+
stroke-linecap="round"
|
|
282
|
+
stroke-linejoin="round"
|
|
283
|
+
stroke-width="3"
|
|
284
|
+
d="M5 13l4 4L19 7"
|
|
285
|
+
/>
|
|
286
|
+
</svg>
|
|
287
|
+
</span>
|
|
288
|
+
{:else if status === 'error'}
|
|
289
|
+
<span
|
|
290
|
+
class="bg-danger-100 text-danger-600 flex size-7 shrink-0 items-center justify-center rounded-full"
|
|
291
|
+
aria-hidden="true"
|
|
292
|
+
>
|
|
293
|
+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
294
|
+
<path
|
|
295
|
+
stroke-linecap="round"
|
|
296
|
+
stroke-linejoin="round"
|
|
297
|
+
stroke-width="3"
|
|
298
|
+
d="M6 18L18 6M6 6l12 12"
|
|
299
|
+
/>
|
|
300
|
+
</svg>
|
|
301
|
+
</span>
|
|
302
|
+
{/if}
|
|
303
|
+
|
|
304
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
305
|
+
<span class="text-default-900 truncate text-sm font-medium">
|
|
306
|
+
{stagedFile.file.name}
|
|
307
|
+
</span>
|
|
308
|
+
<span class="text-default-500 text-xs">
|
|
309
|
+
{formatFileSize(stagedFile.file.size)}
|
|
310
|
+
{#if stagedFile.error}
|
|
311
|
+
<span class="text-danger-600"> · {stagedFile.error}</span>
|
|
312
|
+
{/if}
|
|
313
|
+
</span>
|
|
314
|
+
{#if status === 'uploading' && stagedFile.progress !== undefined}
|
|
315
|
+
<div class="bg-default-100 mt-1 h-1 w-full overflow-hidden rounded-full">
|
|
316
|
+
<div
|
|
317
|
+
data-fileupload-progress=""
|
|
318
|
+
class="bg-primary-500 h-full transition-all duration-200"
|
|
319
|
+
style="width: {Math.max(0, Math.min(100, stagedFile.progress))}%"
|
|
320
|
+
></div>
|
|
321
|
+
</div>
|
|
322
|
+
{/if}
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{#if status === 'error'}
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onclick={() => handleRetry(stagedFile.id)}
|
|
329
|
+
class="text-default-500 hover:bg-primary-50 hover:text-primary-600 shrink-0 cursor-pointer rounded p-1 transition-colors"
|
|
330
|
+
aria-label="Retry upload for {stagedFile.file.name}"
|
|
331
|
+
title="Retry"
|
|
332
|
+
>
|
|
333
|
+
<svg
|
|
334
|
+
class="size-4"
|
|
335
|
+
fill="none"
|
|
336
|
+
viewBox="0 0 24 24"
|
|
337
|
+
stroke="currentColor"
|
|
338
|
+
stroke-width="2"
|
|
339
|
+
stroke-linecap="round"
|
|
340
|
+
stroke-linejoin="round"
|
|
341
|
+
>
|
|
342
|
+
<polyline points="23 4 23 10 17 10" />
|
|
343
|
+
<polyline points="1 20 1 14 7 14" />
|
|
344
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
|
345
|
+
</svg>
|
|
346
|
+
</button>
|
|
347
|
+
{/if}
|
|
348
|
+
|
|
349
|
+
{#if status !== 'uploading' && status !== 'success'}
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
onclick={() => handleRemove(stagedFile.id)}
|
|
353
|
+
class="text-default-400 hover:bg-default-100 hover:text-danger-500 shrink-0 cursor-pointer rounded p-1 transition-colors"
|
|
354
|
+
aria-label="Remove {stagedFile.file.name}"
|
|
355
|
+
title="Remove"
|
|
356
|
+
>
|
|
357
|
+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
358
|
+
<path
|
|
359
|
+
stroke-linecap="round"
|
|
360
|
+
stroke-linejoin="round"
|
|
361
|
+
stroke-width="2"
|
|
362
|
+
d="M6 18L18 6M6 6l12 12"
|
|
363
|
+
/>
|
|
364
|
+
</svg>
|
|
365
|
+
</button>
|
|
366
|
+
{/if}
|
|
367
|
+
</li>
|
|
368
|
+
{/each}
|
|
369
|
+
</ul>
|
|
370
|
+
</div>
|
|
371
|
+
{/if}
|
|
135
372
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { FileUploadProps } from '../../index.js';
|
|
2
|
-
declare const FileUpload: import("svelte").Component<FileUploadProps, {}, "">;
|
|
2
|
+
declare const FileUpload: import("svelte").Component<FileUploadProps, {}, "files">;
|
|
3
3
|
type FileUpload = ReturnType<typeof FileUpload>;
|
|
4
4
|
export default FileUpload;
|
|
@@ -1,42 +1,123 @@
|
|
|
1
1
|
import type { ClassValue } from 'tailwind-variants';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
export type FileUploadSize = 'xl' | 'md' | 'sm';
|
|
4
|
+
/**
|
|
5
|
+
* A file held in a multi-file `FileUpload`'s staging list. Either purely
|
|
6
|
+
* internal (the component owns the state) or bindable (the caller owns the
|
|
7
|
+
* state and can mutate `status` / `progress` / `error` during upload for
|
|
8
|
+
* per-file visual feedback).
|
|
9
|
+
*
|
|
10
|
+
* Only used when `maxFiles > 1`.
|
|
11
|
+
*/
|
|
12
|
+
export interface StagedFile {
|
|
13
|
+
/** Stable client-generated id for list keys and caller-side updates. */
|
|
14
|
+
id: string;
|
|
15
|
+
/** The raw File object. */
|
|
16
|
+
file: File;
|
|
17
|
+
/**
|
|
18
|
+
* Row state. Defaults to 'ready'. Rich-mode callers update this during
|
|
19
|
+
* upload to drive the per-row status icon and progress bar.
|
|
20
|
+
*/
|
|
21
|
+
status?: 'ready' | 'uploading' | 'success' | 'error';
|
|
22
|
+
/** Optional 0-100 for the per-row progress bar while status='uploading'. */
|
|
23
|
+
progress?: number;
|
|
24
|
+
/** Optional error message rendered under the file name while status='error'. */
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
3
27
|
export interface FileUploadProps {
|
|
4
28
|
/**
|
|
5
|
-
* Array of allowed file MIME types or extensions
|
|
6
|
-
* @example ['image/jpeg', 'image/png']
|
|
29
|
+
* Array of allowed file MIME types or extensions.
|
|
30
|
+
* @example ['image/jpeg', 'image/png', '.csv', '.csv.gz']
|
|
7
31
|
*/
|
|
8
32
|
allowedMimeTypes?: string[];
|
|
9
33
|
/**
|
|
10
|
-
* Maximum file size in bytes
|
|
34
|
+
* Maximum file size in bytes (shown in the dropzone hint).
|
|
11
35
|
*/
|
|
12
36
|
maxSize?: number;
|
|
13
37
|
/**
|
|
14
|
-
* Maximum number of files that can be uploaded
|
|
15
|
-
*
|
|
38
|
+
* Maximum number of files that can be uploaded.
|
|
39
|
+
*
|
|
40
|
+
* - `<= 0` — dropzone is disabled
|
|
41
|
+
* - `=== 1` (default) — single-file mode. `onfiles` fires immediately
|
|
42
|
+
* when the user drops or picks a file. No staging list, no "Upload All"
|
|
43
|
+
* button.
|
|
44
|
+
* - `> 1` — multi-file staging mode. Users drop/pick files which appear in
|
|
45
|
+
* a staging list with per-row remove buttons. `onfiles` fires when the
|
|
46
|
+
* user clicks "Upload All" with all ready-status files. The staging list
|
|
47
|
+
* auto-clears on fire unless `clearAfterUpload={false}` is passed.
|
|
48
|
+
*
|
|
49
|
+
* @default 1
|
|
16
50
|
*/
|
|
17
51
|
maxFiles?: number;
|
|
18
52
|
/**
|
|
19
|
-
*
|
|
53
|
+
* Visual size variant of the dropzone.
|
|
54
|
+
*
|
|
55
|
+
* - `xl` (default) — hero square card, column layout, 48px padding +
|
|
56
|
+
* 96px circular icon. For empty-state pages or standalone dialogs.
|
|
57
|
+
* - `md` — wide rectangle, row layout, icon-left + text-right, ~80-90px
|
|
58
|
+
* tall. Form-prominent.
|
|
59
|
+
* - `sm` — compact inline rectangle, row layout, ~56-60px tall. Behaves
|
|
60
|
+
* like a single form field.
|
|
61
|
+
*
|
|
62
|
+
* @default 'xl'
|
|
20
63
|
*/
|
|
64
|
+
size?: FileUploadSize;
|
|
65
|
+
/** CSS class for the component container. */
|
|
21
66
|
class?: ClassValue;
|
|
67
|
+
/** CSS class for the dropzone area. */
|
|
68
|
+
dropzoneClass?: ClassValue;
|
|
69
|
+
/** ID for the file input element. */
|
|
70
|
+
id?: string;
|
|
22
71
|
/**
|
|
23
|
-
*
|
|
72
|
+
* Callback fired with the selected files as a normalized `File[]`.
|
|
73
|
+
*
|
|
74
|
+
* - In **single-file mode** (`maxFiles === 1`) — fires immediately on drop/pick.
|
|
75
|
+
* - In **multi-file mode** (`maxFiles > 1`) — fires when the user clicks
|
|
76
|
+
* "Upload All" with the current ready-status files from the staging list.
|
|
24
77
|
*/
|
|
25
|
-
|
|
78
|
+
onfiles?: (files: File[]) => void;
|
|
79
|
+
/** Snippet to override the default "Click here / Drag & drop" dropzone copy. */
|
|
80
|
+
uploadContent?: Snippet;
|
|
81
|
+
testId?: string;
|
|
26
82
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
83
|
+
* Bindable staging list. Only relevant when `maxFiles > 1`.
|
|
84
|
+
*
|
|
85
|
+
* - **Simple mode** (omit the prop): the component owns the staging list,
|
|
86
|
+
* auto-clears it after `onfiles` fires, and never renders status icons.
|
|
87
|
+
* Rows show `name + size + ×` only.
|
|
88
|
+
*
|
|
89
|
+
* - **Rich mode** (`bind:files`): the caller owns the list. The component
|
|
90
|
+
* writes adds/removes through the binding, and reads each file's
|
|
91
|
+
* `status` / `progress` / `error` to render per-row indicators. Staging
|
|
92
|
+
* does not auto-clear — the caller manages it.
|
|
93
|
+
*
|
|
94
|
+
* Rich mode also requires setting `clearAfterUpload={false}` to prevent
|
|
95
|
+
* the component from emptying the list after firing `onfiles`.
|
|
29
96
|
*/
|
|
30
|
-
|
|
97
|
+
files?: StagedFile[];
|
|
31
98
|
/**
|
|
32
|
-
*
|
|
99
|
+
* Whether to automatically empty the staging list after `onfiles` fires.
|
|
100
|
+
* Only relevant when `maxFiles > 1`. Set to `false` in rich mode so the
|
|
101
|
+
* caller can keep the list visible while updating per-file status during
|
|
102
|
+
* upload.
|
|
103
|
+
* @default true
|
|
33
104
|
*/
|
|
34
|
-
|
|
105
|
+
clearAfterUpload?: boolean;
|
|
106
|
+
/** Label for the primary action button. @default 'Upload All' */
|
|
107
|
+
uploadButtonLabel?: string;
|
|
108
|
+
/** Label for the secondary clear button. @default 'Clear All' */
|
|
109
|
+
clearButtonLabel?: string;
|
|
110
|
+
/** Label above the staging list. @default 'Selected files' */
|
|
111
|
+
filesListLabel?: string;
|
|
35
112
|
/**
|
|
36
|
-
*
|
|
113
|
+
* Max height of the staging list as a Tailwind class. The list scrolls
|
|
114
|
+
* internally when content exceeds this height. Use `'max-h-none'` to
|
|
115
|
+
* disable the cap entirely and let the list grow with its content.
|
|
116
|
+
* @default 'max-h-64'
|
|
37
117
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
118
|
+
listMaxHeight?: string;
|
|
119
|
+
/** Optional snippet rendered above the dropzone (for titles, selects, banners). */
|
|
120
|
+
header?: Snippet;
|
|
40
121
|
}
|
|
41
122
|
export interface FilePreviewProps {
|
|
42
123
|
files: UploadedFile[];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export declare const fileUpload: import("tailwind-variants").TVReturnType<{
|
|
2
|
+
size: {
|
|
3
|
+
/**
|
|
4
|
+
* Hero square dropzone — the original large card.
|
|
5
|
+
* 48px padding, 32px corner radius, 96px circular icon, column layout.
|
|
6
|
+
* Use for empty-state pages or standalone upload dialogs where the
|
|
7
|
+
* dropzone is the main focus of the screen.
|
|
8
|
+
*/
|
|
9
|
+
xl: {
|
|
10
|
+
dropzone: string;
|
|
11
|
+
content: string;
|
|
12
|
+
iconWrapper: string;
|
|
13
|
+
icon: string;
|
|
14
|
+
textBlock: string;
|
|
15
|
+
mainText: string;
|
|
16
|
+
hintText: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Wide medium dropzone — row layout, icon-left + text-right.
|
|
20
|
+
* ~80-90px tall, full-width. Use as a standalone form section
|
|
21
|
+
* where the upload is a primary action but the page also has
|
|
22
|
+
* other fields.
|
|
23
|
+
*/
|
|
24
|
+
md: {
|
|
25
|
+
dropzone: string;
|
|
26
|
+
content: string;
|
|
27
|
+
iconWrapper: string;
|
|
28
|
+
icon: string;
|
|
29
|
+
textBlock: string;
|
|
30
|
+
mainText: string;
|
|
31
|
+
hintText: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Compact inline dropzone — row layout, tiny.
|
|
35
|
+
* ~56-60px tall, behaves like a single form field. Use when the
|
|
36
|
+
* upload sits inline between other form inputs and should not
|
|
37
|
+
* visually dominate them.
|
|
38
|
+
*/
|
|
39
|
+
sm: {
|
|
40
|
+
dropzone: string;
|
|
41
|
+
content: string;
|
|
42
|
+
iconWrapper: string;
|
|
43
|
+
icon: string;
|
|
44
|
+
textBlock: string;
|
|
45
|
+
mainText: string;
|
|
46
|
+
hintText: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
}, {
|
|
50
|
+
dropzone: string;
|
|
51
|
+
content: string;
|
|
52
|
+
iconWrapper: string;
|
|
53
|
+
icon: string;
|
|
54
|
+
textBlock: string;
|
|
55
|
+
mainText: string;
|
|
56
|
+
hintText: string;
|
|
57
|
+
}, undefined, {
|
|
58
|
+
size: {
|
|
59
|
+
/**
|
|
60
|
+
* Hero square dropzone — the original large card.
|
|
61
|
+
* 48px padding, 32px corner radius, 96px circular icon, column layout.
|
|
62
|
+
* Use for empty-state pages or standalone upload dialogs where the
|
|
63
|
+
* dropzone is the main focus of the screen.
|
|
64
|
+
*/
|
|
65
|
+
xl: {
|
|
66
|
+
dropzone: string;
|
|
67
|
+
content: string;
|
|
68
|
+
iconWrapper: string;
|
|
69
|
+
icon: string;
|
|
70
|
+
textBlock: string;
|
|
71
|
+
mainText: string;
|
|
72
|
+
hintText: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Wide medium dropzone — row layout, icon-left + text-right.
|
|
76
|
+
* ~80-90px tall, full-width. Use as a standalone form section
|
|
77
|
+
* where the upload is a primary action but the page also has
|
|
78
|
+
* other fields.
|
|
79
|
+
*/
|
|
80
|
+
md: {
|
|
81
|
+
dropzone: string;
|
|
82
|
+
content: string;
|
|
83
|
+
iconWrapper: string;
|
|
84
|
+
icon: string;
|
|
85
|
+
textBlock: string;
|
|
86
|
+
mainText: string;
|
|
87
|
+
hintText: string;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Compact inline dropzone — row layout, tiny.
|
|
91
|
+
* ~56-60px tall, behaves like a single form field. Use when the
|
|
92
|
+
* upload sits inline between other form inputs and should not
|
|
93
|
+
* visually dominate them.
|
|
94
|
+
*/
|
|
95
|
+
sm: {
|
|
96
|
+
dropzone: string;
|
|
97
|
+
content: string;
|
|
98
|
+
iconWrapper: string;
|
|
99
|
+
icon: string;
|
|
100
|
+
textBlock: string;
|
|
101
|
+
mainText: string;
|
|
102
|
+
hintText: string;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
}, {
|
|
106
|
+
dropzone: string;
|
|
107
|
+
content: string;
|
|
108
|
+
iconWrapper: string;
|
|
109
|
+
icon: string;
|
|
110
|
+
textBlock: string;
|
|
111
|
+
mainText: string;
|
|
112
|
+
hintText: string;
|
|
113
|
+
}, import("tailwind-variants").TVReturnType<{
|
|
114
|
+
size: {
|
|
115
|
+
/**
|
|
116
|
+
* Hero square dropzone — the original large card.
|
|
117
|
+
* 48px padding, 32px corner radius, 96px circular icon, column layout.
|
|
118
|
+
* Use for empty-state pages or standalone upload dialogs where the
|
|
119
|
+
* dropzone is the main focus of the screen.
|
|
120
|
+
*/
|
|
121
|
+
xl: {
|
|
122
|
+
dropzone: string;
|
|
123
|
+
content: string;
|
|
124
|
+
iconWrapper: string;
|
|
125
|
+
icon: string;
|
|
126
|
+
textBlock: string;
|
|
127
|
+
mainText: string;
|
|
128
|
+
hintText: string;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Wide medium dropzone — row layout, icon-left + text-right.
|
|
132
|
+
* ~80-90px tall, full-width. Use as a standalone form section
|
|
133
|
+
* where the upload is a primary action but the page also has
|
|
134
|
+
* other fields.
|
|
135
|
+
*/
|
|
136
|
+
md: {
|
|
137
|
+
dropzone: string;
|
|
138
|
+
content: string;
|
|
139
|
+
iconWrapper: string;
|
|
140
|
+
icon: string;
|
|
141
|
+
textBlock: string;
|
|
142
|
+
mainText: string;
|
|
143
|
+
hintText: string;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Compact inline dropzone — row layout, tiny.
|
|
147
|
+
* ~56-60px tall, behaves like a single form field. Use when the
|
|
148
|
+
* upload sits inline between other form inputs and should not
|
|
149
|
+
* visually dominate them.
|
|
150
|
+
*/
|
|
151
|
+
sm: {
|
|
152
|
+
dropzone: string;
|
|
153
|
+
content: string;
|
|
154
|
+
iconWrapper: string;
|
|
155
|
+
icon: string;
|
|
156
|
+
textBlock: string;
|
|
157
|
+
mainText: string;
|
|
158
|
+
hintText: string;
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
}, {
|
|
162
|
+
dropzone: string;
|
|
163
|
+
content: string;
|
|
164
|
+
iconWrapper: string;
|
|
165
|
+
icon: string;
|
|
166
|
+
textBlock: string;
|
|
167
|
+
mainText: string;
|
|
168
|
+
hintText: string;
|
|
169
|
+
}, undefined, unknown, unknown, undefined>>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { tv } from 'tailwind-variants';
|
|
2
|
+
export const fileUpload = tv({
|
|
3
|
+
slots: {
|
|
4
|
+
dropzone: 'group relative block border-2 border-dashed transition-colors',
|
|
5
|
+
content: 'flex',
|
|
6
|
+
iconWrapper: 'bg-primary-100 flex items-center justify-center rounded-full shrink-0',
|
|
7
|
+
icon: 'text-primary-500',
|
|
8
|
+
textBlock: 'flex flex-col',
|
|
9
|
+
mainText: '',
|
|
10
|
+
hintText: 'text-default-500'
|
|
11
|
+
},
|
|
12
|
+
variants: {
|
|
13
|
+
size: {
|
|
14
|
+
/**
|
|
15
|
+
* Hero square dropzone — the original large card.
|
|
16
|
+
* 48px padding, 32px corner radius, 96px circular icon, column layout.
|
|
17
|
+
* Use for empty-state pages or standalone upload dialogs where the
|
|
18
|
+
* dropzone is the main focus of the screen.
|
|
19
|
+
*/
|
|
20
|
+
xl: {
|
|
21
|
+
dropzone: 'rounded-[32px] p-12 text-center',
|
|
22
|
+
content: 'flex-col items-center gap-4',
|
|
23
|
+
iconWrapper: 'size-24 mb-2',
|
|
24
|
+
icon: 'size-12',
|
|
25
|
+
textBlock: 'items-center gap-1',
|
|
26
|
+
mainText: 'text-sm',
|
|
27
|
+
hintText: 'text-xs'
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Wide medium dropzone — row layout, icon-left + text-right.
|
|
31
|
+
* ~80-90px tall, full-width. Use as a standalone form section
|
|
32
|
+
* where the upload is a primary action but the page also has
|
|
33
|
+
* other fields.
|
|
34
|
+
*/
|
|
35
|
+
md: {
|
|
36
|
+
dropzone: 'rounded-xl p-4 text-left',
|
|
37
|
+
content: 'flex-row items-center gap-4',
|
|
38
|
+
iconWrapper: 'size-12',
|
|
39
|
+
icon: 'size-6',
|
|
40
|
+
textBlock: 'items-start gap-0.5 min-w-0 flex-1',
|
|
41
|
+
mainText: 'text-sm',
|
|
42
|
+
hintText: 'text-xs'
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Compact inline dropzone — row layout, tiny.
|
|
46
|
+
* ~56-60px tall, behaves like a single form field. Use when the
|
|
47
|
+
* upload sits inline between other form inputs and should not
|
|
48
|
+
* visually dominate them.
|
|
49
|
+
*/
|
|
50
|
+
sm: {
|
|
51
|
+
dropzone: 'rounded-lg px-3 py-2 text-left',
|
|
52
|
+
content: 'flex-row items-center gap-3',
|
|
53
|
+
iconWrapper: 'size-9',
|
|
54
|
+
icon: 'size-5',
|
|
55
|
+
textBlock: 'items-start gap-0 min-w-0 flex-1',
|
|
56
|
+
mainText: 'text-xs leading-tight',
|
|
57
|
+
hintText: 'text-[11px] leading-tight truncate'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
defaultVariants: {
|
|
62
|
+
size: 'xl'
|
|
63
|
+
}
|
|
64
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export type { AccordionProps } from './elements/accordion/accordion-types.js';
|
|
|
46
46
|
export type { TimelineItem } from './elements/timeline/timeline-types.js';
|
|
47
47
|
export type { FilterTab, FilterGroup, CompactFiltersProps } from './filters/filter-types.js';
|
|
48
48
|
export type { ActivityItemBadge, ActivityItemAction, ActivityItem, ActivityListProps } from './layout/activity-list/activity-list-types.js';
|
|
49
|
-
export type { FileUploadProps, FilePreviewProps, UploadedFile } from './elements/file-upload/file-upload-types.js';
|
|
49
|
+
export type { FileUploadProps, FileUploadSize, FilePreviewProps, UploadedFile, StagedFile } from './elements/file-upload/file-upload-types.js';
|
|
50
50
|
export type { ChatMessageType, StreamingCallback, ChatAction, ChatMessage, ChatResponse, QuickAction, FileBrowserProps } from './ai/ai-types.js';
|
|
51
51
|
export type { GetUsersOptions, GetUsersResult, UserEmail, UserPhone, User, Permission, Role, UserTableProps, UserModalProps, UserViewModalProps, UserManagementAdapter, UserManagementProps, FormErrors } from './user-management/user-management-types.js';
|
|
52
52
|
export { tv, cn } from './helper/cls.js';
|