@makolabs/ripple 1.14.0 → 2.1.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 +252 -18
- package/dist/elements/file-upload/FileUpload.svelte.d.ts +1 -1
- package/dist/elements/file-upload/file-upload-types.d.ts +74 -107
- package/dist/index.d.ts +2 -3
- package/dist/index.js +0 -1
- package/dist/layout/activity-list/ActivityList.svelte +109 -36
- package/dist/layout/activity-list/activity-list-types.d.ts +82 -2
- package/dist/layout/activity-list/activity-list.d.ts +124 -0
- package/dist/layout/activity-list/activity-list.js +98 -9
- package/package.json +1 -1
- package/dist/elements/file-upload/MultiFileUpload.svelte +0 -274
- package/dist/elements/file-upload/MultiFileUpload.svelte.d.ts +0 -4
|
@@ -1,274 +0,0 @@
|
|
|
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>
|