@timmbr/ui 1.1.0 → 1.2.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.
Files changed (76) hide show
  1. package/dist/components/Accordion/Accordion.d.ts.map +1 -1
  2. package/dist/components/Alert/Alert.types.d.ts +2 -4
  3. package/dist/components/Alert/Alert.types.d.ts.map +1 -1
  4. package/dist/components/AlertDialog/AlertDialog.d.ts +16 -0
  5. package/dist/components/AlertDialog/AlertDialog.d.ts.map +1 -0
  6. package/dist/components/AlertDialog/AlertDialog.js +94 -0
  7. package/dist/components/AlertDialog/AlertDialog.styles.d.ts +8 -0
  8. package/dist/components/AlertDialog/AlertDialog.styles.d.ts.map +1 -0
  9. package/dist/components/AlertDialog/AlertDialog.styles.js +25 -0
  10. package/dist/components/AlertDialog/AlertDialog.types.d.ts +16 -0
  11. package/dist/components/AlertDialog/AlertDialog.types.d.ts.map +1 -0
  12. package/dist/components/AlertDialog/index.d.ts +4 -0
  13. package/dist/components/AlertDialog/index.d.ts.map +1 -0
  14. package/dist/components/Avatar/Avatar.styles.d.ts +1 -1
  15. package/dist/components/Dialog/Dialog.d.ts +2 -1
  16. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  17. package/dist/components/Dialog/Dialog.js +8 -6
  18. package/dist/components/Dialog/Dialog.styles.d.ts +1 -0
  19. package/dist/components/Dialog/Dialog.styles.d.ts.map +1 -1
  20. package/dist/components/Dialog/Dialog.styles.js +7 -4
  21. package/dist/components/Dialog/Dialog.types.d.ts +4 -6
  22. package/dist/components/Dialog/Dialog.types.d.ts.map +1 -1
  23. package/dist/components/Drawer/Drawer.types.d.ts +3 -6
  24. package/dist/components/Drawer/Drawer.types.d.ts.map +1 -1
  25. package/dist/components/Dropdown/Dropdown.types.d.ts +1 -2
  26. package/dist/components/Dropdown/Dropdown.types.d.ts.map +1 -1
  27. package/dist/components/FormField/FormField.js +3 -3
  28. package/dist/components/ImageUpload/ImageUpload.d.ts +4 -0
  29. package/dist/components/ImageUpload/ImageUpload.d.ts.map +1 -0
  30. package/dist/components/ImageUpload/ImageUpload.helpers.d.ts +30 -0
  31. package/dist/components/ImageUpload/ImageUpload.helpers.d.ts.map +1 -0
  32. package/dist/components/ImageUpload/ImageUpload.helpers.js +95 -0
  33. package/dist/components/ImageUpload/ImageUpload.js +365 -0
  34. package/dist/components/ImageUpload/ImageUpload.styles.d.ts +17 -0
  35. package/dist/components/ImageUpload/ImageUpload.styles.d.ts.map +1 -0
  36. package/dist/components/ImageUpload/ImageUpload.styles.js +72 -0
  37. package/dist/components/ImageUpload/ImageUpload.types.d.ts +112 -0
  38. package/dist/components/ImageUpload/ImageUpload.types.d.ts.map +1 -0
  39. package/dist/components/ImageUpload/index.d.ts +6 -0
  40. package/dist/components/ImageUpload/index.d.ts.map +1 -0
  41. package/dist/components/ImageUpload/subcomponents/ImageUploadDropzone.d.ts +25 -0
  42. package/dist/components/ImageUpload/subcomponents/ImageUploadDropzone.d.ts.map +1 -0
  43. package/dist/components/ImageUpload/subcomponents/ImageUploadDropzone.js +106 -0
  44. package/dist/components/ImageUpload/subcomponents/ImageUploadIcons.d.ts +6 -0
  45. package/dist/components/ImageUpload/subcomponents/ImageUploadIcons.d.ts.map +1 -0
  46. package/dist/components/ImageUpload/subcomponents/ImageUploadIcons.js +61 -0
  47. package/dist/components/ImageUpload/subcomponents/ImageUploadModal.d.ts +12 -0
  48. package/dist/components/ImageUpload/subcomponents/ImageUploadModal.d.ts.map +1 -0
  49. package/dist/components/ImageUpload/subcomponents/ImageUploadModal.js +137 -0
  50. package/dist/components/ImageUpload/subcomponents/ImageUploadSinglePreview.d.ts +14 -0
  51. package/dist/components/ImageUpload/subcomponents/ImageUploadSinglePreview.d.ts.map +1 -0
  52. package/dist/components/ImageUpload/subcomponents/ImageUploadSinglePreview.js +142 -0
  53. package/dist/components/ImageUpload/subcomponents/ImageUploadThumbnailCard.d.ts +23 -0
  54. package/dist/components/ImageUpload/subcomponents/ImageUploadThumbnailCard.d.ts.map +1 -0
  55. package/dist/components/ImageUpload/subcomponents/ImageUploadThumbnailCard.js +139 -0
  56. package/dist/components/ImageUpload/subcomponents/index.d.ts +6 -0
  57. package/dist/components/ImageUpload/subcomponents/index.d.ts.map +1 -0
  58. package/dist/components/Input/Input.js +3 -3
  59. package/dist/components/List/List.types.d.ts +2 -4
  60. package/dist/components/List/List.types.d.ts.map +1 -1
  61. package/dist/components/Popover/Popover.types.d.ts +1 -2
  62. package/dist/components/Popover/Popover.types.d.ts.map +1 -1
  63. package/dist/components/Radio/Radio.js +3 -3
  64. package/dist/components/Select/Select.js +3 -3
  65. package/dist/components/Stat/Stat.types.d.ts +3 -6
  66. package/dist/components/Stat/Stat.types.d.ts.map +1 -1
  67. package/dist/components/Table/Table.types.d.ts +7 -14
  68. package/dist/components/Table/Table.types.d.ts.map +1 -1
  69. package/dist/components/Textarea/Textarea.js +3 -3
  70. package/dist/components/Toast/Toast.js +1 -1
  71. package/dist/components/Toast/Toast.types.d.ts +1 -2
  72. package/dist/components/Toast/Toast.types.d.ts.map +1 -1
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +12 -2
  76. package/package.json +5 -4
