@getmicdrop/svelte-components 5.3.3 → 5.3.10

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 (194) hide show
  1. package/dist/calendar/AboutShow/AboutShow.svelte +9 -8
  2. package/dist/calendar/AboutShow/AboutShow.svelte.d.ts.map +1 -1
  3. package/dist/calendar/Calendar/MiniMonthCalendar.svelte +19 -18
  4. package/dist/calendar/Calendar/MiniMonthCalendar.svelte.d.ts +6 -6
  5. package/dist/calendar/Calendar/MiniMonthCalendar.svelte.d.ts.map +1 -1
  6. package/dist/calendar/FAQs/FAQs.svelte +6 -5
  7. package/dist/calendar/FAQs/FAQs.svelte.d.ts.map +1 -1
  8. package/dist/calendar/MonthSwitcher/MonthSwitcher.svelte +3 -2
  9. package/dist/calendar/MonthSwitcher/MonthSwitcher.svelte.d.ts.map +1 -1
  10. package/dist/calendar/OrderSummary/OrderSummary.svelte +21 -20
  11. package/dist/calendar/OrderSummary/OrderSummary.svelte.d.ts +2 -2
  12. package/dist/calendar/OrderSummary/OrderSummary.svelte.d.ts.map +1 -1
  13. package/dist/calendar/PublicCard/PublicCard.svelte +9 -11
  14. package/dist/calendar/PublicCard/PublicCard.svelte.d.ts +2 -2
  15. package/dist/calendar/PublicCard/PublicCard.svelte.d.ts.map +1 -1
  16. package/dist/calendar/ShowCard/ShowCard.svelte +13 -12
  17. package/dist/calendar/ShowCard/ShowCard.svelte.d.ts +2 -2
  18. package/dist/calendar/ShowCard/ShowCard.svelte.d.ts.map +1 -1
  19. package/dist/calendar/ShowTimeCard/ShowTimeCard.svelte +5 -3
  20. package/dist/calendar/ShowTimeCard/ShowTimeCard.svelte.d.ts.map +1 -1
  21. package/dist/components/Layout/Section.svelte +4 -4
  22. package/dist/components/Layout/Section.svelte.d.ts.map +1 -1
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +9 -0
  25. package/dist/patterns/data/DataTable.svelte +4 -2
  26. package/dist/patterns/data/DataTable.svelte.d.ts.map +1 -1
  27. package/dist/patterns/forms/FormSection.svelte +4 -2
  28. package/dist/patterns/forms/FormSection.svelte.d.ts.map +1 -1
  29. package/dist/patterns/navigation/BottomNav.svelte +4 -3
  30. package/dist/patterns/navigation/BottomNav.svelte.d.ts.map +1 -1
  31. package/dist/patterns/navigation/Header.svelte +49 -37
  32. package/dist/patterns/navigation/Header.svelte.d.ts.map +1 -1
  33. package/dist/patterns/page/PageHeader.svelte +3 -2
  34. package/dist/patterns/page/PageHeader.svelte.d.ts.map +1 -1
  35. package/dist/patterns/page/PageLayout.svelte +3 -2
  36. package/dist/patterns/page/PageLayout.svelte.d.ts.map +1 -1
  37. package/dist/patterns/page/PageLoader.svelte +2 -1
  38. package/dist/patterns/page/PageLoader.svelte.d.ts.map +1 -1
  39. package/dist/patterns/page/SectionHeader.svelte +5 -3
  40. package/dist/patterns/page/SectionHeader.svelte.d.ts.map +1 -1
  41. package/dist/primitives/Accordion/Accordion.stories.svelte +75 -0
  42. package/dist/primitives/Accordion/Accordion.stories.svelte.d.ts +28 -0
  43. package/dist/primitives/Accordion/Accordion.stories.svelte.d.ts.map +1 -0
  44. package/dist/primitives/Accordion/Accordion.svelte +2 -1
  45. package/dist/primitives/Accordion/Accordion.svelte.d.ts.map +1 -1
  46. package/dist/primitives/Accordion/AccordionItem.svelte +5 -4
  47. package/dist/primitives/Accordion/AccordionItem.svelte.d.ts.map +1 -1
  48. package/dist/primitives/Alert/Alert.stories.svelte +88 -0
  49. package/dist/primitives/Alert/Alert.stories.svelte.d.ts +28 -0
  50. package/dist/primitives/Alert/Alert.stories.svelte.d.ts.map +1 -0
  51. package/dist/primitives/Avatar/Avatar.stories.svelte +94 -0
  52. package/dist/primitives/Avatar/Avatar.stories.svelte.d.ts +28 -0
  53. package/dist/primitives/Avatar/Avatar.stories.svelte.d.ts.map +1 -0
  54. package/dist/primitives/BottomSheet/BottomSheet.svelte +2 -1
  55. package/dist/primitives/BottomSheet/BottomSheet.svelte.d.ts.map +1 -1
  56. package/dist/primitives/Breadcrumb/Breadcrumb.svelte +7 -6
  57. package/dist/primitives/Breadcrumb/Breadcrumb.svelte.d.ts.map +1 -1
  58. package/dist/primitives/Button/Button.svelte +94 -43
  59. package/dist/primitives/Button/Button.svelte.d.ts +5 -3
  60. package/dist/primitives/Button/Button.svelte.d.ts.map +1 -1
  61. package/dist/primitives/Button/ButtonSaveDemo.svelte +2 -1
  62. package/dist/primitives/Button/ButtonSaveDemo.svelte.d.ts.map +1 -1
  63. package/dist/primitives/Card.svelte +1 -1
  64. package/dist/primitives/Checkbox/Checkbox.stories.svelte +84 -0
  65. package/dist/primitives/Checkbox/Checkbox.stories.svelte.d.ts +28 -0
  66. package/dist/primitives/Checkbox/Checkbox.stories.svelte.d.ts.map +1 -0
  67. package/dist/primitives/DarkModeToggle.svelte +43 -44
  68. package/dist/primitives/DarkModeToggle.svelte.d.ts.map +1 -1
  69. package/dist/primitives/Drawer/Drawer.stories.svelte +100 -0
  70. package/dist/primitives/Drawer/Drawer.stories.svelte.d.ts +28 -0
  71. package/dist/primitives/Drawer/Drawer.stories.svelte.d.ts.map +1 -0
  72. package/dist/primitives/Drawer/Drawer.svelte +121 -47
  73. package/dist/primitives/Drawer/Drawer.svelte.d.ts +4 -0
  74. package/dist/primitives/Drawer/Drawer.svelte.d.ts.map +1 -1
  75. package/dist/primitives/Dropdown/Dropdown.stories.svelte +137 -0
  76. package/dist/primitives/Dropdown/Dropdown.stories.svelte.d.ts +28 -0
  77. package/dist/primitives/Dropdown/Dropdown.stories.svelte.d.ts.map +1 -0
  78. package/dist/primitives/Dropdown/Dropdown.svelte +13 -15
  79. package/dist/primitives/Dropdown/Dropdown.svelte.d.ts.map +1 -1
  80. package/dist/primitives/Icons/Icon.svelte +2 -1
  81. package/dist/primitives/Icons/Icon.svelte.d.ts.map +1 -1
  82. package/dist/primitives/Input/Input.svelte +41 -80
  83. package/dist/primitives/Input/Input.svelte.d.ts +4 -6
  84. package/dist/primitives/Input/Input.svelte.d.ts.map +1 -1
  85. package/dist/primitives/Input/Select.stories.svelte +112 -0
  86. package/dist/primitives/Input/Select.stories.svelte.d.ts +28 -0
  87. package/dist/primitives/Input/Select.stories.svelte.d.ts.map +1 -0
  88. package/dist/primitives/Input/Select.svelte +66 -13
  89. package/dist/primitives/Input/Select.svelte.d.ts +2 -0
  90. package/dist/primitives/Input/Select.svelte.d.ts.map +1 -1
  91. package/dist/primitives/Input/Textarea.stories.svelte +137 -0
  92. package/dist/primitives/Input/Textarea.stories.svelte.d.ts +28 -0
  93. package/dist/primitives/Input/Textarea.stories.svelte.d.ts.map +1 -0
  94. package/dist/primitives/Input/Textarea.svelte +5 -3
  95. package/dist/primitives/Input/Textarea.svelte.d.ts.map +1 -1
  96. package/dist/primitives/Modal/Modal.svelte +13 -1
  97. package/dist/primitives/Modal/Modal.svelte.d.ts.map +1 -1
  98. package/dist/primitives/Pagination/Pagination.stories.svelte +76 -0
  99. package/dist/primitives/Pagination/Pagination.stories.svelte.d.ts +28 -0
  100. package/dist/primitives/Pagination/Pagination.stories.svelte.d.ts.map +1 -0
  101. package/dist/primitives/Pagination/Pagination.svelte +6 -5
  102. package/dist/primitives/Pagination/Pagination.svelte.d.ts +1 -1
  103. package/dist/primitives/Pagination/Pagination.svelte.d.ts.map +1 -1
  104. package/dist/primitives/Radio/Radio.stories.svelte +80 -0
  105. package/dist/primitives/Radio/Radio.stories.svelte.d.ts +28 -0
  106. package/dist/primitives/Radio/Radio.stories.svelte.d.ts.map +1 -0
  107. package/dist/primitives/Skeleton/Skeleton.stories.svelte +151 -0
  108. package/dist/primitives/Skeleton/Skeleton.stories.svelte.d.ts +28 -0
  109. package/dist/primitives/Skeleton/Skeleton.stories.svelte.d.ts.map +1 -0
  110. package/dist/primitives/Tabs/Tabs.stories.svelte +112 -0
  111. package/dist/primitives/Tabs/Tabs.stories.svelte.d.ts +28 -0
  112. package/dist/primitives/Tabs/Tabs.stories.svelte.d.ts.map +1 -0
  113. package/dist/primitives/Tabs/Tabs.svelte +5 -4
  114. package/dist/primitives/Tabs/Tabs.svelte.d.ts.map +1 -1
  115. package/dist/primitives/Toggle.svelte +4 -4
  116. package/dist/primitives/ValidationError.spec.js +25 -1
  117. package/dist/primitives/ValidationError.stories.svelte +24 -0
  118. package/dist/primitives/ValidationError.stories.svelte.d.ts.map +1 -1
  119. package/dist/primitives/ValidationError.svelte +8 -4
  120. package/dist/primitives/ValidationError.svelte.d.ts +2 -0
  121. package/dist/primitives/ValidationError.svelte.d.ts.map +1 -1
  122. package/dist/recipes/CropImage/CropImage.svelte +12 -7
  123. package/dist/recipes/CropImage/CropImage.svelte.d.ts.map +1 -1
  124. package/dist/recipes/ImageUploader/ImageUploader.stories.svelte +125 -0
  125. package/dist/recipes/ImageUploader/ImageUploader.stories.svelte.d.ts +28 -0
  126. package/dist/recipes/ImageUploader/ImageUploader.stories.svelte.d.ts.map +1 -0
  127. package/dist/recipes/ImageUploader/ImageUploader.svelte +939 -0
  128. package/dist/recipes/ImageUploader/ImageUploader.svelte.d.ts +60 -0
  129. package/dist/recipes/ImageUploader/ImageUploader.svelte.d.ts.map +1 -0
  130. package/dist/recipes/SuperLogin/SuperLogin.svelte +71 -63
  131. package/dist/recipes/SuperLogin/SuperLogin.svelte.d.ts.map +1 -1
  132. package/dist/recipes/feedback/EmptyState/EmptyState.svelte +5 -3
  133. package/dist/recipes/feedback/EmptyState/EmptyState.svelte.d.ts.map +1 -1
  134. package/dist/recipes/fields/CheckboxField.svelte +3 -2
  135. package/dist/recipes/fields/CheckboxField.svelte.d.ts.map +1 -1
  136. package/dist/recipes/fields/FormField.svelte +12 -4
  137. package/dist/recipes/fields/FormField.svelte.d.ts +3 -1
  138. package/dist/recipes/fields/FormField.svelte.d.ts.map +1 -1
  139. package/dist/recipes/fields/RadioGroup.svelte +13 -4
  140. package/dist/recipes/fields/RadioGroup.svelte.d.ts +4 -1
  141. package/dist/recipes/fields/RadioGroup.svelte.d.ts.map +1 -1
  142. package/dist/recipes/fields/SelectField.svelte +12 -3
  143. package/dist/recipes/fields/SelectField.svelte.d.ts +4 -1
  144. package/dist/recipes/fields/SelectField.svelte.d.ts.map +1 -1
  145. package/dist/recipes/fields/TextareaField.svelte +2 -1
  146. package/dist/recipes/fields/TextareaField.svelte.d.ts.map +1 -1
  147. package/dist/recipes/fields/ToggleField.svelte +3 -2
  148. package/dist/recipes/fields/ToggleField.svelte.d.ts.map +1 -1
  149. package/dist/recipes/inputs/MultiSelect.svelte +8 -7
  150. package/dist/recipes/inputs/MultiSelect.svelte.d.ts.map +1 -1
  151. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.svelte +9 -9
  152. package/dist/recipes/inputs/PlaceAutocomplete/PlaceAutocomplete.svelte.d.ts.map +1 -1
  153. package/dist/recipes/inputs/Search.svelte +7 -6
  154. package/dist/recipes/inputs/Search.svelte.d.ts.map +1 -1
  155. package/dist/recipes/modals/AlertModal.svelte +3 -2
  156. package/dist/recipes/modals/AlertModal.svelte.d.ts.map +1 -1
  157. package/dist/recipes/modals/ConfirmationModal.svelte +4 -3
  158. package/dist/recipes/modals/ConfirmationModal.svelte.d.ts.map +1 -1
  159. package/dist/recipes/modals/InputModal.svelte +10 -8
  160. package/dist/recipes/modals/InputModal.svelte.d.ts.map +1 -1
  161. package/dist/recipes/modals/ModalStateManager.svelte +4 -3
  162. package/dist/recipes/modals/ModalStateManager.svelte.d.ts.map +1 -1
  163. package/dist/recipes/modals/StatusModal.svelte +5 -4
  164. package/dist/recipes/modals/StatusModal.svelte.d.ts.map +1 -1
  165. package/dist/stories/ButtonAuditReview.svelte +361 -397
  166. package/dist/stories/ButtonAuditReview.svelte.d.ts +24 -4
  167. package/dist/stories/ButtonAuditReview.svelte.d.ts.map +1 -1
  168. package/dist/stories/ComponentConsolidation.stories.svelte +276 -188
  169. package/dist/stories/ComponentConsolidation.stories.svelte.d.ts.map +1 -1
  170. package/dist/stories/PatternsGallery.stories.svelte +19 -0
  171. package/dist/stories/PatternsGallery.stories.svelte.d.ts +28 -0
  172. package/dist/stories/PatternsGallery.stories.svelte.d.ts.map +1 -0
  173. package/dist/stories/PatternsGallery.svelte +388 -0
  174. package/dist/stories/PatternsGallery.svelte.d.ts +4 -0
  175. package/dist/stories/PatternsGallery.svelte.d.ts.map +1 -0
  176. package/dist/stories/PrimitivesGallery.stories.svelte +19 -0
  177. package/dist/stories/PrimitivesGallery.stories.svelte.d.ts +28 -0
  178. package/dist/stories/PrimitivesGallery.stories.svelte.d.ts.map +1 -0
  179. package/dist/stories/PrimitivesGallery.svelte +752 -0
  180. package/dist/stories/PrimitivesGallery.svelte.d.ts +4 -0
  181. package/dist/stories/PrimitivesGallery.svelte.d.ts.map +1 -0
  182. package/dist/stories/RecipesGallery.stories.svelte +19 -0
  183. package/dist/stories/RecipesGallery.stories.svelte.d.ts +28 -0
  184. package/dist/stories/RecipesGallery.stories.svelte.d.ts.map +1 -0
  185. package/dist/stories/RecipesGallery.svelte +441 -0
  186. package/dist/stories/RecipesGallery.svelte.d.ts +4 -0
  187. package/dist/stories/RecipesGallery.svelte.d.ts.map +1 -0
  188. package/dist/tokens/index.d.ts +4 -8
  189. package/dist/tokens/index.d.ts.map +1 -1
  190. package/dist/tokens/index.js +4 -8
  191. package/dist/tokens/typography.d.ts +76 -169
  192. package/dist/tokens/typography.d.ts.map +1 -1
  193. package/dist/tokens/typography.js +93 -62
  194. package/package.json +6 -3
