@makolabs/ripple 1.13.1 → 2.0.0

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