@@ -0,0 +1,365 @@
1
+ "use client";
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import * as React from 'react';
4
+ import { cn } from '@timmbr/utils';
5
+ import { AlertCircle } from '@timmbr/icons';
6
+ import { LayoutGroup, AnimatePresence } from '@timmbr/motion';
7
+ import { resolveMotion } from '../../types/motion.js';
8
+ import { DEFAULT_VIDEO_ACCEPT, DEFAULT_ALL_ACCEPT, DEFAULT_IMAGE_ACCEPT, DEFAULT_VIDEO_MAX_SIZE, DEFAULT_IMAGE_MAX_SIZE, normalizeUploadItems, validateUploadFiles, isVideoMedia, GRID_COLS_MAP } from './ImageUpload.helpers.js';
9
+ import { useGlobalAnimation } from '../../providers/AnimationProvider.js';
10
+ import { ImageUploadSinglePreview } from './subcomponents/ImageUploadSinglePreview.js';
11
+ import { ImageUploadDropzone } from './subcomponents/ImageUploadDropzone.js';
12
+ import { ImageUploadThumbnailCard, ImageUploadTaskCard } from './subcomponents/ImageUploadThumbnailCard.js';
13
+ import { ImageUploadModal } from './subcomponents/ImageUploadModal.js';
14
+
15
+ const ImageUpload = React.forwardRef(
16
+ ({
17
+ className,
18
+ style,
19
+ value,
20
+ multiple = false,
21
+ mediaType = "image",
22
+ minFiles,
23
+ maxFiles,
24
+ onChange,
25
+ onUpload,
26
+ onRemove,
27
+ disabled = false,
28
+ loading = false,
29
+ loadingText,
30
+ accept: customAccept,
31
+ maxSizeBytes: customMaxSize,
32
+ label,
33
+ helperText,
34
+ error: externalError,
35
+ aspectRatio = "auto",
36
+ gridCols = 4,
37
+ layout = "grid",
38
+ enablePreviewModal = true,
39
+ motion: motionProp,
40
+ ...props
41
+ }, ref) => {
42
+ const fileInputRef = React.useRef(null);
43
+ const dragCounterRef = React.useRef(0);
44
+ const globalMotion = useGlobalAnimation();
45
+ const { shouldAnimate, motionClass, dataAttributes } = resolveMotion(
46
+ motionProp,
47
+ globalMotion
48
+ );
49
+ const [isDragging, setIsDragging] = React.useState(false);
50
+ const [uploadingTasks, setUploadingTasks] = React.useState([]);
51
+ const [localError, setLocalError] = React.useState(null);
52
+ const [previewModalIndex, setPreviewModalIndex] = React.useState(null);
53
+ const accept = React.useMemo(() => {
54
+ if (customAccept) return customAccept;
55
+ if (mediaType === "video") return DEFAULT_VIDEO_ACCEPT;
56
+ if (mediaType === "all") return DEFAULT_ALL_ACCEPT;
57
+ return DEFAULT_IMAGE_ACCEPT;
58
+ }, [customAccept, mediaType]);
59
+ const maxSizeBytes = React.useMemo(() => {
60
+ if (customMaxSize) return customMaxSize;
61
+ return mediaType === "video" || mediaType === "all" ? DEFAULT_VIDEO_MAX_SIZE : DEFAULT_IMAGE_MAX_SIZE;
62
+ }, [customMaxSize, mediaType]);
63
+ const defaultLoadingText = loadingText || (mediaType === "video" ? "Uploading video..." : "Uploading image...");
64
+ const items = React.useMemo(
65
+ () => normalizeUploadItems(value),
66
+ [value]
67
+ );
68
+ const isGlobalUploading = loading || uploadingTasks.length > 0;
69
+ const currentCount = items.length + uploadingTasks.length;
70
+ const isMaxReached = Boolean(maxFiles && currentCount >= maxFiles);
71
+ const minFilesWarning = minFiles && items.length > 0 && items.length < minFiles ? `At least ${minFiles} items are required (currently ${items.length}).` : null;
72
+ const error = externalError || localError;
73
+ const isError = Boolean(error);
74
+ React.useEffect(() => {
75
+ return () => {
76
+ uploadingTasks.forEach((task) => {
77
+ if (task.previewUrl.startsWith("blob:")) {
78
+ URL.revokeObjectURL(task.previewUrl);
79
+ }
80
+ });
81
+ };
82
+ }, [uploadingTasks]);
83
+ const handleFiles = React.useCallback(
84
+ async (rawFiles) => {
85
+ setLocalError(null);
86
+ const filesToProcess = multiple ? rawFiles : rawFiles.slice(0, 1);
87
+ const { validFiles, errorMessage } = validateUploadFiles({
88
+ files: filesToProcess,
89
+ accept,
90
+ maxSizeBytes,
91
+ maxFiles,
92
+ currentCount
93
+ });
94
+ if (errorMessage) {
95
+ setLocalError(errorMessage);
96
+ return;
97
+ }
98
+ if (validFiles.length === 0) return;
99
+ onChange?.(multiple ? validFiles : validFiles[0]);
100
+ if (onUpload) {
101
+ const newTasks = validFiles.map((file) => ({
102
+ id: `${file.name}-${Date.now()}-${Math.random()}`,
103
+ name: file.name,
104
+ previewUrl: URL.createObjectURL(file),
105
+ isVideo: file.type.startsWith("video/") || isVideoMedia(file.name)
106
+ }));
107
+ setUploadingTasks((prev) => [...prev, ...newTasks]);
108
+ await Promise.allSettled(
109
+ validFiles.map(async (file, idx) => {
110
+ const taskId = newTasks[idx].id;
111
+ try {
112
+ await onUpload(file);
113
+ } catch (err) {
114
+ const msg = err instanceof Error ? err.message : `Failed to upload "${file.name}".`;
115
+ setLocalError(msg);
116
+ } finally {
117
+ setUploadingTasks((prev) => {
118
+ const task = prev.find((t) => t.id === taskId);
119
+ if (task?.previewUrl.startsWith("blob:")) {
120
+ URL.revokeObjectURL(task.previewUrl);
121
+ }
122
+ return prev.filter((t) => t.id !== taskId);
123
+ });
124
+ }
125
+ })
126
+ );
127
+ }
128
+ },
129
+ [accept, currentCount, maxFiles, maxSizeBytes, multiple, onChange, onUpload]
130
+ );
131
+ const handleDragEnter = (e) => {
132
+ e.preventDefault();
133
+ e.stopPropagation();
134
+ dragCounterRef.current += 1;
135
+ if (!disabled && !isGlobalUploading && !isMaxReached) {
136
+ setIsDragging(true);
137
+ }
138
+ };
139
+ const handleDragOver = (e) => {
140
+ e.preventDefault();
141
+ e.stopPropagation();
142
+ };
143
+ const handleDragLeave = (e) => {
144
+ e.preventDefault();
145
+ e.stopPropagation();
146
+ dragCounterRef.current -= 1;
147
+ if (dragCounterRef.current <= 0) {
148
+ dragCounterRef.current = 0;
149
+ setIsDragging(false);
150
+ }
151
+ };
152
+ const handleDrop = (e) => {
153
+ e.preventDefault();
154
+ e.stopPropagation();
155
+ dragCounterRef.current = 0;
156
+ setIsDragging(false);
157
+ if (disabled || isGlobalUploading || isMaxReached) return;
158
+ const droppedFiles = Array.from(e.dataTransfer.files || []);
159
+ if (droppedFiles.length > 0) {
160
+ handleFiles(droppedFiles);
161
+ }
162
+ };
163
+ const handleInputChange = (e) => {
164
+ const selectedFiles = Array.from(e.target.files || []);
165
+ if (selectedFiles.length > 0) {
166
+ handleFiles(selectedFiles);
167
+ }
168
+ if (fileInputRef.current) {
169
+ fileInputRef.current.value = "";
170
+ }
171
+ };
172
+ const handleSingleRemove = (e) => {
173
+ e.stopPropagation();
174
+ setLocalError(null);
175
+ onRemove?.(0, items[0]);
176
+ };
177
+ const handleItemRemove = (e, index, item) => {
178
+ e.stopPropagation();
179
+ setLocalError(null);
180
+ if (previewModalIndex === index) {
181
+ setPreviewModalIndex(null);
182
+ }
183
+ onRemove?.(index, item);
184
+ };
185
+ const openPreviewModal = (e, index) => {
186
+ e.stopPropagation();
187
+ if (enablePreviewModal) {
188
+ setPreviewModalIndex(index);
189
+ }
190
+ };
191
+ const gridClass = GRID_COLS_MAP[gridCols] || GRID_COLS_MAP[4];
192
+ return /* @__PURE__ */ jsxs(LayoutGroup, { id: "image-upload-group", children: [
193
+ /* @__PURE__ */ jsxs(
194
+ "div",
195
+ {
196
+ ref,
197
+ "data-slot": "image-upload",
198
+ className: cn("flex flex-col gap-2 w-full font-sans", className),
199
+ style,
200
+ ...dataAttributes,
201
+ ...props,
202
+ children: [
203
+ (label || maxFiles || minFiles) && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 select-none", children: [
204
+ label && /* @__PURE__ */ jsxs("label", { className: "text-xs font-semibold text-foreground tracking-wide flex items-center gap-1.5", children: [
205
+ /* @__PURE__ */ jsx("span", { children: label }),
206
+ minFiles && minFiles > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-muted font-normal", children: [
207
+ "(Min ",
208
+ minFiles,
209
+ ")"
210
+ ] })
211
+ ] }),
212
+ multiple && /* @__PURE__ */ jsxs("span", { className: "text-[11px] font-medium text-muted", children: [
213
+ items.length,
214
+ maxFiles ? ` / ${maxFiles}` : "",
215
+ " uploaded"
216
+ ] })
217
+ ] }),
218
+ /* @__PURE__ */ jsx(
219
+ "input",
220
+ {
221
+ ref: fileInputRef,
222
+ type: "file",
223
+ accept,
224
+ multiple,
225
+ className: "hidden",
226
+ disabled: disabled || isGlobalUploading || isMaxReached,
227
+ onChange: handleInputChange
228
+ }
229
+ ),
230
+ !multiple ? items.length > 0 ? /* @__PURE__ */ jsx(
231
+ ImageUploadSinglePreview,
232
+ {
233
+ item: items[0],
234
+ aspectRatio,
235
+ disabled,
236
+ isUploading: isGlobalUploading,
237
+ enablePreviewModal,
238
+ onReplace: () => fileInputRef.current?.click(),
239
+ onRemove: handleSingleRemove,
240
+ onPreview: (e) => openPreviewModal(e, 0)
241
+ }
242
+ ) : /* @__PURE__ */ jsx(
243
+ ImageUploadDropzone,
244
+ {
245
+ disabled,
246
+ isUploading: isGlobalUploading,
247
+ loadingText: defaultLoadingText,
248
+ isDragging,
249
+ isError,
250
+ aspectRatio,
251
+ mediaType,
252
+ maxSizeBytes,
253
+ shouldAnimate,
254
+ motionClass,
255
+ onClick: () => !disabled && !isGlobalUploading && fileInputRef.current?.click(),
256
+ onDragEnter: handleDragEnter,
257
+ onDragOver: handleDragOver,
258
+ onDragLeave: handleDragLeave,
259
+ onDrop: handleDrop
260
+ }
261
+ ) : (
262
+ /* MULTIPLE MODE */
263
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 w-full", children: [
264
+ !isMaxReached ? /* @__PURE__ */ jsx(
265
+ ImageUploadDropzone,
266
+ {
267
+ disabled,
268
+ isUploading: isGlobalUploading,
269
+ loadingText: defaultLoadingText,
270
+ isDragging,
271
+ isError,
272
+ aspectRatio: items.length > 0 ? "auto" : aspectRatio,
273
+ size: items.length > 0 ? "compact" : "default",
274
+ mediaType,
275
+ maxSizeBytes,
276
+ maxFiles,
277
+ multiple: true,
278
+ hasItems: items.length > 0,
279
+ shouldAnimate,
280
+ motionClass,
281
+ onClick: () => !disabled && !isGlobalUploading && fileInputRef.current?.click(),
282
+ onDragEnter: handleDragEnter,
283
+ onDragOver: handleDragOver,
284
+ onDragLeave: handleDragLeave,
285
+ onDrop: handleDrop
286
+ }
287
+ ) : /* @__PURE__ */ jsxs("div", { className: "w-full rounded-xl border border-dashed border-grey-300 dark:border-grey-700 bg-grey-50/50 dark:bg-grey-900/30 p-3 text-center text-xs text-muted", children: [
288
+ "Maximum upload limit reached (",
289
+ maxFiles,
290
+ "/",
291
+ maxFiles,
292
+ " items). Remove an item to add another."
293
+ ] }),
294
+ (items.length > 0 || uploadingTasks.length > 0) && /* @__PURE__ */ jsx(
295
+ "div",
296
+ {
297
+ className: cn(
298
+ layout === "scroll" ? "flex gap-3 overflow-x-auto pb-2.5 pt-0.5 scrollbar-thin snap-x snap-mandatory scroll-smooth w-full" : "grid gap-3 w-full",
299
+ layout === "grid" && gridClass
300
+ ),
301
+ children: /* @__PURE__ */ jsxs(AnimatePresence, { initial: false, children: [
302
+ items.map((item, index) => /* @__PURE__ */ jsx(
303
+ ImageUploadThumbnailCard,
304
+ {
305
+ item,
306
+ index,
307
+ layout,
308
+ disabled,
309
+ enablePreviewModal,
310
+ shouldAnimate,
311
+ onPreview: (e) => openPreviewModal(e, index),
312
+ onRemove: (e) => handleItemRemove(e, index, item)
313
+ },
314
+ item.key || item.url || index
315
+ )),
316
+ uploadingTasks.map((task) => /* @__PURE__ */ jsx(
317
+ ImageUploadTaskCard,
318
+ {
319
+ id: task.id,
320
+ name: task.name,
321
+ previewUrl: task.previewUrl,
322
+ isVideo: task.isVideo,
323
+ layout,
324
+ shouldAnimate
325
+ },
326
+ task.id
327
+ ))
328
+ ] })
329
+ }
330
+ )
331
+ ] })
332
+ ),
333
+ (error || minFilesWarning || helperText) && /* @__PURE__ */ jsxs(
334
+ "p",
335
+ {
336
+ className: cn(
337
+ "text-xs mt-1 flex items-center gap-1.5 leading-normal",
338
+ isError ? "text-destructive font-medium" : minFilesWarning ? "text-amber-600 dark:text-amber-400 font-medium" : "text-muted"
339
+ ),
340
+ children: [
341
+ (isError || minFilesWarning) && /* @__PURE__ */ jsx(AlertCircle, { className: "size-3.5 shrink-0", "aria-hidden": "true" }),
342
+ /* @__PURE__ */ jsx("span", { children: error || minFilesWarning || helperText })
343
+ ]
344
+ }
345
+ )
346
+ ]
347
+ }
348
+ ),
349
+ /* @__PURE__ */ jsx(
350
+ ImageUploadModal,
351
+ {
352
+ isOpen: enablePreviewModal && previewModalIndex !== null,
353
+ activeIndex: previewModalIndex,
354
+ items,
355
+ shouldAnimate,
356
+ onClose: () => setPreviewModalIndex(null),
357
+ onNavigate: (newIndex) => setPreviewModalIndex(newIndex)
358
+ }
359
+ )
360
+ ] });
361
+ }
362
+ );
363
+ ImageUpload.displayName = "ImageUpload";
364
+
365
+ export { ImageUpload };
@@ -0,0 +1,17 @@
1
+ import { type VariantProps } from 'class-variance-authority';
2
+ export declare const imageUploadZoneVariants: (props?: ({
3
+ aspectRatio?: "video" | "auto" | "square" | "wide" | null | undefined;
4
+ size?: "default" | "compact" | null | undefined;
5
+ isDragging?: boolean | null | undefined;
6
+ isError?: boolean | null | undefined;
7
+ isDisabled?: boolean | null | undefined;
8
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
9
+ export declare const imagePreviewContainerVariants: (props?: ({
10
+ aspectRatio?: "video" | "auto" | "square" | "wide" | null | undefined;
11
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
12
+ export declare const thumbnailCardVariants: (props?: ({
13
+ aspectRatio?: "video" | "auto" | "square" | "wide" | null | undefined;
14
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
15
+ export type ImageUploadZoneVariants = VariantProps<typeof imageUploadZoneVariants>;
16
+ export type ImagePreviewContainerVariants = VariantProps<typeof imagePreviewContainerVariants>;
17
+ //# sourceMappingURL=ImageUpload.styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageUpload.styles.d.ts","sourceRoot":"","sources":["../../../src/components/ImageUpload/ImageUpload.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAElE,eAAO,MAAM,uBAAuB;;;;;;8EAmCnC,CAAC;AAEF,eAAO,MAAM,6BAA6B;;8EAezC,CAAC;AAEF,eAAO,MAAM,qBAAqB;;8EAejC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,YAAY,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACnF,MAAM,MAAM,6BAA6B,GAAG,YAAY,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ const imageUploadZoneVariants = cva(
4
+ "relative rounded-xl border-2 border-dashed transition-all duration-200 flex flex-col items-center justify-center text-center select-none font-sans outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:border-primary",
5
+ {
6
+ variants: {
7
+ aspectRatio: {
8
+ square: "size-32 p-2 aspect-square shrink-0",
9
+ video: "w-full aspect-[16/10] min-h-[160px] p-5",
10
+ wide: "w-full aspect-[21/9] min-h-[130px] p-5",
11
+ auto: "w-full min-h-[130px] p-5"
12
+ },
13
+ size: {
14
+ default: "",
15
+ compact: "py-3 px-2 min-h-[80px]"
16
+ },
17
+ isDragging: {
18
+ true: "border-primary bg-primary-50/40 dark:bg-primary-950/20 scale-[0.99] shadow-sm",
19
+ false: ""
20
+ },
21
+ isError: {
22
+ true: "border-destructive/80 bg-destructive/5 hover:border-destructive",
23
+ false: "border-grey-300 dark:border-grey-700 bg-grey-50/60 dark:bg-grey-900/40 hover:border-grey-400 hover:bg-grey-50 dark:hover:bg-grey-900/60"
24
+ },
25
+ isDisabled: {
26
+ true: "opacity-50 cursor-not-allowed pointer-events-none",
27
+ false: "cursor-pointer"
28
+ }
29
+ },
30
+ defaultVariants: {
31
+ aspectRatio: "auto",
32
+ size: "default",
33
+ isDragging: false,
34
+ isError: false,
35
+ isDisabled: false
36
+ }
37
+ }
38
+ );
39
+ const imagePreviewContainerVariants = cva(
40
+ "relative group rounded-xl border border-grey-200 dark:border-grey-800 bg-grey-50 dark:bg-grey-900 overflow-hidden flex items-center justify-center transition-all shadow-xs",
41
+ {
42
+ variants: {
43
+ aspectRatio: {
44
+ square: "size-32 aspect-square shrink-0",
45
+ video: "w-full aspect-[16/10]",
46
+ wide: "w-full aspect-[21/9]",
47
+ auto: "w-full h-44"
48
+ }
49
+ },
50
+ defaultVariants: {
51
+ aspectRatio: "auto"
52
+ }
53
+ }
54
+ );
55
+ const thumbnailCardVariants = cva(
56
+ "relative group rounded-xl border border-grey-200 dark:border-grey-800 bg-grey-100 dark:bg-grey-900 overflow-hidden transition-all shadow-xs hover:shadow-md hover:border-grey-300 dark:hover:border-grey-700",
57
+ {
58
+ variants: {
59
+ aspectRatio: {
60
+ square: "aspect-square",
61
+ video: "aspect-[16/10]",
62
+ wide: "aspect-[21/9]",
63
+ auto: "aspect-square min-h-[110px]"
64
+ }
65
+ },
66
+ defaultVariants: {
67
+ aspectRatio: "square"
68
+ }
69
+ }
70
+ );
71
+
72
+ export { imagePreviewContainerVariants, imageUploadZoneVariants, thumbnailCardVariants };
@@ -0,0 +1,112 @@
1
+ import type * as React from 'react';
2
+ import type { MotionProps } from '../../types/motion';
3
+ export type ImageUploadAspectRatio = 'square' | 'video' | 'wide' | 'auto';
4
+ export type ImageUploadMediaType = 'image' | 'video' | 'all';
5
+ export interface ImageUploadItem {
6
+ url: string;
7
+ key?: string;
8
+ id?: string;
9
+ name?: string;
10
+ size?: number;
11
+ type?: 'image' | 'video' | string;
12
+ }
13
+ export type ImageUploadValue = string | ImageUploadItem;
14
+ export interface ImageUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'value'>, MotionProps {
15
+ /**
16
+ * Current media URL(s) to display as preview.
17
+ * Single string/object for single mode, array for multiple mode.
18
+ */
19
+ value?: ImageUploadValue | ImageUploadValue[] | null;
20
+ /**
21
+ * Whether multiple media uploads are supported.
22
+ * @default false
23
+ */
24
+ multiple?: boolean;
25
+ /**
26
+ * Media types accepted by the component.
27
+ * @default "image"
28
+ */
29
+ mediaType?: ImageUploadMediaType;
30
+ /**
31
+ * Minimum number of files required (for multiple uploads validation).
32
+ */
33
+ minFiles?: number;
34
+ /**
35
+ * Maximum number of files allowed (for multiple uploads validation).
36
+ */
37
+ maxFiles?: number;
38
+ /**
39
+ * Callback fired when files are selected by the user.
40
+ */
41
+ onChange?: (files: File[] | File | null) => void;
42
+ /**
43
+ * Optional async upload handler.
44
+ */
45
+ onUpload?: (file: File) => Promise<{
46
+ url: string;
47
+ key?: string;
48
+ type?: string;
49
+ } | string>;
50
+ /**
51
+ * Callback fired when an item is removed by the user.
52
+ * In multi mode, receives the index and item being removed.
53
+ */
54
+ onRemove?: (index?: number, item?: ImageUploadItem | string) => void;
55
+ /**
56
+ * Disable the upload zone and actions.
57
+ */
58
+ disabled?: boolean;
59
+ /**
60
+ * Manually control the global loading state.
61
+ */
62
+ loading?: boolean;
63
+ /**
64
+ * Custom loading text displayed during upload.
65
+ */
66
+ loadingText?: string;
67
+ /**
68
+ * Accepted MIME types or extensions.
69
+ * Defaults automatically based on `mediaType`.
70
+ */
71
+ accept?: string;
72
+ /**
73
+ * Maximum allowed file size in bytes per file.
74
+ * @default 5242880 (5MB) for images, 52428800 (50MB) for videos
75
+ */
76
+ maxSizeBytes?: number;
77
+ /**
78
+ * Label rendered above the upload dropzone.
79
+ */
80
+ label?: string;
81
+ /**
82
+ * Subtitle / helper instructions rendered below the dropzone.
83
+ */
84
+ helperText?: string;
85
+ /**
86
+ * Error message to display.
87
+ */
88
+ error?: string;
89
+ /**
90
+ * Aspect ratio of the dropzone / preview cards.
91
+ * @default "auto"
92
+ */
93
+ aspectRatio?: ImageUploadAspectRatio;
94
+ /**
95
+ * Grid columns for multiple image previews.
96
+ * @default 4 (responsive 2-4 columns)
97
+ */
98
+ gridCols?: 2 | 3 | 4 | 5;
99
+ /**
100
+ * Layout mode for multiple image previews.
101
+ * - 'grid': Responsive multi-column wrap grid (default)
102
+ * - 'scroll': Sleek horizontal scroll strip with snap points
103
+ * @default "grid"
104
+ */
105
+ layout?: 'grid' | 'scroll';
106
+ /**
107
+ * Whether to enable the inline full-screen media lightbox popup.
108
+ * @default true
109
+ */
110
+ enablePreviewModal?: boolean;
111
+ }
112
+ //# sourceMappingURL=ImageUpload.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageUpload.types.d.ts","sourceRoot":"","sources":["../../../src/components/ImageUpload/ImageUpload.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,MAAM,MAAM,sBAAsB,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;CACnC;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,eAAe,CAAC;AAExD,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,EACtE,WAAW;IACb;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,GAAG,IAAI,CAAC;IAErD;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,oBAAoB,CAAC;IAEjC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IAEjD;;OAEG;IACH,QAAQ,CAAC,EAAE,CACT,IAAI,EAAE,IAAI,KACP,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,CAAC;IAEpE;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,KAAK,IAAI,CAAC;IAErE;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,WAAW,CAAC,EAAE,sBAAsB,CAAC;IAErC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAEzB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAE3B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B"}
@@ -0,0 +1,6 @@
1
+ export * from './ImageUpload';
2
+ export * from './ImageUpload.types';
3
+ export * from './ImageUpload.styles';
4
+ export * from './ImageUpload.helpers';
5
+ export * from './subcomponents';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ImageUpload/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,25 @@
1
+ import * as React from 'react';
2
+ import type { ImageUploadAspectRatio, ImageUploadMediaType } from '../ImageUpload.types';
3
+ export interface ImageUploadDropzoneProps {
4
+ disabled?: boolean;
5
+ isUploading?: boolean;
6
+ loadingText?: string;
7
+ isDragging: boolean;
8
+ isError: boolean;
9
+ aspectRatio: ImageUploadAspectRatio;
10
+ size?: 'default' | 'compact';
11
+ mediaType: ImageUploadMediaType;
12
+ maxSizeBytes: number;
13
+ maxFiles?: number;
14
+ multiple?: boolean;
15
+ hasItems?: boolean;
16
+ shouldAnimate?: boolean;
17
+ motionClass?: string;
18
+ onClick: () => void;
19
+ onDragEnter: (e: React.DragEvent) => void;
20
+ onDragOver: (e: React.DragEvent) => void;
21
+ onDragLeave: (e: React.DragEvent) => void;
22
+ onDrop: (e: React.DragEvent) => void;
23
+ }
24
+ export declare const ImageUploadDropzone: React.FC<ImageUploadDropzoneProps>;
25
+ //# sourceMappingURL=ImageUploadDropzone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageUploadDropzone.d.ts","sourceRoot":"","sources":["../../../../src/components/ImageUpload/subcomponents/ImageUploadDropzone.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAU/B,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACrB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,sBAAsB,CAAC;IACpC,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,SAAS,EAAE,oBAAoB,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;IAC1C,UAAU,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;IACzC,WAAW,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;IAC1C,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;CACtC;AAED,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,wBAAwB,CAqIlE,CAAC"}