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