@juspay/svelte-ui-components 2.133.1 → 2.134.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/README.md +30 -0
- package/dist/Draggable/Draggable.svelte +175 -0
- package/dist/Draggable/Draggable.svelte.d.ts +4 -0
- package/dist/Draggable/properties.d.ts +26 -0
- package/dist/Draggable/properties.js +1 -0
- package/dist/Gallery/Gallery.svelte +593 -0
- package/dist/Gallery/Gallery.svelte.d.ts +4 -0
- package/dist/Gallery/properties.d.ts +39 -0
- package/dist/Gallery/properties.js +1 -0
- package/dist/MediaPlayer/MediaPlayer.svelte +277 -0
- package/dist/MediaPlayer/MediaPlayer.svelte.d.ts +4 -0
- package/dist/MediaPlayer/properties.d.ts +34 -0
- package/dist/MediaPlayer/properties.js +1 -0
- package/dist/MediaUpload/MediaUpload.svelte +528 -0
- package/dist/MediaUpload/MediaUpload.svelte.d.ts +4 -0
- package/dist/MediaUpload/properties.d.ts +46 -0
- package/dist/MediaUpload/properties.js +1 -0
- package/dist/Modal/Modal.svelte +17 -3
- package/dist/Modal/properties.d.ts +4 -0
- package/dist/assets/add.svg +4 -0
- package/dist/assets/delete.svg +7 -0
- package/dist/assets/edit.svg +4 -0
- package/dist/assets/file.svg +4 -0
- package/dist/assets/mute.svg +5 -0
- package/dist/assets/pause.svg +4 -0
- package/dist/assets/play.svg +3 -0
- package/dist/assets/volume.svg +5 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist-wc/index.d.ts +1 -0
- package/dist-wc/index.js +33018 -0
- package/package.json +16 -9
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../Button/Button.svelte';
|
|
3
|
+
import Img from '../Img/Img.svelte';
|
|
4
|
+
import Shimmer from '../Shimmer/Shimmer.svelte';
|
|
5
|
+
import addSvg from '../assets/add.svg?raw';
|
|
6
|
+
import closeSvg from '../assets/close.svg?raw';
|
|
7
|
+
import fileSvg from '../assets/file.svg?raw';
|
|
8
|
+
import type { MediaUploadItem, MediaUploadProperties, MediaUploadRejection } from './properties';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
label,
|
|
12
|
+
description,
|
|
13
|
+
addText,
|
|
14
|
+
hintText,
|
|
15
|
+
maxLength = 3,
|
|
16
|
+
accept = 'image/*',
|
|
17
|
+
maxFileSize = 0,
|
|
18
|
+
multiple = false,
|
|
19
|
+
dragAndDrop = true,
|
|
20
|
+
disabled = false,
|
|
21
|
+
showCounter = true,
|
|
22
|
+
showFileName = true,
|
|
23
|
+
showFileSize = true,
|
|
24
|
+
addIcon,
|
|
25
|
+
removeIcon,
|
|
26
|
+
fileIcon,
|
|
27
|
+
errorMessages,
|
|
28
|
+
files = $bindable([]),
|
|
29
|
+
onFilesChange,
|
|
30
|
+
onRemove,
|
|
31
|
+
onRejected,
|
|
32
|
+
testId,
|
|
33
|
+
classes
|
|
34
|
+
}: MediaUploadProperties = $props();
|
|
35
|
+
|
|
36
|
+
let items: MediaUploadItem[] = $state([]);
|
|
37
|
+
let error: string = $state('');
|
|
38
|
+
let dragging: boolean = $state(false);
|
|
39
|
+
let inputId = $props.id();
|
|
40
|
+
|
|
41
|
+
let atCapacity = $derived(items.length >= maxLength);
|
|
42
|
+
let showDropTile = $derived(!atCapacity && !disabled);
|
|
43
|
+
|
|
44
|
+
function syncFiles(): void {
|
|
45
|
+
files = items.map((item) => item.file);
|
|
46
|
+
onFilesChange?.(files);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatSize(bytes: number): string {
|
|
50
|
+
if (bytes < 1024) {
|
|
51
|
+
return `${bytes} B`;
|
|
52
|
+
}
|
|
53
|
+
const units = ['KB', 'MB', 'GB'];
|
|
54
|
+
let size = bytes / 1024;
|
|
55
|
+
let unit = 0;
|
|
56
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
57
|
+
size /= 1024;
|
|
58
|
+
unit += 1;
|
|
59
|
+
}
|
|
60
|
+
return `${size.toFixed(size >= 10 || unit === 0 ? 0 : 1)} ${units.at(unit) ?? ''}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isAccepted(file: File): boolean {
|
|
64
|
+
if (accept.length === 0 || accept === '*' || accept === '*/*') {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const patterns = accept.split(',').map((pattern) => pattern.trim().toLowerCase());
|
|
68
|
+
const name = file.name.toLowerCase();
|
|
69
|
+
const mime = file.type.toLowerCase();
|
|
70
|
+
return patterns.some((pattern) => {
|
|
71
|
+
if (pattern.startsWith('.')) {
|
|
72
|
+
return name.endsWith(pattern);
|
|
73
|
+
}
|
|
74
|
+
if (pattern.endsWith('/*')) {
|
|
75
|
+
return mime.startsWith(pattern.slice(0, -1));
|
|
76
|
+
}
|
|
77
|
+
return mime === pattern;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function appendItem(file: File): void {
|
|
82
|
+
const isImage = file.type.startsWith('image/');
|
|
83
|
+
const item: MediaUploadItem = {
|
|
84
|
+
file,
|
|
85
|
+
src: '',
|
|
86
|
+
name: file.name,
|
|
87
|
+
size: file.size,
|
|
88
|
+
isImage
|
|
89
|
+
};
|
|
90
|
+
items = [...items, item];
|
|
91
|
+
const updatedItem = items[items.length - 1];
|
|
92
|
+
|
|
93
|
+
if (isImage) {
|
|
94
|
+
const reader = new FileReader();
|
|
95
|
+
reader.onload = (event) => {
|
|
96
|
+
if (typeof event.target?.result === 'string') {
|
|
97
|
+
updatedItem.src = event.target.result;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
reader.readAsDataURL(file);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildError(rejections: MediaUploadRejection[]): string {
|
|
105
|
+
const reason = rejections.at(0)?.reason;
|
|
106
|
+
if (reason === 'type') {
|
|
107
|
+
return errorMessages?.type ?? 'Some files were skipped — unsupported file type.';
|
|
108
|
+
}
|
|
109
|
+
if (reason === 'size') {
|
|
110
|
+
const limit = maxFileSize > 0 ? ` (max ${formatSize(maxFileSize)})` : '';
|
|
111
|
+
return errorMessages?.size ?? `Some files were too large${limit}.`;
|
|
112
|
+
}
|
|
113
|
+
return errorMessages?.max ?? `You can attach up to ${maxLength} files.`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function addFiles(selected: File[]): void {
|
|
117
|
+
if (disabled) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const rejections: MediaUploadRejection[] = [];
|
|
121
|
+
|
|
122
|
+
for (const file of selected) {
|
|
123
|
+
if (items.length >= maxLength) {
|
|
124
|
+
rejections.push({ file, reason: 'max' });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!isAccepted(file)) {
|
|
128
|
+
rejections.push({ file, reason: 'type' });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (maxFileSize > 0 && file.size > maxFileSize) {
|
|
132
|
+
rejections.push({ file, reason: 'size' });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
appendItem(file);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
error = rejections.length > 0 ? buildError(rejections) : '';
|
|
139
|
+
if (rejections.length > 0) {
|
|
140
|
+
onRejected?.(rejections);
|
|
141
|
+
}
|
|
142
|
+
syncFiles();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function handleInputChange(event: Event & { currentTarget: HTMLInputElement }): void {
|
|
146
|
+
const target = event.currentTarget;
|
|
147
|
+
addFiles(Array.from(target.files ?? []));
|
|
148
|
+
target.value = '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handleDrop(event: DragEvent): void {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
dragging = false;
|
|
154
|
+
if (disabled || !dragAndDrop) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
addFiles(Array.from(event.dataTransfer?.files ?? []));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function handleDragOver(event: DragEvent): void {
|
|
161
|
+
if (disabled || !dragAndDrop) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
dragging = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleDragLeave(): void {
|
|
169
|
+
dragging = false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function removeImage(index: number): void {
|
|
173
|
+
const removed = items.at(index);
|
|
174
|
+
items = items.filter((_, position) => position !== index);
|
|
175
|
+
error = '';
|
|
176
|
+
syncFiles();
|
|
177
|
+
if (typeof removed === 'object') {
|
|
178
|
+
onRemove?.(removed.file);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
</script>
|
|
182
|
+
|
|
183
|
+
<div
|
|
184
|
+
class="media-upload {classes ?? ''}"
|
|
185
|
+
class:disabled
|
|
186
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
187
|
+
>
|
|
188
|
+
{#if (typeof label === 'string' && label.length > 0) || showCounter}
|
|
189
|
+
<div class="header">
|
|
190
|
+
{#if typeof label === 'string' && label.length > 0}
|
|
191
|
+
<div class="label">{label}</div>
|
|
192
|
+
{/if}
|
|
193
|
+
{#if showCounter}
|
|
194
|
+
<div class="counter">{items.length} / {maxLength}</div>
|
|
195
|
+
{/if}
|
|
196
|
+
</div>
|
|
197
|
+
{/if}
|
|
198
|
+
{#if typeof description === 'string' && description.length > 0}
|
|
199
|
+
<div class="description">{description}</div>
|
|
200
|
+
{/if}
|
|
201
|
+
|
|
202
|
+
<div class="grid">
|
|
203
|
+
{#each items as item, index (item.file.name + index)}
|
|
204
|
+
<div class="card" class:is-file={!item.isImage}>
|
|
205
|
+
{#if item.isImage}
|
|
206
|
+
{#if item.src.length > 0}
|
|
207
|
+
<span class="thumb">
|
|
208
|
+
<Img src={item.src} alt={item.name} />
|
|
209
|
+
</span>
|
|
210
|
+
{:else}
|
|
211
|
+
<span class="thumb-loading">
|
|
212
|
+
<Shimmer />
|
|
213
|
+
</span>
|
|
214
|
+
{/if}
|
|
215
|
+
{:else}
|
|
216
|
+
<div class="file-icon">
|
|
217
|
+
{#if typeof fileIcon === 'function'}
|
|
218
|
+
{@render fileIcon()}
|
|
219
|
+
{:else}
|
|
220
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
221
|
+
{@html fileSvg}
|
|
222
|
+
{/if}
|
|
223
|
+
</div>
|
|
224
|
+
{/if}
|
|
225
|
+
|
|
226
|
+
{#if (showFileName || showFileSize) && (showFileName || item.size > 0)}
|
|
227
|
+
<div class="meta">
|
|
228
|
+
{#if showFileName}
|
|
229
|
+
<span class="meta-name">{item.name}</span>
|
|
230
|
+
{/if}
|
|
231
|
+
{#if showFileSize && item.size > 0}
|
|
232
|
+
<span class="meta-size">{formatSize(item.size)}</span>
|
|
233
|
+
{/if}
|
|
234
|
+
</div>
|
|
235
|
+
{/if}
|
|
236
|
+
|
|
237
|
+
{#if !disabled}
|
|
238
|
+
<div class="remove">
|
|
239
|
+
<Button onclick={() => removeImage(index)} ariaLabel="Remove {item.name}">
|
|
240
|
+
{#if typeof removeIcon === 'function'}
|
|
241
|
+
{@render removeIcon()}
|
|
242
|
+
{:else}
|
|
243
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
244
|
+
{@html closeSvg}
|
|
245
|
+
{/if}
|
|
246
|
+
</Button>
|
|
247
|
+
</div>
|
|
248
|
+
{/if}
|
|
249
|
+
</div>
|
|
250
|
+
{/each}
|
|
251
|
+
|
|
252
|
+
{#if showDropTile}
|
|
253
|
+
<label
|
|
254
|
+
for={inputId}
|
|
255
|
+
class="drop-tile"
|
|
256
|
+
class:dragging
|
|
257
|
+
ondragover={handleDragOver}
|
|
258
|
+
ondragleave={handleDragLeave}
|
|
259
|
+
ondrop={handleDrop}
|
|
260
|
+
>
|
|
261
|
+
<span class="drop-icon">
|
|
262
|
+
{#if typeof addIcon === 'function'}
|
|
263
|
+
{@render addIcon()}
|
|
264
|
+
{:else}
|
|
265
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
266
|
+
{@html addSvg}
|
|
267
|
+
{/if}
|
|
268
|
+
</span>
|
|
269
|
+
{#if typeof addText === 'string' && addText.length > 0}
|
|
270
|
+
<span class="drop-text">{addText}</span>
|
|
271
|
+
{/if}
|
|
272
|
+
{#if typeof hintText === 'string' && hintText.length > 0}
|
|
273
|
+
<span class="drop-hint">{hintText}</span>
|
|
274
|
+
{/if}
|
|
275
|
+
<input
|
|
276
|
+
id={inputId}
|
|
277
|
+
type="file"
|
|
278
|
+
{accept}
|
|
279
|
+
{multiple}
|
|
280
|
+
{disabled}
|
|
281
|
+
class="file-input"
|
|
282
|
+
onchange={handleInputChange}
|
|
283
|
+
hidden
|
|
284
|
+
/>
|
|
285
|
+
</label>
|
|
286
|
+
{/if}
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
{#if error.length > 0}
|
|
290
|
+
<div class="error" role="alert">{error}</div>
|
|
291
|
+
{/if}
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<style>
|
|
295
|
+
.media-upload {
|
|
296
|
+
box-sizing: border-box;
|
|
297
|
+
display: flex;
|
|
298
|
+
flex-direction: column;
|
|
299
|
+
width: var(--media-upload-width, fit-content);
|
|
300
|
+
font-family: var(--media-upload-font-family, inherit);
|
|
301
|
+
color: var(--media-upload-color, inherit);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.media-upload.disabled {
|
|
305
|
+
opacity: var(--media-upload-disabled-opacity, 0.55);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.header {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: baseline;
|
|
311
|
+
justify-content: space-between;
|
|
312
|
+
gap: var(--media-upload-header-gap, 12px);
|
|
313
|
+
margin: var(--media-upload-label-margin, 0 0 4px 0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.label {
|
|
317
|
+
font-size: var(--media-upload-label-font-size, 14px);
|
|
318
|
+
font-weight: var(--media-upload-label-font-weight, 600);
|
|
319
|
+
color: var(--media-upload-label-color, #242833);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.counter {
|
|
323
|
+
font-size: var(--media-upload-counter-font-size, 12px);
|
|
324
|
+
font-weight: var(--media-upload-counter-font-weight, 500);
|
|
325
|
+
color: var(--media-upload-counter-color, #8a8f98);
|
|
326
|
+
font-variant-numeric: tabular-nums;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.description {
|
|
330
|
+
font-size: var(--media-upload-description-font-size, 12px);
|
|
331
|
+
color: var(--media-upload-description-color, #656565);
|
|
332
|
+
margin: var(--media-upload-description-margin, 0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.grid {
|
|
336
|
+
display: flex;
|
|
337
|
+
flex-wrap: wrap;
|
|
338
|
+
align-items: stretch;
|
|
339
|
+
gap: var(--media-upload-gap, 12px);
|
|
340
|
+
margin: var(--media-upload-content-margin, 12px 0 0 0);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.card,
|
|
344
|
+
.drop-tile {
|
|
345
|
+
box-sizing: border-box;
|
|
346
|
+
height: var(--media-upload-item-height, 110px);
|
|
347
|
+
width: var(--media-upload-item-width, 110px);
|
|
348
|
+
border-radius: var(--media-upload-item-border-radius, 14px);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.card {
|
|
352
|
+
position: relative;
|
|
353
|
+
overflow: hidden;
|
|
354
|
+
border: var(--media-upload-item-border, 1px solid #e4e4e7);
|
|
355
|
+
background-color: var(--media-upload-item-background-color, #fafafa);
|
|
356
|
+
box-shadow: var(--media-upload-item-box-shadow, 0 1px 2px rgba(0, 0, 0, 0.06));
|
|
357
|
+
transition: var(--media-upload-item-transition, box-shadow 0.2s ease, transform 0.2s ease);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.card:hover {
|
|
361
|
+
box-shadow: var(--media-upload-item-hover-box-shadow, 0 6px 18px rgba(0, 0, 0, 0.14));
|
|
362
|
+
transform: var(--media-upload-item-hover-transform, translateY(-2px));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.thumb {
|
|
366
|
+
display: contents;
|
|
367
|
+
--image-width: 100%;
|
|
368
|
+
--image-height: 100%;
|
|
369
|
+
--image-object-fit: var(--media-upload-item-object-fit, cover);
|
|
370
|
+
--image-border-radius: 0px;
|
|
371
|
+
--image-padding: 0px;
|
|
372
|
+
--image-margin: 0px;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.thumb-loading {
|
|
376
|
+
display: contents;
|
|
377
|
+
--shimmer-width: 100%;
|
|
378
|
+
--shimmer-height: 100%;
|
|
379
|
+
--shimmer-border-radius: 0px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.file-icon {
|
|
383
|
+
display: flex;
|
|
384
|
+
height: 100%;
|
|
385
|
+
width: 100%;
|
|
386
|
+
justify-content: center;
|
|
387
|
+
align-items: center;
|
|
388
|
+
color: var(--media-upload-file-icon-color, #8a8f98);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.file-icon :global(svg),
|
|
392
|
+
.file-icon :global(img) {
|
|
393
|
+
height: var(--media-upload-file-icon-size, 36px);
|
|
394
|
+
width: var(--media-upload-file-icon-size, 36px);
|
|
395
|
+
object-fit: contain;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.meta {
|
|
399
|
+
position: absolute;
|
|
400
|
+
left: 0;
|
|
401
|
+
right: 0;
|
|
402
|
+
bottom: 0;
|
|
403
|
+
display: flex;
|
|
404
|
+
flex-direction: column;
|
|
405
|
+
gap: 2px;
|
|
406
|
+
box-sizing: border-box;
|
|
407
|
+
padding: var(--media-upload-meta-padding, 6px 8px);
|
|
408
|
+
background: var(
|
|
409
|
+
--media-upload-meta-background,
|
|
410
|
+
linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0))
|
|
411
|
+
);
|
|
412
|
+
color: var(--media-upload-meta-color, #ffffff);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.is-file .meta {
|
|
416
|
+
background: var(--media-upload-file-meta-background, transparent);
|
|
417
|
+
color: var(--media-upload-file-meta-color, #52525b);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.meta-name {
|
|
421
|
+
font-size: var(--media-upload-meta-name-font-size, 11px);
|
|
422
|
+
font-weight: var(--media-upload-meta-name-font-weight, 500);
|
|
423
|
+
white-space: nowrap;
|
|
424
|
+
overflow: hidden;
|
|
425
|
+
text-overflow: ellipsis;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.meta-size {
|
|
429
|
+
font-size: var(--media-upload-meta-size-font-size, 10px);
|
|
430
|
+
opacity: var(--media-upload-meta-size-opacity, 0.85);
|
|
431
|
+
font-variant-numeric: tabular-nums;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.remove {
|
|
435
|
+
position: absolute;
|
|
436
|
+
top: var(--media-upload-remove-inset, 6px);
|
|
437
|
+
right: var(--media-upload-remove-inset, 6px);
|
|
438
|
+
opacity: var(--media-upload-remove-opacity, 0);
|
|
439
|
+
transition: var(--media-upload-remove-transition, opacity 0.18s ease);
|
|
440
|
+
--button-width: var(--media-upload-remove-size, 24px);
|
|
441
|
+
--button-height: var(--media-upload-remove-size, 24px);
|
|
442
|
+
--button-padding: var(--media-upload-remove-padding, 4px);
|
|
443
|
+
--button-border: var(--media-upload-remove-border, none);
|
|
444
|
+
--button-border-radius: var(--media-upload-remove-border-radius, 50%);
|
|
445
|
+
--button-color: var(--media-upload-remove-background-color, rgba(0, 0, 0, 0.55));
|
|
446
|
+
--button-text-color: var(--media-upload-remove-color, #ffffff);
|
|
447
|
+
--button-content-gap: 0px;
|
|
448
|
+
--button-hover-color: var(--media-upload-remove-hover-background-color, rgba(0, 0, 0, 0.78));
|
|
449
|
+
--button-hover-text-color: var(
|
|
450
|
+
--media-upload-remove-hover-color,
|
|
451
|
+
var(--media-upload-remove-color, #ffffff)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.card:hover .remove,
|
|
456
|
+
.remove:focus-within {
|
|
457
|
+
opacity: 1;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.remove :global(svg),
|
|
461
|
+
.remove :global(img) {
|
|
462
|
+
height: var(--media-upload-remove-icon-size, 100%);
|
|
463
|
+
width: var(--media-upload-remove-icon-size, 100%);
|
|
464
|
+
object-fit: contain;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.drop-tile {
|
|
468
|
+
display: flex;
|
|
469
|
+
flex-direction: column;
|
|
470
|
+
justify-content: center;
|
|
471
|
+
align-items: center;
|
|
472
|
+
gap: var(--media-upload-add-gap, 6px);
|
|
473
|
+
box-sizing: border-box;
|
|
474
|
+
padding: var(--media-upload-add-padding, 12px);
|
|
475
|
+
text-align: center;
|
|
476
|
+
background-color: var(--media-upload-add-background-color, #fafafa);
|
|
477
|
+
border: var(--media-upload-add-border, 1.5px dashed #c8ccd2);
|
|
478
|
+
color: var(--media-upload-add-color, #6b7280);
|
|
479
|
+
cursor: pointer;
|
|
480
|
+
transition: var(
|
|
481
|
+
--media-upload-add-transition,
|
|
482
|
+
border-color 0.18s ease,
|
|
483
|
+
background-color 0.18s ease,
|
|
484
|
+
color 0.18s ease
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.drop-tile:hover {
|
|
489
|
+
background-color: var(--media-upload-add-hover-background-color, #f1f5ff);
|
|
490
|
+
border: var(--media-upload-add-hover-border, 1.5px dashed #6d8eff);
|
|
491
|
+
color: var(--media-upload-add-hover-color, #3b5bdb);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.drop-tile.dragging {
|
|
495
|
+
background-color: var(--media-upload-add-dragging-background-color, #e7efff);
|
|
496
|
+
border: var(--media-upload-add-dragging-border, 1.5px solid #3b5bdb);
|
|
497
|
+
color: var(--media-upload-add-dragging-color, #3b5bdb);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.drop-icon {
|
|
501
|
+
display: flex;
|
|
502
|
+
justify-content: center;
|
|
503
|
+
align-items: center;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.drop-icon :global(svg),
|
|
507
|
+
.drop-icon :global(img) {
|
|
508
|
+
height: var(--media-upload-add-icon-size, 22px);
|
|
509
|
+
width: var(--media-upload-add-icon-size, 22px);
|
|
510
|
+
object-fit: contain;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.drop-text {
|
|
514
|
+
font-size: var(--media-upload-add-text-font-size, 11px);
|
|
515
|
+
line-height: var(--media-upload-add-text-line-height, 1.3);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.drop-hint {
|
|
519
|
+
font-size: var(--media-upload-add-hint-font-size, 10px);
|
|
520
|
+
color: var(--media-upload-add-hint-color, #9aa0a8);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.error {
|
|
524
|
+
font-size: var(--media-upload-error-font-size, 12px);
|
|
525
|
+
color: var(--media-upload-error-color, #e0334b);
|
|
526
|
+
margin: var(--media-upload-error-margin, 8px 0 0 0);
|
|
527
|
+
}
|
|
528
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type MediaUploadProperties = OptionalMediaUploadProperties & MediaUploadEventProperties;
|
|
3
|
+
export type MediaUploadItem = {
|
|
4
|
+
file: File;
|
|
5
|
+
src: string;
|
|
6
|
+
name: string;
|
|
7
|
+
size: number;
|
|
8
|
+
isImage: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type MediaUploadRejectionReason = 'type' | 'size' | 'max';
|
|
11
|
+
export type MediaUploadRejection = {
|
|
12
|
+
file: File;
|
|
13
|
+
reason: MediaUploadRejectionReason;
|
|
14
|
+
};
|
|
15
|
+
export type MediaUploadErrorMessages = {
|
|
16
|
+
type?: string;
|
|
17
|
+
size?: string;
|
|
18
|
+
max?: string;
|
|
19
|
+
};
|
|
20
|
+
export type OptionalMediaUploadProperties = {
|
|
21
|
+
label?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
addText?: string;
|
|
24
|
+
hintText?: string;
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
accept?: string;
|
|
27
|
+
maxFileSize?: number;
|
|
28
|
+
multiple?: boolean;
|
|
29
|
+
dragAndDrop?: boolean;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
showCounter?: boolean;
|
|
32
|
+
showFileName?: boolean;
|
|
33
|
+
showFileSize?: boolean;
|
|
34
|
+
addIcon?: Snippet;
|
|
35
|
+
removeIcon?: Snippet;
|
|
36
|
+
fileIcon?: Snippet;
|
|
37
|
+
errorMessages?: MediaUploadErrorMessages;
|
|
38
|
+
files?: File[];
|
|
39
|
+
testId?: string;
|
|
40
|
+
classes?: string;
|
|
41
|
+
};
|
|
42
|
+
export type MediaUploadEventProperties = {
|
|
43
|
+
onFilesChange?: (files: File[]) => void;
|
|
44
|
+
onRemove?: (file: File) => void;
|
|
45
|
+
onRejected?: (rejections: MediaUploadRejection[]) => void;
|
|
46
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/Modal/Modal.svelte
CHANGED
|
@@ -36,9 +36,13 @@
|
|
|
36
36
|
classes,
|
|
37
37
|
overlayBackdropFilter,
|
|
38
38
|
overlayFadeIn = false,
|
|
39
|
-
usePortal = false
|
|
39
|
+
usePortal = false,
|
|
40
|
+
lockScroll = true,
|
|
41
|
+
autoDismissAfter = null
|
|
40
42
|
}: ModalProperties = $props();
|
|
41
43
|
|
|
44
|
+
let dismissTimer: ReturnType<typeof setTimeout> | null = null;
|
|
45
|
+
|
|
42
46
|
// Fix [major]: plain const so the debouncer closure retains its internal lastCallTime state
|
|
43
47
|
// across re-renders. $derived would recreate the debouncer on every reactive re-evaluation,
|
|
44
48
|
// resetting the timer and breaking debounce correctness.
|
|
@@ -117,7 +121,12 @@
|
|
|
117
121
|
};
|
|
118
122
|
|
|
119
123
|
onMount(() => {
|
|
120
|
-
|
|
124
|
+
if (lockScroll) {
|
|
125
|
+
document.body.style.overflow = 'hidden';
|
|
126
|
+
}
|
|
127
|
+
if (typeof autoDismissAfter === 'number') {
|
|
128
|
+
dismissTimer = setTimeout(() => onclose?.(), autoDismissAfter);
|
|
129
|
+
}
|
|
121
130
|
if (supportHardwareBackPress) {
|
|
122
131
|
history.pushState(null, '', window.location.href);
|
|
123
132
|
window.addEventListener('popstate', handlePopstate);
|
|
@@ -125,8 +134,13 @@
|
|
|
125
134
|
});
|
|
126
135
|
|
|
127
136
|
onDestroy(() => {
|
|
137
|
+
if (dismissTimer !== null) {
|
|
138
|
+
clearTimeout(dismissTimer);
|
|
139
|
+
}
|
|
128
140
|
if (typeof window !== 'undefined') {
|
|
129
|
-
|
|
141
|
+
if (lockScroll) {
|
|
142
|
+
document.body.style.overflow = '';
|
|
143
|
+
}
|
|
130
144
|
if (supportHardwareBackPress) {
|
|
131
145
|
if (!backPressed) {
|
|
132
146
|
history.back();
|
|
@@ -48,6 +48,10 @@ export type OptionalModalProperties = {
|
|
|
48
48
|
overlayFadeIn?: boolean;
|
|
49
49
|
/** When true, mounts the overlay at document.body to escape clipping/stacking contexts. Default: false. */
|
|
50
50
|
usePortal?: boolean;
|
|
51
|
+
/** When true, locks document.body scroll while the modal is mounted, restoring it on unmount. Default: true (matches prior unconditional behavior). Set false for a non-blocking modal that should allow background scroll. */
|
|
52
|
+
lockScroll?: boolean;
|
|
53
|
+
/** Milliseconds after mount to automatically fire onclose, e.g. for a transient success/confirmation modal. null (default) disables auto-dismiss. */
|
|
54
|
+
autoDismissAfter?: number | null;
|
|
51
55
|
};
|
|
52
56
|
export type ModalEventProperties = {
|
|
53
57
|
onclose?: () => void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M3 6h18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<line x1="10" y1="11" x2="10" y2="17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
+
<line x1="14" y1="11" x2="14" y2="17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="m15 5 4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor" stroke="none" />
|
|
3
|
+
<line x1="16" y1="9" x2="21" y2="14" />
|
|
4
|
+
<line x1="21" y1="9" x2="16" y2="14" />
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor" stroke="none" />
|
|
3
|
+
<path d="M16 8.5a4 4 0 0 1 0 7" />
|
|
4
|
+
<path d="M18.5 6a7 7 0 0 1 0 12" />
|
|
5
|
+
</svg>
|