@makolabs/ripple 1.13.1 → 1.14.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 +27 -24
- package/dist/elements/file-upload/MultiFileUpload.svelte +274 -0
- package/dist/elements/file-upload/MultiFileUpload.svelte.d.ts +4 -0
- package/dist/elements/file-upload/file-upload-types.d.ts +114 -0
- 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 +2 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../../helper/cls.js';
|
|
3
|
+
import { fileUpload } from './file-upload.js';
|
|
3
4
|
import type { FileUploadProps } from '../../index.js';
|
|
4
5
|
|
|
5
6
|
let {
|
|
6
7
|
allowedMimeTypes = [],
|
|
7
8
|
maxFiles = 10,
|
|
8
9
|
maxSize,
|
|
10
|
+
size = 'xl',
|
|
9
11
|
class: className = '',
|
|
10
12
|
dropzoneClass = '',
|
|
11
13
|
id = 'file-upload',
|
|
@@ -13,6 +15,8 @@
|
|
|
13
15
|
uploadContent
|
|
14
16
|
}: FileUploadProps = $props();
|
|
15
17
|
|
|
18
|
+
const slots = $derived(fileUpload({ size }));
|
|
19
|
+
|
|
16
20
|
const disabled = $derived(maxFiles <= 0);
|
|
17
21
|
|
|
18
22
|
let isDragging = $state(false);
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
<div class={cn('w-full', className)}>
|
|
71
75
|
<label
|
|
72
76
|
class={cn(
|
|
73
|
-
|
|
77
|
+
slots.dropzone(),
|
|
74
78
|
{
|
|
75
79
|
'border-primary-400 bg-primary-50': isDragging,
|
|
76
80
|
'border-default-200 bg-white': !isDragging,
|
|
@@ -96,14 +100,10 @@
|
|
|
96
100
|
{id}
|
|
97
101
|
/>
|
|
98
102
|
|
|
99
|
-
<div class=
|
|
103
|
+
<div class={slots.content()}>
|
|
100
104
|
<!-- Upload Icon -->
|
|
101
|
-
<div class=
|
|
102
|
-
<svg
|
|
103
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
104
|
-
class="text-primary-500 size-12"
|
|
105
|
-
viewBox="0 0 24 24"
|
|
106
|
-
>
|
|
105
|
+
<div class={slots.iconWrapper()} data-fileupload-icon-wrapper="">
|
|
106
|
+
<svg xmlns="http://www.w3.org/2000/svg" class={slots.icon()} viewBox="0 0 24 24">
|
|
107
107
|
<path
|
|
108
108
|
fill="currentColor"
|
|
109
109
|
d="M11 14.2V6.8l-3.7 3.7L6 9l6-6l6 6l-1.3 1.4L13 6.8v7.4zm-5 4.3h12v2H6z"
|
|
@@ -111,24 +111,27 @@
|
|
|
111
111
|
</svg>
|
|
112
112
|
</div>
|
|
113
113
|
|
|
114
|
-
<!--
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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)
|
|
114
|
+
<!-- Text stack (wrapped so row layout in sm can place icon on the left and this block on the right) -->
|
|
115
|
+
<div class={slots.textBlock()}>
|
|
116
|
+
{#if !uploadContent}
|
|
117
|
+
<div class={slots.mainText()}>
|
|
118
|
+
<span class="text-primary-500 font-medium">Click here</span>
|
|
119
|
+
<span class="text-default-600"> to upload your file or drag and drop.</span>
|
|
120
|
+
</div>
|
|
129
121
|
{:else}
|
|
130
|
-
(
|
|
122
|
+
{@render uploadContent()}
|
|
131
123
|
{/if}
|
|
124
|
+
|
|
125
|
+
<div class={slots.hintText()}>
|
|
126
|
+
Supported Format: {allowedMimeTypes.length
|
|
127
|
+
? allowedMimeTypes.join(', ')
|
|
128
|
+
: 'SVG, JPG, PNG'}
|
|
129
|
+
{#if maxSize}
|
|
130
|
+
({formatFileSize(maxSize)} each)
|
|
131
|
+
{:else}
|
|
132
|
+
(10MB each)
|
|
133
|
+
{/if}
|
|
134
|
+
</div>
|
|
132
135
|
</div>
|
|
133
136
|
</div>
|
|
134
137
|
</label>
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
4
|
+
import FileUpload from './FileUpload.svelte';
|
|
5
|
+
import Button from '../../button/Button.svelte';
|
|
6
|
+
import Spinner from '../spinner/Spinner.svelte';
|
|
7
|
+
import { Size, Color } from '../../variants.js';
|
|
8
|
+
import type { MultiFileUploadProps, StagedFile } from '../../index.js';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
files = $bindable<StagedFile[]>([]),
|
|
12
|
+
allowedMimeTypes = [],
|
|
13
|
+
maxFiles = 10,
|
|
14
|
+
maxSize,
|
|
15
|
+
size = 'md',
|
|
16
|
+
id = 'multi-file-upload',
|
|
17
|
+
class: className = '',
|
|
18
|
+
dropzoneClass = '',
|
|
19
|
+
onfiles,
|
|
20
|
+
clearAfterUpload = true,
|
|
21
|
+
uploadButtonLabel = 'Upload All',
|
|
22
|
+
clearButtonLabel = 'Clear All',
|
|
23
|
+
filesListLabel = 'Selected files',
|
|
24
|
+
listMaxHeight = 'max-h-64',
|
|
25
|
+
header,
|
|
26
|
+
testId
|
|
27
|
+
}: MultiFileUploadProps = $props();
|
|
28
|
+
|
|
29
|
+
function makeId(): string {
|
|
30
|
+
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
|
31
|
+
return crypto.randomUUID();
|
|
32
|
+
}
|
|
33
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleAdd(newFiles: FileList | File[]) {
|
|
37
|
+
const arr = Array.from(newFiles);
|
|
38
|
+
const room = Math.max(0, maxFiles - files.length);
|
|
39
|
+
const accepted = arr.slice(0, room);
|
|
40
|
+
const toAdd: StagedFile[] = accepted.map((file) => ({
|
|
41
|
+
id: makeId(),
|
|
42
|
+
file,
|
|
43
|
+
status: 'ready' as const
|
|
44
|
+
}));
|
|
45
|
+
files = [...files, ...toAdd];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handleRemove(fileId: string) {
|
|
49
|
+
files = files.filter((f) => f.id !== fileId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleClearAll() {
|
|
53
|
+
files = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const readyCount = $derived(files.filter((f) => !f.status || f.status === 'ready').length);
|
|
57
|
+
const hasAnyFile = $derived(files.length > 0);
|
|
58
|
+
const anyUploading = $derived(files.some((f) => f.status === 'uploading'));
|
|
59
|
+
const canUpload = $derived(readyCount > 0 && !anyUploading && !!onfiles);
|
|
60
|
+
|
|
61
|
+
function handleUploadAll() {
|
|
62
|
+
if (!onfiles) return;
|
|
63
|
+
const rawFiles = files.filter((f) => !f.status || f.status === 'ready').map((f) => f.file);
|
|
64
|
+
if (rawFiles.length === 0) return;
|
|
65
|
+
onfiles(rawFiles);
|
|
66
|
+
if (clearAfterUpload) {
|
|
67
|
+
files = [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleRetry(fileId: string) {
|
|
72
|
+
const target = files.find((f) => f.id === fileId);
|
|
73
|
+
if (!target || !onfiles) return;
|
|
74
|
+
// Reset the row to 'ready' so the caller's upload handler picks it up.
|
|
75
|
+
files = files.map((f) =>
|
|
76
|
+
f.id === fileId
|
|
77
|
+
? { ...f, status: 'ready' as const, error: undefined, progress: undefined }
|
|
78
|
+
: f
|
|
79
|
+
);
|
|
80
|
+
onfiles([target.file]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatFileSize(bytes: number): string {
|
|
84
|
+
if (bytes === 0) return '0 B';
|
|
85
|
+
const k = 1024;
|
|
86
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
87
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
88
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Internal FileUpload should only accept files up to the remaining room.
|
|
92
|
+
// This caps input but the dropzone still renders.
|
|
93
|
+
const remainingFiles = $derived(Math.max(0, maxFiles - files.length));
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<div
|
|
97
|
+
class={cn('flex flex-col gap-4', className)}
|
|
98
|
+
data-testid={buildTestId('multi-file-upload', undefined, testId)}
|
|
99
|
+
>
|
|
100
|
+
{#if header}
|
|
101
|
+
{@render header()}
|
|
102
|
+
{/if}
|
|
103
|
+
|
|
104
|
+
<FileUpload
|
|
105
|
+
{allowedMimeTypes}
|
|
106
|
+
maxFiles={remainingFiles}
|
|
107
|
+
{maxSize}
|
|
108
|
+
{size}
|
|
109
|
+
{dropzoneClass}
|
|
110
|
+
{id}
|
|
111
|
+
onfiles={handleAdd}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
{#if hasAnyFile}
|
|
115
|
+
<div class="flex flex-col gap-2">
|
|
116
|
+
<div class="flex items-center justify-between">
|
|
117
|
+
<span class="text-default-700 text-sm font-medium">
|
|
118
|
+
{filesListLabel} ({files.length})
|
|
119
|
+
</span>
|
|
120
|
+
<div class="flex gap-2">
|
|
121
|
+
<Button
|
|
122
|
+
size={Size.XS}
|
|
123
|
+
variant="outline"
|
|
124
|
+
color={Color.DEFAULT}
|
|
125
|
+
onclick={handleClearAll}
|
|
126
|
+
disabled={anyUploading}
|
|
127
|
+
>
|
|
128
|
+
{clearButtonLabel}
|
|
129
|
+
</Button>
|
|
130
|
+
<Button
|
|
131
|
+
size={Size.XS}
|
|
132
|
+
color={Color.PRIMARY}
|
|
133
|
+
onclick={handleUploadAll}
|
|
134
|
+
disabled={!canUpload}
|
|
135
|
+
>
|
|
136
|
+
{uploadButtonLabel}{readyCount > 0 ? ` (${readyCount})` : ''}
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<ul
|
|
142
|
+
class={cn(
|
|
143
|
+
'border-default-200 divide-default-100 divide-y overflow-y-auto rounded-lg border',
|
|
144
|
+
listMaxHeight
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
{#each files as stagedFile (stagedFile.id)}
|
|
148
|
+
{@const status = stagedFile.status ?? 'ready'}
|
|
149
|
+
<li
|
|
150
|
+
class={cn('flex items-center gap-3 px-3 py-2', status === 'success' && 'opacity-60')}
|
|
151
|
+
data-multi-file-row=""
|
|
152
|
+
data-status={status}
|
|
153
|
+
>
|
|
154
|
+
<!-- Status icon -->
|
|
155
|
+
{#if status === 'ready'}
|
|
156
|
+
<span
|
|
157
|
+
class="bg-default-100 text-default-500 flex size-7 shrink-0 items-center justify-center rounded-full"
|
|
158
|
+
aria-hidden="true"
|
|
159
|
+
>
|
|
160
|
+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
161
|
+
<path
|
|
162
|
+
stroke-linecap="round"
|
|
163
|
+
stroke-linejoin="round"
|
|
164
|
+
stroke-width="2"
|
|
165
|
+
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"
|
|
166
|
+
/>
|
|
167
|
+
</svg>
|
|
168
|
+
</span>
|
|
169
|
+
{:else if status === 'uploading'}
|
|
170
|
+
<div class="flex size-7 shrink-0 items-center justify-center">
|
|
171
|
+
<Spinner size={Size.SM} color={Color.PRIMARY} />
|
|
172
|
+
</div>
|
|
173
|
+
{:else if status === 'success'}
|
|
174
|
+
<span
|
|
175
|
+
class="bg-success-100 text-success-600 flex size-7 shrink-0 items-center justify-center rounded-full"
|
|
176
|
+
aria-hidden="true"
|
|
177
|
+
>
|
|
178
|
+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
179
|
+
<path
|
|
180
|
+
stroke-linecap="round"
|
|
181
|
+
stroke-linejoin="round"
|
|
182
|
+
stroke-width="3"
|
|
183
|
+
d="M5 13l4 4L19 7"
|
|
184
|
+
/>
|
|
185
|
+
</svg>
|
|
186
|
+
</span>
|
|
187
|
+
{:else if status === 'error'}
|
|
188
|
+
<span
|
|
189
|
+
class="bg-danger-100 text-danger-600 flex size-7 shrink-0 items-center justify-center rounded-full"
|
|
190
|
+
aria-hidden="true"
|
|
191
|
+
>
|
|
192
|
+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
193
|
+
<path
|
|
194
|
+
stroke-linecap="round"
|
|
195
|
+
stroke-linejoin="round"
|
|
196
|
+
stroke-width="3"
|
|
197
|
+
d="M6 18L18 6M6 6l12 12"
|
|
198
|
+
/>
|
|
199
|
+
</svg>
|
|
200
|
+
</span>
|
|
201
|
+
{/if}
|
|
202
|
+
|
|
203
|
+
<!-- File info -->
|
|
204
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
205
|
+
<span class="text-default-900 truncate text-sm font-medium">
|
|
206
|
+
{stagedFile.file.name}
|
|
207
|
+
</span>
|
|
208
|
+
<span class="text-default-500 text-xs">
|
|
209
|
+
{formatFileSize(stagedFile.file.size)}
|
|
210
|
+
{#if stagedFile.error}
|
|
211
|
+
<span class="text-danger-600"> · {stagedFile.error}</span>
|
|
212
|
+
{/if}
|
|
213
|
+
</span>
|
|
214
|
+
{#if status === 'uploading' && stagedFile.progress !== undefined}
|
|
215
|
+
<div class="bg-default-100 mt-1 h-1 w-full overflow-hidden rounded-full">
|
|
216
|
+
<div
|
|
217
|
+
data-multi-file-progress=""
|
|
218
|
+
class="bg-primary-500 h-full transition-all duration-200"
|
|
219
|
+
style="width: {Math.max(0, Math.min(100, stagedFile.progress))}%"
|
|
220
|
+
></div>
|
|
221
|
+
</div>
|
|
222
|
+
{/if}
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- Retry button — only for error rows (re-runs upload for this one file) -->
|
|
226
|
+
{#if status === 'error'}
|
|
227
|
+
<button
|
|
228
|
+
type="button"
|
|
229
|
+
onclick={() => handleRetry(stagedFile.id)}
|
|
230
|
+
class="text-default-500 hover:bg-primary-50 hover:text-primary-600 shrink-0 cursor-pointer rounded p-1 transition-colors"
|
|
231
|
+
aria-label="Retry upload for {stagedFile.file.name}"
|
|
232
|
+
title="Retry"
|
|
233
|
+
>
|
|
234
|
+
<svg
|
|
235
|
+
class="size-4"
|
|
236
|
+
fill="none"
|
|
237
|
+
viewBox="0 0 24 24"
|
|
238
|
+
stroke="currentColor"
|
|
239
|
+
stroke-width="2"
|
|
240
|
+
stroke-linecap="round"
|
|
241
|
+
stroke-linejoin="round"
|
|
242
|
+
>
|
|
243
|
+
<polyline points="23 4 23 10 17 10" />
|
|
244
|
+
<polyline points="1 20 1 14 7 14" />
|
|
245
|
+
<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" />
|
|
246
|
+
</svg>
|
|
247
|
+
</button>
|
|
248
|
+
{/if}
|
|
249
|
+
|
|
250
|
+
<!-- Remove button — hidden for in-flight or successful rows -->
|
|
251
|
+
{#if status !== 'uploading' && status !== 'success'}
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
onclick={() => handleRemove(stagedFile.id)}
|
|
255
|
+
class="text-default-400 hover:bg-default-100 hover:text-danger-500 shrink-0 cursor-pointer rounded p-1 transition-colors"
|
|
256
|
+
aria-label="Remove {stagedFile.file.name}"
|
|
257
|
+
title="Remove"
|
|
258
|
+
>
|
|
259
|
+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
260
|
+
<path
|
|
261
|
+
stroke-linecap="round"
|
|
262
|
+
stroke-linejoin="round"
|
|
263
|
+
stroke-width="2"
|
|
264
|
+
d="M6 18L18 6M6 6l12 12"
|
|
265
|
+
/>
|
|
266
|
+
</svg>
|
|
267
|
+
</button>
|
|
268
|
+
{/if}
|
|
269
|
+
</li>
|
|
270
|
+
{/each}
|
|
271
|
+
</ul>
|
|
272
|
+
</div>
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClassValue } from 'tailwind-variants';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
export type FileUploadSize = 'xl' | 'md' | 'sm';
|
|
3
4
|
export interface FileUploadProps {
|
|
4
5
|
/**
|
|
5
6
|
* Array of allowed file MIME types or extensions
|
|
@@ -15,6 +16,22 @@ export interface FileUploadProps {
|
|
|
15
16
|
* @default 10
|
|
16
17
|
*/
|
|
17
18
|
maxFiles?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Visual size variant.
|
|
21
|
+
*
|
|
22
|
+
* - `xl` (default) — hero square card, column layout, 48px padding +
|
|
23
|
+
* 96px circular icon. Use for empty-state pages or standalone upload
|
|
24
|
+
* dialogs where the dropzone is the main focus of the screen.
|
|
25
|
+
* - `md` — wide medium rectangle, row layout, icon-left + text-right,
|
|
26
|
+
* ~80-90px tall, full-width. Use as a prominent form section when
|
|
27
|
+
* the upload is a primary action alongside other fields.
|
|
28
|
+
* - `sm` — compact inline rectangle, row layout, ~56-60px tall. Behaves
|
|
29
|
+
* like a single form field; sits next to other inputs without
|
|
30
|
+
* dominating.
|
|
31
|
+
*
|
|
32
|
+
* @default 'xl'
|
|
33
|
+
*/
|
|
34
|
+
size?: FileUploadSize;
|
|
18
35
|
/**
|
|
19
36
|
* CSS class for the component container
|
|
20
37
|
*/
|
|
@@ -66,3 +83,100 @@ export interface UploadedFile {
|
|
|
66
83
|
*/
|
|
67
84
|
error?: string;
|
|
68
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* A file held in a `MultiFileUpload` component's staging list. Either purely
|
|
88
|
+
* internal (the component owns the state) or bindable (the caller owns the
|
|
89
|
+
* state and can mutate `status` / `progress` / `error` during upload for
|
|
90
|
+
* per-file visual feedback).
|
|
91
|
+
*/
|
|
92
|
+
export interface StagedFile {
|
|
93
|
+
/** Stable client-generated id for list keys and caller-side updates. */
|
|
94
|
+
id: string;
|
|
95
|
+
/** The raw File object. */
|
|
96
|
+
file: File;
|
|
97
|
+
/**
|
|
98
|
+
* Row state. Defaults to 'ready'. Rich-mode callers update this during
|
|
99
|
+
* upload to drive the per-row status icon and progress bar.
|
|
100
|
+
*/
|
|
101
|
+
status?: 'ready' | 'uploading' | 'success' | 'error';
|
|
102
|
+
/** Optional 0-100 for the per-row progress bar while status='uploading'. */
|
|
103
|
+
progress?: number;
|
|
104
|
+
/** Optional error message rendered under the file name while status='error'. */
|
|
105
|
+
error?: string;
|
|
106
|
+
}
|
|
107
|
+
export interface MultiFileUploadProps {
|
|
108
|
+
/**
|
|
109
|
+
* Array of allowed file MIME types or extensions.
|
|
110
|
+
* Forwarded to the internal `FileUpload` dropzone.
|
|
111
|
+
* @example ['text/csv', '.csv.gz']
|
|
112
|
+
*/
|
|
113
|
+
allowedMimeTypes?: string[];
|
|
114
|
+
/**
|
|
115
|
+
* Maximum file size in bytes (informational; shown in the dropzone hint).
|
|
116
|
+
* Forwarded to the internal `FileUpload` dropzone.
|
|
117
|
+
*/
|
|
118
|
+
maxSize?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Maximum number of files that can be in the staging list at once.
|
|
121
|
+
* Additional drops beyond this are discarded.
|
|
122
|
+
* @default 10
|
|
123
|
+
*/
|
|
124
|
+
maxFiles?: number;
|
|
125
|
+
/**
|
|
126
|
+
* Dropzone size variant. Forwarded to the internal `FileUpload`.
|
|
127
|
+
* @default 'md'
|
|
128
|
+
*/
|
|
129
|
+
size?: FileUploadSize;
|
|
130
|
+
/** CSS class for the component wrapper. */
|
|
131
|
+
class?: ClassValue;
|
|
132
|
+
/** CSS class for the internal dropzone. */
|
|
133
|
+
dropzoneClass?: ClassValue;
|
|
134
|
+
/** ID for the internal file input element. */
|
|
135
|
+
id?: string;
|
|
136
|
+
/**
|
|
137
|
+
* Bindable staging list. Two usage modes:
|
|
138
|
+
*
|
|
139
|
+
* - **Simple mode** (omit the prop): the component owns the staging list,
|
|
140
|
+
* auto-clears it after `onfiles` fires, and never renders status icons.
|
|
141
|
+
* Rows show `name + size + ×` only. Exactly like a batched `FileUpload`.
|
|
142
|
+
*
|
|
143
|
+
* - **Rich mode** (`bind:files`): the caller owns the list. The component
|
|
144
|
+
* writes adds/removes through the binding, and reads each file's
|
|
145
|
+
* `status` / `progress` / `error` to render per-row indicators. Staging
|
|
146
|
+
* does not auto-clear — the caller manages it.
|
|
147
|
+
*
|
|
148
|
+
* Rich mode also requires setting `clearAfterUpload={false}` to prevent
|
|
149
|
+
* the component from emptying the list after firing `onfiles`.
|
|
150
|
+
*/
|
|
151
|
+
files?: StagedFile[];
|
|
152
|
+
/**
|
|
153
|
+
* Fires when the user clicks "Upload All", with the raw `File[]` for every
|
|
154
|
+
* file currently in the staging list with status `'ready'`. Identical shape
|
|
155
|
+
* to `FileUpload.onfiles` — callers that already handle `FileUpload` can
|
|
156
|
+
* drop this in as a batched equivalent.
|
|
157
|
+
*/
|
|
158
|
+
onfiles?: (files: File[]) => void;
|
|
159
|
+
/**
|
|
160
|
+
* Whether to automatically empty the staging list after `onfiles` fires.
|
|
161
|
+
* Set to `false` in rich mode so the caller can keep the list visible
|
|
162
|
+
* while updating per-file status during upload.
|
|
163
|
+
* @default true
|
|
164
|
+
*/
|
|
165
|
+
clearAfterUpload?: boolean;
|
|
166
|
+
/** Label for the primary action button. @default 'Upload All' */
|
|
167
|
+
uploadButtonLabel?: string;
|
|
168
|
+
/** Label for the secondary clear button. @default 'Clear All' */
|
|
169
|
+
clearButtonLabel?: string;
|
|
170
|
+
/** Label above the staging list. @default 'Selected files' */
|
|
171
|
+
filesListLabel?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Max height of the staging list as a Tailwind class. The list scrolls
|
|
174
|
+
* internally when content exceeds this height. Use `'max-h-none'` to
|
|
175
|
+
* disable the cap entirely and let the list grow with its content.
|
|
176
|
+
* @default 'max-h-64'
|
|
177
|
+
*/
|
|
178
|
+
listMaxHeight?: string;
|
|
179
|
+
/** Optional snippet rendered above the dropzone (for titles, selects, banners). */
|
|
180
|
+
header?: Snippet;
|
|
181
|
+
testId?: string;
|
|
182
|
+
}
|
|
@@ -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, MultiFileUploadProps } 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';
|
|
@@ -92,6 +92,7 @@ export { default as Timeline } from './elements/timeline/Timeline.svelte';
|
|
|
92
92
|
export { default as Chart } from './charts/Chart.svelte';
|
|
93
93
|
export { default as FileUpload } from './elements/file-upload/FileUpload.svelte';
|
|
94
94
|
export { default as FilesPreview } from './elements/file-upload/FilesPreview.svelte';
|
|
95
|
+
export { default as MultiFileUpload } from './elements/file-upload/MultiFileUpload.svelte';
|
|
95
96
|
export { default as Toaster } from './sonner/sonner.svelte';
|
|
96
97
|
export { default as AIChatInterface } from './ai/AIChatInterface.svelte';
|
|
97
98
|
export { default as MermaidRenderer } from './ai/MermaidRenderer.svelte';
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,7 @@ export { default as Chart } from './charts/Chart.svelte';
|
|
|
82
82
|
// File Upload
|
|
83
83
|
export { default as FileUpload } from './elements/file-upload/FileUpload.svelte';
|
|
84
84
|
export { default as FilesPreview } from './elements/file-upload/FilesPreview.svelte';
|
|
85
|
+
export { default as MultiFileUpload } from './elements/file-upload/MultiFileUpload.svelte';
|
|
85
86
|
// Toaster: Should be registered in +layout.svelte
|
|
86
87
|
export { default as Toaster } from './sonner/sonner.svelte';
|
|
87
88
|
// AI Components
|