@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.
@@ -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,4 @@
1
+ import type { MediaUploadProperties } from './properties';
2
+ declare const MediaUpload: import("svelte").Component<MediaUploadProperties, {}, "files">;
3
+ type MediaUpload = ReturnType<typeof MediaUpload>;
4
+ export default MediaUpload;
@@ -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 {};
@@ -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
- document.body.style.overflow = 'hidden';
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
- document.body.style.overflow = '';
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,4 @@
1
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" xmlns="http://www.w3.org/2000/svg">
2
+ <line x1="12" y1="5" x2="12" y2="19" />
3
+ <line x1="5" y1="12" x2="19" y2="12" />
4
+ </svg>
@@ -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,4 @@
1
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M14 3v5h5" />
3
+ <path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5z" />
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,4 @@
1
+ <svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="6" y="5" width="4" height="14" rx="1" />
3
+ <rect x="14" y="5" width="4" height="14" rx="1" />
4
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M8 5v14l11-7z" />
3
+ </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>