@@ -0,0 +1,939 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ImageUploader - Unified image upload component
4
+ *
5
+ * Supports:
6
+ * - Single image mode (event posters, venue images)
7
+ * - Multi-image mode with grid layout (performer photos)
8
+ * - Optional cropping with aspect ratio selection
9
+ * - Optional drag-to-reorder for multi-image
10
+ * - Different shapes: square, wide
11
+ * - Image compression before upload callback
12
+ */
13
+
14
+ import { browser } from '../../__LIB_ENVIRONMENT__.js';
15
+ import FilePond, { registerPlugin } from 'svelte-filepond';
16
+ import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
17
+ import FilePondPluginImageExifOrientation from 'filepond-plugin-image-exif-orientation';
18
+ import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
19
+ import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';
20
+ import 'filepond/dist/filepond.min.css';
21
+ import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css';
22
+
23
+ import Sortable from 'sortablejs';
24
+ import CropImage from '../CropImage/CropImage.svelte';
25
+ import Badge from '../../primitives/Badges/Badge.svelte';
26
+ import { CloseOutline, PlusOutline } from '../../primitives/Icons';
27
+ import { typography } from '../../tokens/typography';
28
+
29
+ // Register FilePond plugins once
30
+ registerPlugin(
31
+ FilePondPluginImagePreview,
32
+ FilePondPluginImageExifOrientation,
33
+ FilePondPluginFileValidateType,
34
+ FilePondPluginFileValidateSize
35
+ );
36
+
37
+ // Types
38
+ type Shape = 'square' | 'wide';
39
+ type AspectRatio = '1:1' | '2:1' | '16:9' | '4:3' | 'free';
40
+ type Size = 'sm' | 'md' | 'lg';
41
+
42
+ interface Props {
43
+ /** Single image or array of images (URLs) */
44
+ images?: string | string[];
45
+ /** Maximum number of images (only for multi mode) */
46
+ maxImages?: number;
47
+ /** Enable cropping step */
48
+ enableCrop?: boolean;
49
+ /** Crop aspect ratio */
50
+ cropAspectRatio?: AspectRatio;
51
+ /** Enable drag-to-reorder (only for multi mode) */
52
+ enableReorder?: boolean;
53
+ /** Dropzone shape - affects aspect ratio and styling */
54
+ shape?: Shape;
55
+ /** Size of image slots: sm (~64px), md (~96px), lg (~200px+) */
56
+ size?: Size;
57
+ /** Show "Main" badge on first image (multi mode) */
58
+ showMainBadge?: boolean;
59
+ /** Maximum file size (e.g., "10MB") */
60
+ maxFileSize?: string;
61
+ /** Accepted file types */
62
+ acceptedTypes?: string[];
63
+ /** Label shown when empty */
64
+ emptyLabel?: string;
65
+ /** Helper text shown below dropzone */
66
+ helperText?: string;
67
+ /** Callback when image is uploaded - receives File, optionally returns URL
68
+ * If returns URL: component adds to images (self-managed)
69
+ * If returns void/null: parent must manage images (controlled) */
70
+ onUpload?: (file: File) => Promise<string | null | void> | void;
71
+ /** Callback when image is removed */
72
+ onRemove?: (index: number) => void;
73
+ /** Callback when images are reordered - receives new array */
74
+ onReorder?: (images: string[]) => void;
75
+ /** Callback when images are reordered - receives indices (for store integration) */
76
+ onReorderIndices?: (data: { from: number; to: number }) => void;
77
+ /** Callback when user clicks to set main image (moves to first position) */
78
+ onSetMain?: (data: { index: number }) => void;
79
+ /** Callback when images change (single source of truth) */
80
+ onchange?: (images: string[]) => void;
81
+ /** Show upload error */
82
+ error?: string;
83
+ /** Disabled state */
84
+ disabled?: boolean;
85
+ /** Custom class for wrapper */
86
+ class?: string;
87
+ }
88
+
89
+ let {
90
+ images = $bindable([]),
91
+ maxImages = 6,
92
+ enableCrop = false,
93
+ cropAspectRatio = '1:1',
94
+ enableReorder = true,
95
+ shape = 'square',
96
+ size = 'md',
97
+ showMainBadge = true,
98
+ maxFileSize = '20MB',
99
+ acceptedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif'],
100
+ emptyLabel = 'Drag & drop or click to upload',
101
+ helperText = '',
102
+ onUpload,
103
+ onRemove,
104
+ onReorder,
105
+ onReorderIndices,
106
+ onSetMain,
107
+ onchange,
108
+ error = '',
109
+ disabled = false,
110
+ class: className = '',
111
+ }: Props = $props();
112
+
113
+ // Normalize images to array
114
+ let imageArray = $derived(
115
+ Array.isArray(images)
116
+ ? images.filter(Boolean)
117
+ : images
118
+ ? [images]
119
+ : []
120
+ );
121
+
122
+ // Determine mode based on maxImages
123
+ let isMultiMode = $derived(maxImages > 1);
124
+ let canAddMore = $derived(imageArray.length < maxImages);
125
+
126
+ // FilePond state
127
+ let filePondFiles = $state<unknown[]>([]);
128
+ let filePondInstance: unknown;
129
+ let showFilePondModal = $state(false);
130
+ let targetSlotIndex = $state<number | null>(null);
131
+
132
+ // Crop state
133
+ let showCropModal = $state(false);
134
+ let imageForCrop = $state('');
135
+ let isUploading = $state(false);
136
+
137
+ // Reorder state
138
+ let gridContainer: HTMLDivElement;
139
+ let sortableInstance: Sortable | null = null;
140
+ let isDragging = $state(false);
141
+ let gridRenderKey = $state(0);
142
+
143
+ // Direct drag-drop state for dropzone
144
+ let isDropzoneDragOver = $state(false);
145
+
146
+ // Native modal dropzone state
147
+ let isModalDragOver = $state(false);
148
+ let modalFileInput: HTMLInputElement;
149
+
150
+ // Prevent browser from opening dragged files in new tab
151
+ // This MUST be at document level to intercept before browser default behavior
152
+ $effect(() => {
153
+ if (!browser) return;
154
+
155
+ const preventBrowserDefault = (e: DragEvent) => {
156
+ // Only prevent if it's a file drag (not text selection, etc.)
157
+ if (e.dataTransfer?.types?.includes('Files')) {
158
+ e.preventDefault();
159
+ }
160
+ };
161
+
162
+ window.addEventListener('dragover', preventBrowserDefault, true);
163
+ window.addEventListener('drop', preventBrowserDefault, true);
164
+
165
+ return () => {
166
+ window.removeEventListener('dragover', preventBrowserDefault, true);
167
+ window.removeEventListener('drop', preventBrowserDefault, true);
168
+ };
169
+ });
170
+
171
+ // Shape to aspect ratio mapping
172
+ const shapeAspects: Record<Shape, string> = {
173
+ square: 'aspect-square',
174
+ wide: 'aspect-video',
175
+ };
176
+
177
+ // Size configuration - layout classes, slot width, and element sizes
178
+ const sizeConfig: Record<Size, { containerClass: string; slotClass: string; iconClass: string; badgeSize: 'small' | 'medium'; removeSize: string }> = {
179
+ sm: {
180
+ containerClass: 'flex flex-wrap gap-1.5',
181
+ slotClass: 'w-16 h-16', // 64px fixed
182
+ iconClass: 'w-4 h-4',
183
+ badgeSize: 'small',
184
+ removeSize: 'w-4 h-4',
185
+ },
186
+ md: {
187
+ containerClass: 'flex flex-wrap gap-2',
188
+ slotClass: 'w-24 h-24', // 96px fixed
189
+ iconClass: 'w-5 h-5',
190
+ badgeSize: 'small',
191
+ removeSize: 'w-5 h-5',
192
+ },
193
+ lg: {
194
+ containerClass: 'grid grid-cols-3 gap-4',
195
+ slotClass: '', // Let grid control width (uses aspect-square)
196
+ iconClass: 'w-8 h-8',
197
+ badgeSize: 'medium',
198
+ removeSize: 'w-6 h-6',
199
+ },
200
+ };
201
+
202
+ // Get current size config
203
+ let currentSizeConfig = $derived(sizeConfig[size]);
204
+
205
+ // Crop aspect ratio to number
206
+ const aspectRatioValues: Record<AspectRatio, number> = {
207
+ '1:1': 1,
208
+ '2:1': 2,
209
+ '16:9': 16 / 9,
210
+ '4:3': 4 / 3,
211
+ 'free': 0,
212
+ };
213
+
214
+ // FilePond options
215
+ const filePondOptions = $derived({
216
+ allowMultiple: false,
217
+ maxFiles: 1,
218
+ acceptedFileTypes: acceptedTypes,
219
+ maxFileSize: maxFileSize,
220
+ // Drag-drop configuration
221
+ allowDrop: true,
222
+ dropOnPage: false, // Only accept drops on the FilePond element
223
+ dropOnElement: true,
224
+ labelIdle: `
225
+ <div class="filepond-custom-label">
226
+ <svg class="w-8 h-8 mb-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
227
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
228
+ </svg>
229
+ <span class="text-sm text-gray-500 dark:text-gray-400">${emptyLabel}</span>
230
+ <span class="text-xs text-gray-400 dark:text-gray-500 mt-1">JPG, PNG, WebP</span>
231
+ </div>
232
+ `,
233
+ credits: false,
234
+ instantUpload: false,
235
+ allowRevert: true,
236
+ allowProcess: false,
237
+ imagePreviewHeight: shape === 'wide' ? 150 : 200,
238
+ stylePanelLayout: 'compact',
239
+ stylePanelAspectRatio: shape === 'wide' ? '16:9' : '1:1',
240
+ });
241
+
242
+ // Handle FilePond file add
243
+ function handleFilePondAddFile(err: unknown, fileItem: { file: File }) {
244
+ if (err) {
245
+ console.error('FilePond error:', err);
246
+ return;
247
+ }
248
+
249
+ const file = fileItem?.file;
250
+ if (!file) return;
251
+
252
+ if (enableCrop) {
253
+ // Show crop modal
254
+ const objectUrl = URL.createObjectURL(file);
255
+ imageForCrop = objectUrl;
256
+ showCropModal = true;
257
+ } else {
258
+ // Directly upload
259
+ handleUpload(file);
260
+ }
261
+
262
+ // Clear FilePond
263
+ setTimeout(() => {
264
+ if (filePondInstance && typeof (filePondInstance as { removeFiles: () => void }).removeFiles === 'function') {
265
+ (filePondInstance as { removeFiles: () => void }).removeFiles();
266
+ }
267
+ }, 100);
268
+
269
+ showFilePondModal = false;
270
+ }
271
+
272
+ // Handle crop save
273
+ async function handleCropSave(croppedFile: File) {
274
+ showCropModal = false;
275
+ if (imageForCrop.startsWith('blob:')) {
276
+ URL.revokeObjectURL(imageForCrop);
277
+ }
278
+ imageForCrop = '';
279
+ await handleUpload(croppedFile);
280
+ }
281
+
282
+ // Handle crop cancel
283
+ function handleCropCancel() {
284
+ showCropModal = false;
285
+ if (imageForCrop.startsWith('blob:')) {
286
+ URL.revokeObjectURL(imageForCrop);
287
+ }
288
+ imageForCrop = '';
289
+ }
290
+
291
+ // Handle upload
292
+ // Supports two modes:
293
+ // 1. Self-managed: onUpload returns URL, component adds to images
294
+ // 2. Controlled: onUpload returns void, parent manages images array
295
+ async function handleUpload(file: File) {
296
+ if (!onUpload) {
297
+ console.warn('ImageUploader: onUpload callback not provided');
298
+ return;
299
+ }
300
+
301
+ isUploading = true;
302
+
303
+ try {
304
+ const result = await onUpload(file);
305
+
306
+ // If onUpload returns a URL, component manages adding to images (self-managed mode)
307
+ // If onUpload returns void/null, parent is expected to manage images (controlled mode)
308
+ if (typeof result === 'string' && result) {
309
+ const url = result;
310
+ const newImages = [...imageArray];
311
+ if (targetSlotIndex !== null && targetSlotIndex < newImages.length) {
312
+ // Replace existing image
313
+ newImages[targetSlotIndex] = url;
314
+ } else {
315
+ // Add new image
316
+ newImages.push(url);
317
+ }
318
+
319
+ // Update images and notify
320
+ if (Array.isArray(images)) {
321
+ images = newImages;
322
+ } else {
323
+ images = newImages[0] || '';
324
+ }
325
+ onchange?.(newImages);
326
+ }
327
+ // If result is void/null/undefined, parent handles adding to images
328
+ } catch (err) {
329
+ console.error('Upload failed:', err);
330
+ } finally {
331
+ isUploading = false;
332
+ targetSlotIndex = null;
333
+ }
334
+ }
335
+
336
+ // Handle remove
337
+ // In controlled mode (onReorderIndices provided), just fire callback
338
+ // In self-managed mode, update images internally
339
+ function handleRemoveImage(index: number) {
340
+ // Always fire the callback
341
+ onRemove?.(index);
342
+
343
+ // Only manage state internally if not in controlled mode
344
+ if (!onReorderIndices) {
345
+ const newImages = [...imageArray];
346
+ newImages.splice(index, 1);
347
+
348
+ if (Array.isArray(images)) {
349
+ images = newImages;
350
+ } else {
351
+ images = newImages[0] || '';
352
+ }
353
+
354
+ onchange?.(newImages);
355
+ }
356
+
357
+ gridRenderKey++;
358
+ }
359
+
360
+ // Open upload modal/dropzone
361
+ function openUploadModal(slotIndex?: number) {
362
+ if (disabled) return;
363
+ targetSlotIndex = slotIndex ?? null;
364
+ if (isMultiMode) {
365
+ showFilePondModal = true;
366
+ }
367
+ }
368
+
369
+ // Handle direct drag-drop on dropzone (bypasses modal)
370
+ function handleDropzoneDragOver(e: DragEvent) {
371
+ e.preventDefault();
372
+ e.stopPropagation();
373
+ if (disabled || !canAddMore) return;
374
+ isDropzoneDragOver = true;
375
+ }
376
+
377
+ function handleDropzoneDragLeave(e: DragEvent) {
378
+ e.preventDefault();
379
+ e.stopPropagation();
380
+ isDropzoneDragOver = false;
381
+ }
382
+
383
+ function handleDropzoneDrop(e: DragEvent, slotIndex?: number) {
384
+ e.preventDefault();
385
+ e.stopPropagation();
386
+ isDropzoneDragOver = false;
387
+
388
+ if (disabled || !canAddMore) return;
389
+
390
+ const files = e.dataTransfer?.files;
391
+ if (!files || files.length === 0) return;
392
+
393
+ const file = files[0];
394
+
395
+ // Check if it's an accepted image type
396
+ if (!acceptedTypes.some(type => file.type === type || type.includes('*'))) {
397
+ console.warn('File type not accepted:', file.type);
398
+ return;
399
+ }
400
+
401
+ targetSlotIndex = slotIndex ?? null;
402
+
403
+ if (enableCrop) {
404
+ // Show crop modal
405
+ const objectUrl = URL.createObjectURL(file);
406
+ imageForCrop = objectUrl;
407
+ showCropModal = true;
408
+ } else {
409
+ // Directly upload
410
+ handleUpload(file);
411
+ }
412
+ }
413
+
414
+ // Handle modal dropzone drop
415
+ function handleModalDrop(e: DragEvent) {
416
+ e.preventDefault();
417
+ e.stopPropagation();
418
+ isModalDragOver = false;
419
+ showFilePondModal = false;
420
+
421
+ if (disabled || !canAddMore) return;
422
+
423
+ const files = e.dataTransfer?.files;
424
+ if (!files || files.length === 0) return;
425
+
426
+ const file = files[0];
427
+
428
+ // Check if it's an accepted image type
429
+ if (!acceptedTypes.some(type => file.type === type || type.includes('*'))) {
430
+ console.warn('File type not accepted:', file.type);
431
+ return;
432
+ }
433
+
434
+ if (enableCrop) {
435
+ const objectUrl = URL.createObjectURL(file);
436
+ imageForCrop = objectUrl;
437
+ showCropModal = true;
438
+ } else {
439
+ handleUpload(file);
440
+ }
441
+ }
442
+
443
+ // Handle modal file input selection
444
+ function handleModalFileSelect(e: Event) {
445
+ const input = e.target as HTMLInputElement;
446
+ const file = input?.files?.[0];
447
+ if (!file) return;
448
+
449
+ showFilePondModal = false;
450
+
451
+ if (enableCrop) {
452
+ const objectUrl = URL.createObjectURL(file);
453
+ imageForCrop = objectUrl;
454
+ showCropModal = true;
455
+ } else {
456
+ handleUpload(file);
457
+ }
458
+
459
+ // Clear input for next selection
460
+ input.value = '';
461
+ }
462
+
463
+ // Initialize sortable for multi-image grid
464
+ function initSortable() {
465
+ if (!browser || !gridContainer || !enableReorder || !isMultiMode) return;
466
+
467
+ if (sortableInstance) {
468
+ sortableInstance.destroy();
469
+ }
470
+
471
+ sortableInstance = new Sortable(gridContainer, {
472
+ animation: 200,
473
+ ghostClass: 'sortable-ghost',
474
+ chosenClass: 'sortable-chosen',
475
+ dragClass: 'sortable-drag',
476
+ delay: 150,
477
+ delayOnTouchOnly: true,
478
+ touchStartThreshold: 5,
479
+ filter: '.empty-slot, .remove-btn',
480
+ preventOnFilter: false,
481
+ onStart: () => {
482
+ isDragging = true;
483
+ },
484
+ onEnd: (evt) => {
485
+ isDragging = false;
486
+ const { oldIndex, newIndex } = evt;
487
+ if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) return;
488
+
489
+ // Fire index-based callback for store integration (controlled mode)
490
+ onReorderIndices?.({ from: oldIndex, to: newIndex });
491
+
492
+ // For self-managed mode, update images internally
493
+ if (!onReorderIndices) {
494
+ const newImages = [...imageArray];
495
+ const [moved] = newImages.splice(oldIndex, 1);
496
+ newImages.splice(newIndex, 0, moved);
497
+
498
+ if (Array.isArray(images)) {
499
+ images = newImages;
500
+ }
501
+
502
+ onReorder?.(newImages);
503
+ onchange?.(newImages);
504
+ }
505
+
506
+ gridRenderKey++;
507
+
508
+ // Re-init after state update
509
+ if (sortableInstance) {
510
+ sortableInstance.destroy();
511
+ sortableInstance = null;
512
+ }
513
+ },
514
+ });
515
+ }
516
+
517
+ // Setup sortable on mount and when images change
518
+ $effect(() => {
519
+ if (browser && gridContainer && !isDragging && isMultiMode && enableReorder) {
520
+ // Use setTimeout to ensure DOM is ready
521
+ setTimeout(initSortable, 0);
522
+ }
523
+ });
524
+
525
+ // Cleanup
526
+ $effect(() => {
527
+ return () => {
528
+ if (sortableInstance) {
529
+ sortableInstance.destroy();
530
+ }
531
+ };
532
+ });
533
+
534
+ // Generate slots with progressive disclosure
535
+ // Only show filled images + 1 empty slot (up to maxImages)
536
+ let slots = $derived.by(() => {
537
+ const slotCount = Math.min(imageArray.length + 1, maxImages);
538
+ return Array.from({ length: slotCount }, (_, i) => ({
539
+ id: `slot-${i}`,
540
+ image: imageArray[i] || null,
541
+ isNext: i === imageArray.length,
542
+ }));
543
+ });
544
+ </script>
545
+
546
+ <!-- Single Image Mode -->
547
+ {#if !isMultiMode}
548
+ <div class="image-uploader-single {className}">
549
+ {#if imageArray.length > 0}
550
+ <!-- Show existing image with remove option -->
551
+ <div class="relative {shapeAspects[shape]} bg-gray-100 dark:bg-gray-800 rounded-xl overflow-hidden group">
552
+ <img
553
+ src={imageArray[0]}
554
+ alt="Uploaded"
555
+ class="w-full h-full object-cover"
556
+ />
557
+ {#if !disabled}
558
+ <div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
559
+ <button
560
+ type="button"
561
+ class="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
562
+ onclick={() => openUploadModal(0)}
563
+ aria-label="Replace image"
564
+ >
565
+ <PlusOutline class="w-5 h-5 text-gray-700" />
566
+ </button>
567
+ <button
568
+ type="button"
569
+ class="p-2 bg-red-500/90 rounded-full hover:bg-red-500 transition-colors"
570
+ onclick={() => handleRemoveImage(0)}
571
+ aria-label="Remove image"
572
+ >
573
+ <CloseOutline class="w-5 h-5 text-white" />
574
+ </button>
575
+ </div>
576
+ {/if}
577
+ </div>
578
+ {:else}
579
+ <!-- Empty dropzone with direct drag-drop support -->
580
+ <div
581
+ class="{shapeAspects[shape]} filepond-wrapper-single {shape === 'wide' ? 'filepond-wide' : ''}
582
+ {isDropzoneDragOver ? 'dropzone-drag-over' : ''}"
583
+ role="button"
584
+ tabindex="0"
585
+ ondragover={handleDropzoneDragOver}
586
+ ondragleave={handleDropzoneDragLeave}
587
+ ondrop={(e) => handleDropzoneDrop(e, 0)}
588
+ >
589
+ <FilePond
590
+ bind:this={filePondInstance}
591
+ bind:files={filePondFiles}
592
+ {...filePondOptions}
593
+ onaddfile={handleFilePondAddFile}
594
+ />
595
+ </div>
596
+ {/if}
597
+
598
+ {#if helperText}
599
+ <p class={`${typography.smMuted} mt-2`}>{helperText}</p>
600
+ {/if}
601
+
602
+ {#if error}
603
+ <p class={`${typography.smMuted} text-red-500 mt-2`}>{error}</p>
604
+ {/if}
605
+ </div>
606
+
607
+ <!-- Multi Image Mode (Grid) -->
608
+ {:else}
609
+ <div class="image-uploader-multi {className}">
610
+ {#key gridRenderKey}
611
+ <div
612
+ bind:this={gridContainer}
613
+ class="{currentSizeConfig.containerClass}"
614
+ >
615
+ {#each slots as slot, index (slot.id)}
616
+ <div
617
+ class="relative {size === 'lg' ? shapeAspects[shape] : ''} {currentSizeConfig.slotClass} rounded-lg overflow-hidden"
618
+ class:cursor-grab={slot.image && enableReorder && !disabled}
619
+ class:empty-slot={!slot.image}
620
+ data-slot-id={slot.id}
621
+ >
622
+ {#if slot.image}
623
+ <!-- Filled slot -->
624
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
625
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
626
+ <div
627
+ class="relative w-full h-full bg-gray-100 dark:bg-gray-800 group"
628
+ class:cursor-pointer={onSetMain && index !== 0 && !disabled}
629
+ onclick={() => {
630
+ if (onSetMain && index !== 0 && !disabled) {
631
+ onSetMain({ index });
632
+ }
633
+ }}
634
+ >
635
+ <img
636
+ src={slot.image}
637
+ alt="Uploaded"
638
+ class="w-full h-full object-cover pointer-events-none"
639
+ draggable="false"
640
+ />
641
+ {#if showMainBadge && index === 0}
642
+ <div class="absolute bottom-1 left-1 z-10 pointer-events-none">
643
+ <Badge variant="info" size={currentSizeConfig.badgeSize}>Main</Badge>
644
+ </div>
645
+ {/if}
646
+ {#if !disabled}
647
+ <button
648
+ type="button"
649
+ class="remove-btn absolute top-1 right-1 {currentSizeConfig.removeSize} bg-white/90 dark:bg-gray-900/90 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-white dark:hover:bg-gray-900"
650
+ onclick={(e) => { e.stopPropagation(); handleRemoveImage(index); }}
651
+ aria-label="Remove photo"
652
+ >
653
+ <CloseOutline class="w-3 h-3 text-gray-700 dark:text-gray-300" />
654
+ </button>
655
+ {/if}
656
+ </div>
657
+ {:else if slot.isNext && canAddMore && !disabled}
658
+ <!-- Next available slot - dropzone with direct drag-drop support -->
659
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
660
+ <div
661
+ class="w-full h-full border-2 border-dashed rounded-lg transition-colors flex items-center justify-center cursor-pointer
662
+ {isDropzoneDragOver
663
+ ? 'border-blue-500 bg-blue-100 dark:bg-blue-900/40'
664
+ : 'border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20'}"
665
+ role="button"
666
+ tabindex="0"
667
+ onclick={() => openUploadModal(index)}
668
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') openUploadModal(index); }}
669
+ ondragenter={(e) => { e.preventDefault(); e.stopPropagation(); isDropzoneDragOver = true; }}
670
+ ondragover={(e) => { e.preventDefault(); e.stopPropagation(); isDropzoneDragOver = true; }}
671
+ ondragleave={(e) => { e.preventDefault(); e.stopPropagation(); isDropzoneDragOver = false; }}
672
+ ondrop={(e) => handleDropzoneDrop(e, index)}
673
+ aria-label="Add photo - drag and drop or click to upload"
674
+ >
675
+ <PlusOutline class={currentSizeConfig.iconClass + ' text-gray-400 dark:text-gray-500'} />
676
+ </div>
677
+ {/if}
678
+ </div>
679
+ {/each}
680
+ </div>
681
+ {/key}
682
+
683
+ {#if helperText}
684
+ <p class={`${typography.smMuted} mt-3`}>{helperText}</p>
685
+ {/if}
686
+
687
+ {#if error}
688
+ <p class={`${typography.smMuted} text-red-500 mt-2`}>{error}</p>
689
+ {/if}
690
+ </div>
691
+
692
+ <!-- Upload Modal for multi-image (native dropzone - more reliable than FilePond) -->
693
+ {#if showFilePondModal}
694
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
695
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
696
+ <div
697
+ class="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50 dark:bg-gray-900/80 p-4"
698
+ onclick={() => (showFilePondModal = false)}
699
+ ondragover={(e) => e.preventDefault()}
700
+ ondrop={(e) => e.preventDefault()}
701
+ >
702
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
703
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
704
+ <div
705
+ class="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-md overflow-hidden"
706
+ onclick={(e) => e.stopPropagation()}
707
+ >
708
+ <div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
709
+ <h3 class={typography.h3}>Upload photo</h3>
710
+ <button
711
+ type="button"
712
+ class="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
713
+ onclick={() => (showFilePondModal = false)}
714
+ aria-label="Close"
715
+ >
716
+ <CloseOutline class="w-5 h-5" />
717
+ </button>
718
+ </div>
719
+ <div class="p-4">
720
+ <!-- Native dropzone with file input -->
721
+ <div
722
+ class="dropzone-native border-2 border-dashed rounded-lg transition-all duration-200 min-h-[200px] flex flex-col items-center justify-center cursor-pointer
723
+ {isModalDragOver
724
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30'
725
+ : 'border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 hover:border-blue-400 hover:bg-blue-50/50 dark:hover:bg-blue-900/20'}"
726
+ role="button"
727
+ tabindex="0"
728
+ onclick={() => modalFileInput?.click()}
729
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') modalFileInput?.click(); }}
730
+ ondragenter={(e) => { e.preventDefault(); e.stopPropagation(); isModalDragOver = true; }}
731
+ ondragover={(e) => { e.preventDefault(); e.stopPropagation(); isModalDragOver = true; }}
732
+ ondragleave={(e) => { e.preventDefault(); e.stopPropagation(); isModalDragOver = false; }}
733
+ ondrop={(e) => handleModalDrop(e)}
734
+ >
735
+ <PlusOutline class="w-8 h-8 mb-2 text-gray-400" />
736
+ <span class={typography.smMuted}>{emptyLabel}</span>
737
+ <span class={`${typography.xsMuted} mt-1`}>JPG, PNG, WebP</span>
738
+ </div>
739
+ <input
740
+ bind:this={modalFileInput}
741
+ type="file"
742
+ accept={acceptedTypes.join(',')}
743
+ class="hidden"
744
+ onchange={handleModalFileSelect}
745
+ />
746
+ </div>
747
+ </div>
748
+ </div>
749
+ {/if}
750
+ {/if}
751
+
752
+ <!-- Crop Modal -->
753
+ {#if enableCrop}
754
+ <CropImage
755
+ bind:showModal={showCropModal}
756
+ imageSrc={imageForCrop}
757
+ onSave={handleCropSave}
758
+ onCancel={handleCropCancel}
759
+ isUploadingImage={isUploading}
760
+ />
761
+ {/if}
762
+
763
+ <style>
764
+ /* FilePond customizations */
765
+ :global(.filepond-wrapper .filepond--root) {
766
+ font-family: inherit;
767
+ min-height: 180px;
768
+ }
769
+
770
+ :global(.filepond-wrapper .filepond--panel-root) {
771
+ background-color: transparent;
772
+ }
773
+
774
+ :global(.filepond-wrapper .filepond--drop-label) {
775
+ color: inherit;
776
+ min-height: 180px;
777
+ display: flex;
778
+ align-items: center;
779
+ justify-content: center;
780
+ }
781
+
782
+ :global(.filepond-wrapper .filepond--drop-label label) {
783
+ display: flex;
784
+ flex-direction: column;
785
+ align-items: center;
786
+ justify-content: center;
787
+ width: 100%;
788
+ height: 100%;
789
+ cursor: pointer;
790
+ }
791
+
792
+ :global(.filepond-wrapper .filepond--label-action) {
793
+ text-decoration: none;
794
+ color: inherit;
795
+ }
796
+
797
+ /* FilePond drag hover state */
798
+ :global(.filepond-wrapper .filepond--root[data-hopper-state="drag-over"]) {
799
+ --tw-border-opacity: 1;
800
+ border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
801
+ --tw-bg-opacity: 1;
802
+ background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1));
803
+ }
804
+ :global(.filepond-wrapper .filepond--root[data-hopper-state="drag-over"]):is(.dark *) {
805
+ background-color: rgb(30 58 138 / 0.3);
806
+ }
807
+
808
+ :global(.filepond-wrapper .filepond--root[data-hopper-state="drag-over"] .filepond--panel-root) {
809
+ --tw-border-opacity: 1;
810
+ border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
811
+ }
812
+
813
+ /* Modal FilePond styling */
814
+ :global(.filepond-wrapper) {
815
+ border-radius: 0.5rem;
816
+ border-width: 2px;
817
+ border-style: dashed;
818
+ --tw-border-opacity: 1;
819
+ border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
820
+ --tw-bg-opacity: 1;
821
+ background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
822
+ }
823
+ :global(.filepond-wrapper):is(.dark *) {
824
+ --tw-border-opacity: 1;
825
+ border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
826
+ --tw-bg-opacity: 1;
827
+ background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
828
+ }
829
+ :global(.filepond-wrapper) {
830
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
831
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
832
+ transition-duration: 150ms;
833
+ }
834
+
835
+ :global(.filepond-wrapper:hover) {
836
+ --tw-border-opacity: 1;
837
+ border-color: rgb(96 165 250 / var(--tw-border-opacity, 1));
838
+ }
839
+
840
+ /* Single mode dropzone styling */
841
+ .filepond-wrapper-single {
842
+ border-radius: 0.75rem;
843
+ border-width: 2px;
844
+ border-style: dashed;
845
+ --tw-border-opacity: 1;
846
+ border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
847
+ --tw-bg-opacity: 1;
848
+ background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
849
+ }
850
+ .filepond-wrapper-single:is(.dark *) {
851
+ --tw-border-opacity: 1;
852
+ border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
853
+ --tw-bg-opacity: 1;
854
+ background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
855
+ }
856
+ .filepond-wrapper-single {
857
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
858
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
859
+ transition-duration: 150ms;
860
+ }
861
+ .filepond-wrapper-single:hover {
862
+ --tw-border-opacity: 1;
863
+ border-color: rgb(96 165 250 / var(--tw-border-opacity, 1));
864
+ --tw-bg-opacity: 1;
865
+ background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1));
866
+ }
867
+ .filepond-wrapper-single:hover:is(.dark *) {
868
+ background-color: rgb(30 58 138 / 0.2);
869
+ }
870
+
871
+ .filepond-wrapper-single :global(.filepond--root) {
872
+ height: 100%;
873
+ }
874
+
875
+ .filepond-wrapper-single :global(.filepond--root .filepond--drop-label) {
876
+ height: 100%;
877
+ min-height: 0px;
878
+ }
879
+
880
+ /* Wide shape specific styling */
881
+ .filepond-wide {
882
+ aspect-ratio: 16 / 9;
883
+ }
884
+
885
+ .filepond-wide :global(.filepond--root .filepond--drop-label label) {
886
+ display: flex;
887
+ height: 100%;
888
+ flex-direction: column;
889
+ align-items: center;
890
+ justify-content: center;
891
+ padding-top: 2rem;
892
+ padding-bottom: 2rem;
893
+ }
894
+
895
+ /* Direct drag-over state for dropzone */
896
+ .dropzone-drag-over {
897
+ --tw-border-opacity: 1;
898
+ border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
899
+ --tw-bg-opacity: 1;
900
+ background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
901
+ }
902
+ .dropzone-drag-over:is(.dark *) {
903
+ background-color: rgb(30 58 138 / 0.4);
904
+ }
905
+
906
+ /* Custom label styling inside FilePond */
907
+ :global(.filepond-custom-label) {
908
+ display: flex;
909
+ flex-direction: column;
910
+ align-items: center;
911
+ justify-content: center;
912
+ }
913
+
914
+ /* Cursor for draggable items */
915
+ .cursor-grab {
916
+ cursor: grab;
917
+ }
918
+
919
+ .cursor-grab:active {
920
+ cursor: grabbing;
921
+ }
922
+
923
+ /* SortableJS classes - must be single class names, not space-separated */
924
+ :global(.sortable-ghost) {
925
+ opacity: 0.5;
926
+ }
927
+
928
+ :global(.sortable-chosen) {
929
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
930
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
931
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
932
+ --tw-ring-opacity: 1;
933
+ --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
934
+ }
935
+
936
+ :global(.sortable-drag) {
937
+ cursor: grabbing;
938
+ }
939
+ </style>