@makolabs/ripple 1.13.0 → 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.
@@ -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
- 'group relative block rounded-[32px] border-2 border-dashed p-12 text-center transition-colors',
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="flex flex-col items-center gap-4">
103
+ <div class={slots.content()}>
100
104
  <!-- 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
- >
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
- <!-- 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)
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
- (10MB each)
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>
@@ -0,0 +1,4 @@
1
+ import type { MultiFileUploadProps } from '../../index.js';
2
+ declare const MultiFileUpload: import("svelte").Component<MultiFileUploadProps, {}, "files">;
3
+ type MultiFileUpload = ReturnType<typeof MultiFileUpload>;
4
+ export default MultiFileUpload;
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {