ui-svelte 0.2.11 → 0.2.13

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 (225) hide show
  1. package/README.md +2 -2
  2. package/dist/charts/ArcChart.svelte +9 -14
  3. package/dist/charts/ArcChart.svelte.d.ts +3 -3
  4. package/dist/charts/AreaChart.svelte +347 -118
  5. package/dist/charts/AreaChart.svelte.d.ts +33 -4
  6. package/dist/charts/BarChart.svelte +288 -66
  7. package/dist/charts/BarChart.svelte.d.ts +26 -1
  8. package/dist/charts/Candlestick.svelte +53 -50
  9. package/dist/charts/Candlestick.svelte.d.ts +8 -8
  10. package/dist/charts/LineChart.svelte +391 -91
  11. package/dist/charts/LineChart.svelte.d.ts +26 -3
  12. package/dist/charts/PieChart.svelte +333 -92
  13. package/dist/charts/PieChart.svelte.d.ts +33 -5
  14. package/dist/charts/css/arc-chart.css +3 -3
  15. package/dist/charts/css/area-chart.css +127 -29
  16. package/dist/charts/css/bar-chart.css +114 -8
  17. package/dist/charts/css/candlestick.css +2 -0
  18. package/dist/charts/css/line-chart.css +111 -13
  19. package/dist/charts/css/pie-chart.css +92 -20
  20. package/dist/control/Audio.svelte +86 -44
  21. package/dist/control/Audio.svelte.d.ts +4 -1
  22. package/dist/control/Button.svelte +18 -27
  23. package/dist/control/Button.svelte.d.ts +3 -2
  24. package/dist/control/Fab.svelte +103 -0
  25. package/dist/control/Fab.svelte.d.ts +25 -0
  26. package/dist/control/IconButton.svelte +17 -27
  27. package/dist/control/IconButton.svelte.d.ts +3 -3
  28. package/dist/control/Image.svelte +123 -0
  29. package/dist/control/Image.svelte.d.ts +13 -0
  30. package/dist/control/Record.svelte +141 -98
  31. package/dist/control/Record.svelte.d.ts +2 -1
  32. package/dist/control/ToggleGroup.svelte +22 -8
  33. package/dist/control/ToggleGroup.svelte.d.ts +2 -1
  34. package/dist/control/ToggleTheme.svelte +13 -11
  35. package/dist/control/ToggleTheme.svelte.d.ts +3 -2
  36. package/dist/control/Video.svelte +57 -29
  37. package/dist/control/Video.svelte.d.ts +1 -0
  38. package/dist/control/css/btn.css +200 -152
  39. package/dist/control/css/fab.css +84 -0
  40. package/dist/control/css/image.css +56 -0
  41. package/dist/control/css/media.css +95 -30
  42. package/dist/control/css/toggle-group.css +253 -84
  43. package/dist/control/css/video.css +1 -14
  44. package/dist/css/animations.css +5 -9
  45. package/dist/css/base.css +13 -347
  46. package/dist/css/decorations.css +561 -0
  47. package/dist/css/rich-text.css +485 -0
  48. package/dist/css/transitions.css +158 -0
  49. package/dist/css/typography.css +291 -0
  50. package/dist/css/utilities.css +0 -4
  51. package/dist/display/Accordion.svelte +28 -4
  52. package/dist/display/Accordion.svelte.d.ts +2 -1
  53. package/dist/display/Alert.svelte +32 -12
  54. package/dist/display/Alert.svelte.d.ts +2 -3
  55. package/dist/display/Avatar.svelte +23 -18
  56. package/dist/display/Avatar.svelte.d.ts +4 -1
  57. package/dist/display/AvatarGroup.svelte +20 -18
  58. package/dist/display/AvatarGroup.svelte.d.ts +6 -3
  59. package/dist/display/Badge.svelte +11 -4
  60. package/dist/display/Badge.svelte.d.ts +2 -1
  61. package/dist/display/Card.svelte +15 -14
  62. package/dist/display/Card.svelte.d.ts +2 -3
  63. package/dist/display/Carousel.svelte +130 -99
  64. package/dist/display/Carousel.svelte.d.ts +6 -4
  65. package/dist/display/ChatBox.svelte +245 -106
  66. package/dist/display/ChatBox.svelte.d.ts +32 -5
  67. package/dist/display/Chip.svelte +31 -17
  68. package/dist/display/Chip.svelte.d.ts +3 -2
  69. package/dist/display/Code.svelte +7 -4
  70. package/dist/display/Code.svelte.d.ts +1 -0
  71. package/dist/display/Collapsible.svelte +30 -4
  72. package/dist/display/Collapsible.svelte.d.ts +2 -1
  73. package/dist/display/Countdown.svelte +169 -0
  74. package/dist/display/Countdown.svelte.d.ts +21 -0
  75. package/dist/display/Empty.svelte +37 -3
  76. package/dist/display/Empty.svelte.d.ts +3 -0
  77. package/dist/display/Item.svelte +42 -11
  78. package/dist/display/Item.svelte.d.ts +4 -2
  79. package/dist/display/Map.svelte +488 -0
  80. package/dist/display/Map.svelte.d.ts +44 -0
  81. package/dist/display/Marquee.svelte +0 -2
  82. package/dist/display/Section.svelte +14 -12
  83. package/dist/display/Section.svelte.d.ts +2 -3
  84. package/dist/display/Skeleton.svelte +32 -0
  85. package/dist/display/Skeleton.svelte.d.ts +10 -0
  86. package/dist/display/Table.svelte +94 -132
  87. package/dist/display/Table.svelte.d.ts +10 -1
  88. package/dist/display/css/accordion.css +349 -52
  89. package/dist/display/css/alert.css +38 -18
  90. package/dist/display/css/avatar-group.css +38 -75
  91. package/dist/display/css/avatar.css +139 -121
  92. package/dist/display/css/badge.css +50 -27
  93. package/dist/display/css/card.css +123 -71
  94. package/dist/display/css/carousel.css +25 -5
  95. package/dist/display/css/chat-box.css +158 -26
  96. package/dist/display/css/chip.css +142 -68
  97. package/dist/display/css/code.css +2 -6
  98. package/dist/display/css/collapsible.css +349 -45
  99. package/dist/display/css/countdown.css +206 -0
  100. package/dist/display/css/divider.css +8 -6
  101. package/dist/display/css/empty.css +7 -0
  102. package/dist/display/css/item.css +330 -84
  103. package/dist/display/css/map.css +164 -0
  104. package/dist/display/css/marquee.css +0 -3
  105. package/dist/display/css/section.css +89 -65
  106. package/dist/display/css/skeleton.css +58 -0
  107. package/dist/display/css/table.css +309 -193
  108. package/dist/form/Checkbox.svelte +11 -5
  109. package/dist/form/Checkbox.svelte.d.ts +2 -1
  110. package/dist/form/ColorField.svelte +601 -0
  111. package/dist/form/ColorField.svelte.d.ts +29 -0
  112. package/dist/form/ComboBox.svelte +24 -9
  113. package/dist/form/ComboBox.svelte.d.ts +2 -2
  114. package/dist/form/CsvField.svelte +62 -136
  115. package/dist/form/CsvField.svelte.d.ts +2 -2
  116. package/dist/form/DateField.svelte +33 -15
  117. package/dist/form/DateField.svelte.d.ts +2 -1
  118. package/dist/form/DateRange.svelte +436 -0
  119. package/dist/form/DateRange.svelte.d.ts +24 -0
  120. package/dist/form/DragDrop.svelte +578 -0
  121. package/dist/form/DragDrop.svelte.d.ts +33 -0
  122. package/dist/form/Dropzone.svelte +28 -8
  123. package/dist/form/Dropzone.svelte.d.ts +2 -2
  124. package/dist/form/Editor.svelte +626 -0
  125. package/dist/form/Editor.svelte.d.ts +50 -0
  126. package/dist/form/ImageCropper.svelte +422 -61
  127. package/dist/form/ImageCropper.svelte.d.ts +15 -1
  128. package/dist/form/{PasswordStrength.svelte → PasswordField.svelte} +58 -24
  129. package/dist/form/{PasswordStrength.svelte.d.ts → PasswordField.svelte.d.ts} +6 -5
  130. package/dist/form/PhoneField.svelte +26 -14
  131. package/dist/form/PhoneField.svelte.d.ts +4 -3
  132. package/dist/form/PinField.svelte +39 -31
  133. package/dist/form/PinField.svelte.d.ts +3 -3
  134. package/dist/form/RadioGroup.svelte +9 -5
  135. package/dist/form/RadioGroup.svelte.d.ts +1 -1
  136. package/dist/form/Select.svelte +20 -19
  137. package/dist/form/Select.svelte.d.ts +2 -2
  138. package/dist/form/Slider.svelte +10 -4
  139. package/dist/form/Slider.svelte.d.ts +2 -1
  140. package/dist/form/TextField.svelte +29 -11
  141. package/dist/form/TextField.svelte.d.ts +5 -4
  142. package/dist/form/Textarea.svelte +15 -6
  143. package/dist/form/Textarea.svelte.d.ts +2 -2
  144. package/dist/form/Toggle.svelte +7 -3
  145. package/dist/form/Toggle.svelte.d.ts +1 -1
  146. package/dist/form/css/checkbox.css +18 -2
  147. package/dist/form/css/color-field.css +141 -0
  148. package/dist/form/css/control.css +193 -82
  149. package/dist/form/css/csv-field.css +221 -0
  150. package/dist/form/css/date-range.css +122 -0
  151. package/dist/form/css/date.css +24 -2
  152. package/dist/form/css/drag-drop.css +234 -0
  153. package/dist/form/css/dropzone.css +153 -34
  154. package/dist/form/css/editor.css +367 -0
  155. package/dist/form/css/field.css +4 -0
  156. package/dist/form/css/image-cropper.css +242 -20
  157. package/dist/form/css/radio-group.css +26 -1
  158. package/dist/form/css/select.css +2 -2
  159. package/dist/form/css/slider.css +37 -0
  160. package/dist/form/css/textarea.css +178 -75
  161. package/dist/form/css/toggle.css +15 -3
  162. package/dist/hooks/use-chat.svelte.js +1 -1
  163. package/dist/hooks/use-form.svelte.js +3 -3
  164. package/dist/hooks/use-search.svelte.js +0 -3
  165. package/dist/hooks/use-table.svelte.d.ts +1 -0
  166. package/dist/hooks/use-table.svelte.js +6 -0
  167. package/dist/icons/index.d.ts +34 -2
  168. package/dist/icons/index.js +36 -4
  169. package/dist/index.css +44 -49
  170. package/dist/index.d.ts +14 -4
  171. package/dist/index.js +13 -3
  172. package/dist/layout/AppBar.svelte +22 -14
  173. package/dist/layout/AppBar.svelte.d.ts +2 -1
  174. package/dist/layout/Footer.svelte +19 -11
  175. package/dist/layout/Footer.svelte.d.ts +2 -1
  176. package/dist/layout/Provider.svelte +32 -9
  177. package/dist/layout/Provider.svelte.d.ts +3 -1
  178. package/dist/layout/Sidebar.svelte +17 -8
  179. package/dist/layout/Sidebar.svelte.d.ts +2 -1
  180. package/dist/layout/css/app-bar.css +63 -66
  181. package/dist/layout/css/footer.css +62 -65
  182. package/dist/layout/css/sidebar.css +120 -59
  183. package/dist/navigation/BottomNav.svelte +51 -14
  184. package/dist/navigation/FooterGroup.svelte +1 -1
  185. package/dist/navigation/NavMenu.svelte +47 -23
  186. package/dist/navigation/NavMenu.svelte.d.ts +29 -0
  187. package/dist/navigation/Pagination.svelte +158 -0
  188. package/dist/navigation/Pagination.svelte.d.ts +18 -0
  189. package/dist/navigation/SideNav.svelte +30 -25
  190. package/dist/navigation/SideNav.svelte.d.ts +2 -3
  191. package/dist/navigation/Tabs.svelte +17 -7
  192. package/dist/navigation/Tabs.svelte.d.ts +2 -2
  193. package/dist/navigation/css/bottom-nav.css +319 -257
  194. package/dist/navigation/css/footer-group.css +1 -1
  195. package/dist/navigation/css/footer-nav.css +1 -1
  196. package/dist/navigation/css/nav-menu.css +331 -106
  197. package/dist/navigation/css/pagination.css +74 -0
  198. package/dist/navigation/css/side-nav.css +514 -75
  199. package/dist/navigation/css/tabs.css +246 -52
  200. package/dist/overlay/AlertDialog.svelte +58 -0
  201. package/dist/overlay/AlertDialog.svelte.d.ts +14 -25
  202. package/dist/overlay/Command.svelte +347 -0
  203. package/dist/overlay/Command.svelte.d.ts +33 -25
  204. package/dist/overlay/Drawer.svelte +49 -21
  205. package/dist/overlay/Drawer.svelte.d.ts +2 -2
  206. package/dist/overlay/Dropdown.svelte +3 -3
  207. package/dist/overlay/Modal.svelte +51 -16
  208. package/dist/overlay/Modal.svelte.d.ts +3 -3
  209. package/dist/overlay/Toast.svelte +41 -17
  210. package/dist/overlay/Toast.svelte.d.ts +1 -1
  211. package/dist/overlay/Tooltip.svelte +36 -27
  212. package/dist/overlay/Tooltip.svelte.d.ts +2 -2
  213. package/dist/overlay/css/command.css +68 -0
  214. package/dist/overlay/css/drawer.css +63 -24
  215. package/dist/overlay/css/dropdown.css +1 -1
  216. package/dist/overlay/css/hovercard.css +1 -1
  217. package/dist/overlay/css/modal.css +79 -27
  218. package/dist/overlay/css/toast.css +40 -24
  219. package/dist/overlay/css/tooltip.css +110 -67
  220. package/dist/stores/theme.svelte.js +44 -12
  221. package/dist/stores/toast.svelte.d.ts +4 -4
  222. package/dist/stores/toast.svelte.js +2 -2
  223. package/package.json +1 -1
  224. package/dist/utils/charts.d.ts +0 -27
  225. package/dist/utils/charts.js +0 -140
@@ -1,36 +1,80 @@
1
1
  <script lang="ts">
2
- import { Icon } from '../index.js';
2
+ import type { IconData } from '../display/Icon.svelte';
3
+ import {
4
+ ImageAddRegularIcon,
5
+ ArrowResetRegularIcon,
6
+ CropRegularIcon,
7
+ Dismiss24RegularIcon
8
+ } from '../icons/index.js';
9
+ import { Icon, IconButton, Modal, Button } from '../index.js';
3
10
  import { cn } from '../utils/class-names.js';
4
11
 
5
12
  type Props = {
6
- src: string;
13
+ src?: string;
7
14
  alt?: string;
8
15
  aspectRatio?: number;
9
16
  minWidth?: number;
10
17
  minHeight?: number;
11
18
  maxWidth?: number;
12
19
  maxHeight?: number;
20
+ accept?: string;
21
+ placeholder?: string;
22
+ icon?: IconData;
23
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
24
+ shape?: 'circle' | 'square';
25
+ color?: 'primary' | 'secondary' | 'muted' | 'success' | 'info' | 'danger' | 'warning';
26
+ variant?: 'solid' | 'soft' | 'outlined' | 'ghost';
27
+ disabled?: boolean;
13
28
  onCrop?: (croppedImage: string) => void;
29
+ onChange?: (file: File | null) => void;
14
30
  class?: string;
31
+ name?: string;
32
+ label?: string;
33
+ helpText?: string;
34
+ errorText?: string;
15
35
  };
16
36
 
17
37
  const {
18
- src,
19
- alt = 'Image to crop',
38
+ src: initialSrc,
39
+ alt = 'Avatar',
20
40
  aspectRatio,
21
41
  minWidth = 50,
22
42
  minHeight = 50,
23
43
  maxWidth,
24
44
  maxHeight,
45
+ accept = 'image/*',
46
+ placeholder = 'Upload',
47
+ icon = ImageAddRegularIcon,
48
+ size = 'md',
49
+ shape = 'circle',
50
+ color = 'muted',
51
+ variant = 'soft',
52
+ disabled = false,
25
53
  onCrop,
26
- class: className
54
+ onChange,
55
+ class: className,
56
+ name,
57
+ label,
58
+ helpText,
59
+ errorText
27
60
  }: Props = $props();
28
61
 
29
- let canvas: HTMLCanvasElement;
30
- let image: HTMLImageElement;
31
- let containerDiv: HTMLDivElement;
62
+ const uid = $props.id();
32
63
 
33
- let isDragging = $state(false);
64
+ let canvas = $state<HTMLCanvasElement>();
65
+ let image = $state<HTMLImageElement>();
66
+ let containerDiv = $state<HTMLDivElement>();
67
+ let fileInput = $state<HTMLInputElement>();
68
+
69
+ // svelte-ignore state_referenced_locally
70
+ let currentSrc = $state(initialSrc || '');
71
+ // svelte-ignore state_referenced_locally
72
+ let croppedSrc = $state(initialSrc || '');
73
+ let uploadedFile = $state<File | null>(null);
74
+
75
+ let showModal = $state(false);
76
+
77
+ let isCropDragging = $state(false);
34
78
  let isResizing = $state(false);
35
79
  let resizeHandle = $state<string>('');
36
80
 
@@ -51,6 +95,59 @@
51
95
  let imageHeight = $state(0);
52
96
  let scale = $state(1);
53
97
 
98
+ let isTouchDevice = $state(false);
99
+
100
+ const sizeClasses = {
101
+ xs: 'is-xs',
102
+ sm: 'is-sm',
103
+ md: 'is-md',
104
+ lg: 'is-lg',
105
+ xl: 'is-xl'
106
+ };
107
+
108
+ const colorClasses = {
109
+ primary: 'is-primary',
110
+ secondary: 'is-secondary',
111
+ muted: 'is-muted',
112
+ success: 'is-success',
113
+ info: 'is-info',
114
+ danger: 'is-danger',
115
+ warning: 'is-warning'
116
+ };
117
+
118
+ const variantClasses = {
119
+ solid: 'is-solid',
120
+ soft: 'is-soft',
121
+ outlined: 'is-outlined',
122
+ ghost: 'is-ghost'
123
+ };
124
+
125
+ $effect(() => {
126
+ if (initialSrc && initialSrc !== currentSrc) {
127
+ currentSrc = initialSrc;
128
+ croppedSrc = initialSrc;
129
+ imageLoaded = false;
130
+ }
131
+ });
132
+
133
+ function handleFiles(fileList: FileList | null) {
134
+ if (!fileList || fileList.length === 0 || disabled) return;
135
+
136
+ const file = fileList[0];
137
+ if (!file.type.startsWith('image/')) return;
138
+
139
+ if (uploadedFile && currentSrc.startsWith('blob:')) {
140
+ URL.revokeObjectURL(currentSrc);
141
+ }
142
+
143
+ uploadedFile = file;
144
+ currentSrc = URL.createObjectURL(file);
145
+ imageLoaded = false;
146
+ onChange?.(file);
147
+
148
+ showModal = true;
149
+ }
150
+
54
151
  function handleImageLoad() {
55
152
  if (!image || !containerDiv) return;
56
153
 
@@ -67,7 +164,7 @@
67
164
  imageWidth = naturalWidth * scale;
68
165
  imageHeight = naturalHeight * scale;
69
166
 
70
- const initialSize = Math.min(imageWidth, imageHeight) * 0.6;
167
+ const initialSize = Math.min(imageWidth, imageHeight) * 0.8;
71
168
  cropWidth = aspectRatio ? initialSize : initialSize;
72
169
  cropHeight = aspectRatio ? initialSize / aspectRatio : initialSize;
73
170
  cropX = (imageWidth - cropWidth) / 2;
@@ -84,7 +181,7 @@
84
181
  isResizing = true;
85
182
  resizeHandle = handle;
86
183
  } else {
87
- isDragging = true;
184
+ isCropDragging = true;
88
185
  }
89
186
 
90
187
  startX = e.clientX;
@@ -102,7 +199,7 @@
102
199
  const deltaX = e.clientX - startX;
103
200
  const deltaY = e.clientY - startY;
104
201
 
105
- if (isDragging) {
202
+ if (isCropDragging) {
106
203
  cropX = Math.max(0, Math.min(imageWidth - cropWidth, startCropX + deltaX));
107
204
  cropY = Math.max(0, Math.min(imageHeight - cropHeight, startCropY + deltaY));
108
205
  } else if (isResizing) {
@@ -148,13 +245,121 @@
148
245
  }
149
246
 
150
247
  function handleMouseUp() {
151
- isDragging = false;
248
+ isCropDragging = false;
152
249
  isResizing = false;
153
250
  resizeHandle = '';
154
251
  document.removeEventListener('mousemove', handleMouseMove);
155
252
  document.removeEventListener('mouseup', handleMouseUp);
156
253
  }
157
254
 
255
+ function handleTouchStart(e: TouchEvent, handle?: string) {
256
+ e.preventDefault();
257
+ e.stopPropagation();
258
+
259
+ const touch = e.touches[0];
260
+
261
+ if (handle) {
262
+ isResizing = true;
263
+ resizeHandle = handle;
264
+ } else {
265
+ isCropDragging = true;
266
+ }
267
+
268
+ startX = touch.clientX;
269
+ startY = touch.clientY;
270
+ startCropX = cropX;
271
+ startCropY = cropY;
272
+ startCropWidth = cropWidth;
273
+ startCropHeight = cropHeight;
274
+ }
275
+
276
+ function handleTouchMove(e: TouchEvent) {
277
+ if (!isCropDragging && !isResizing) return;
278
+
279
+ e.preventDefault();
280
+ const touch = e.touches[0];
281
+ const deltaX = touch.clientX - startX;
282
+ const deltaY = touch.clientY - startY;
283
+
284
+ if (isCropDragging) {
285
+ cropX = Math.max(0, Math.min(imageWidth - cropWidth, startCropX + deltaX));
286
+ cropY = Math.max(0, Math.min(imageHeight - cropHeight, startCropY + deltaY));
287
+ } else if (isResizing) {
288
+ let newWidth = startCropWidth;
289
+ let newHeight = startCropHeight;
290
+ let newX = startCropX;
291
+ let newY = startCropY;
292
+
293
+ if (resizeHandle.includes('e')) {
294
+ newWidth = Math.max(minWidth, Math.min(imageWidth - startCropX, startCropWidth + deltaX));
295
+ }
296
+ if (resizeHandle.includes('w')) {
297
+ newWidth = Math.max(minWidth, startCropWidth - deltaX);
298
+ newX = startCropX + (startCropWidth - newWidth);
299
+ }
300
+ if (resizeHandle.includes('s')) {
301
+ newHeight = Math.max(
302
+ minHeight,
303
+ Math.min(imageHeight - startCropY, startCropHeight + deltaY)
304
+ );
305
+ }
306
+ if (resizeHandle.includes('n')) {
307
+ newHeight = Math.max(minHeight, startCropHeight - deltaY);
308
+ newY = startCropY + (startCropHeight - newHeight);
309
+ }
310
+
311
+ if (aspectRatio) {
312
+ if (resizeHandle.includes('e') || resizeHandle.includes('w')) {
313
+ newHeight = newWidth / aspectRatio;
314
+ } else {
315
+ newWidth = newHeight * aspectRatio;
316
+ }
317
+ }
318
+
319
+ if (maxWidth) newWidth = Math.min(newWidth, maxWidth);
320
+ if (maxHeight) newHeight = Math.min(newHeight, maxHeight);
321
+
322
+ cropWidth = newWidth;
323
+ cropHeight = newHeight;
324
+ cropX = Math.max(0, Math.min(imageWidth - cropWidth, newX));
325
+ cropY = Math.max(0, Math.min(imageHeight - cropHeight, newY));
326
+ }
327
+ }
328
+
329
+ function handleTouchEnd() {
330
+ isCropDragging = false;
331
+ isResizing = false;
332
+ resizeHandle = '';
333
+ }
334
+
335
+ import { onMount } from 'svelte';
336
+
337
+ onMount(() => {
338
+ isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
339
+
340
+ const onGlobalTouchMove = (e: TouchEvent) => {
341
+ if (isCropDragging || isResizing) {
342
+ handleTouchMove(e);
343
+ }
344
+ };
345
+
346
+ const onGlobalTouchEnd = () => {
347
+ if (isCropDragging || isResizing) {
348
+ handleTouchEnd();
349
+ }
350
+ };
351
+
352
+ document.addEventListener('touchmove', onGlobalTouchMove, { passive: false });
353
+ document.addEventListener('touchend', onGlobalTouchEnd);
354
+ document.addEventListener('touchcancel', onGlobalTouchEnd);
355
+
356
+ return () => {
357
+ document.removeEventListener('touchmove', onGlobalTouchMove);
358
+ document.removeEventListener('touchend', onGlobalTouchEnd);
359
+ document.removeEventListener('touchcancel', onGlobalTouchEnd);
360
+ };
361
+ });
362
+
158
363
  function handleCrop() {
159
364
  if (!canvas || !image) return;
160
365
 
@@ -180,75 +385,231 @@
180
385
  );
181
386
 
182
387
  const croppedImageUrl = canvas.toDataURL('image/png');
388
+ croppedSrc = croppedImageUrl;
183
389
  onCrop?.(croppedImageUrl);
390
+ showModal = false;
184
391
  }
185
392
 
186
393
  function handleReset() {
187
394
  if (!imageLoaded) return;
188
- const initialSize = Math.min(imageWidth, imageHeight) * 0.6;
395
+ const initialSize = Math.min(imageWidth, imageHeight) * 0.8;
189
396
  cropWidth = aspectRatio ? initialSize : initialSize;
190
397
  cropHeight = aspectRatio ? initialSize / aspectRatio : initialSize;
191
398
  cropX = (imageWidth - cropWidth) / 2;
192
399
  cropY = (imageHeight - cropHeight) / 2;
193
400
  }
401
+
402
+ function handleRemoveImage() {
403
+ if (uploadedFile && currentSrc.startsWith('blob:')) {
404
+ URL.revokeObjectURL(currentSrc);
405
+ }
406
+ uploadedFile = null;
407
+ currentSrc = '';
408
+ croppedSrc = '';
409
+ imageLoaded = false;
410
+ onChange?.(null);
411
+ }
412
+
413
+ function openCropModal() {
414
+ if (currentSrc && !disabled) {
415
+ showModal = true;
416
+ }
417
+ }
418
+
419
+ function closeModal() {
420
+ showModal = false;
421
+ }
194
422
  </script>
195
423
 
196
- <div class={cn('image-cropper', className)}>
197
- <div class="image-cropper-container" bind:this={containerDiv}>
198
- <img
199
- bind:this={image}
200
- {src}
201
- {alt}
202
- class="image-cropper-image"
203
- onload={handleImageLoad}
204
- style="width: {imageWidth}px; height: {imageHeight}px;"
424
+ <div class={cn('field', className)}>
425
+ {#if label}
426
+ <span class="field-label">{label}</span>
427
+ {/if}
428
+
429
+ <div class="image-cropper-wrapper">
430
+ <input
431
+ bind:this={fileInput}
432
+ id={`${uid}-${name}`}
433
+ type="file"
434
+ hidden
435
+ {accept}
436
+ {disabled}
437
+ {name}
438
+ onchange={(e) => handleFiles((e.target as HTMLInputElement).files)}
205
439
  />
206
440
 
207
- {#if imageLoaded}
208
- <div class="image-cropper-overlay">
209
- <div class="overlay-top" style="height: {cropY}px;"></div>
210
- <div class="overlay-middle" style="top: {cropY}px; height: {cropHeight}px;">
211
- <div class="overlay-left" style="width: {cropX}px;"></div>
212
- <div
213
- class="overlay-right"
214
- style="left: {cropX + cropWidth}px; width: {imageWidth - cropX - cropWidth}px;"
215
- ></div>
441
+ <button
442
+ type="button"
443
+ class={cn(
444
+ 'image-cropper-avatar',
445
+ sizeClasses[size],
446
+ colorClasses[color],
447
+ variantClasses[variant],
448
+ shape === 'circle' && 'is-circle',
449
+ disabled && 'is-disabled'
450
+ )}
451
+ class:is-error={errorText}
452
+ onclick={() => (croppedSrc ? openCropModal() : fileInput?.click())}
453
+ {disabled}
454
+ >
455
+ {#if croppedSrc}
456
+ <img src={croppedSrc} {alt} class="image-cropper-preview" />
457
+ <div class="image-cropper-overlay-edit">
458
+ <Icon icon={CropRegularIcon} />
216
459
  </div>
217
- <div
218
- class="overlay-bottom"
219
- style="top: {cropY + cropHeight}px; height: {imageHeight - cropY - cropHeight}px;"
220
- ></div>
460
+ {:else}
461
+ <Icon {icon} class="image-cropper-icon" />
462
+ <span class="image-cropper-placeholder">{placeholder}</span>
463
+ {/if}
464
+ </button>
465
+
466
+ {#if croppedSrc}
467
+ <div class="image-cropper-actions">
468
+ <IconButton
469
+ icon={ImageAddRegularIcon}
470
+ variant="ghost"
471
+ {color}
472
+ size="sm"
473
+ onclick={() => fileInput?.click()}
474
+ />
475
+ {#if uploadedFile}
476
+ <IconButton
477
+ icon={Dismiss24RegularIcon}
478
+ variant="ghost"
479
+ color="danger"
480
+ size="sm"
481
+ onclick={handleRemoveImage}
482
+ />
483
+ {/if}
484
+ </div>
485
+ {/if}
486
+ </div>
221
487
 
222
- <!-- svelte-ignore a11y_no_static_element_interactions -->
488
+ {#if errorText || helpText}
489
+ <div class={cn('field-help', errorText && 'is-danger')}>
490
+ {errorText || helpText}
491
+ </div>
492
+ {/if}
493
+ </div>
494
+
495
+ <Modal bind:open={showModal} onclose={closeModal} color="surface" rootClass="image-cropper-modal">
496
+ {#snippet header()}
497
+ <h2 class="image-cropper-modal-title">Crop Image</h2>
498
+ {/snippet}
499
+
500
+ <div class="image-cropper-modal-body">
501
+ <div class="image-cropper-container" bind:this={containerDiv}>
502
+ <img
503
+ bind:this={image}
504
+ src={currentSrc}
505
+ {alt}
506
+ class="image-cropper-image"
507
+ onload={handleImageLoad}
508
+ style="width: {imageWidth}px; height: {imageHeight}px;"
509
+ />
510
+
511
+ {#if imageLoaded}
223
512
  <div
224
- class="crop-area"
225
- style="left: {cropX}px; top: {cropY}px; width: {cropWidth}px; height: {cropHeight}px;"
226
- onmousedown={(e) => handleMouseDown(e)}
513
+ class="image-cropper-crop-overlay"
514
+ style="width: {imageWidth}px; height: {imageHeight}px;"
227
515
  >
228
- <div class="crop-grid">
229
- <div class="grid-line grid-line-v" style="left: 33.33%;"></div>
230
- <div class="grid-line grid-line-v" style="left: 66.66%;"></div>
231
- <div class="grid-line grid-line-h" style="top: 33.33%;"></div>
232
- <div class="grid-line grid-line-h" style="top: 66.66%;"></div>
516
+ <div class="overlay-top" style="height: {cropY}px;"></div>
517
+ <div class="overlay-middle" style="top: {cropY}px; height: {cropHeight}px;">
518
+ <div class="overlay-left" style="width: {cropX}px;"></div>
519
+ <div
520
+ class="overlay-right"
521
+ style="left: {cropX + cropWidth}px; width: {imageWidth - cropX - cropWidth}px;"
522
+ ></div>
233
523
  </div>
524
+ <div
525
+ class="overlay-bottom"
526
+ style="top: {cropY + cropHeight}px; height: {imageHeight - cropY - cropHeight}px;"
527
+ ></div>
234
528
 
235
- <div class="resize-handle handle-nw" onmousedown={(e) => handleMouseDown(e, 'nw')}></div>
236
- <div class="resize-handle handle-n" onmousedown={(e) => handleMouseDown(e, 'n')}></div>
237
- <div class="resize-handle handle-ne" onmousedown={(e) => handleMouseDown(e, 'ne')}></div>
238
- <div class="resize-handle handle-e" onmousedown={(e) => handleMouseDown(e, 'e')}></div>
239
- <div class="resize-handle handle-se" onmousedown={(e) => handleMouseDown(e, 'se')}></div>
240
- <div class="resize-handle handle-s" onmousedown={(e) => handleMouseDown(e, 's')}></div>
241
- <div class="resize-handle handle-sw" onmousedown={(e) => handleMouseDown(e, 'sw')}></div>
242
- <div class="resize-handle handle-w" onmousedown={(e) => handleMouseDown(e, 'w')}></div>
529
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
530
+ <div
531
+ class="crop-area"
532
+ class:is-circle={shape === 'circle'}
533
+ style="left: {cropX}px; top: {cropY}px; width: {cropWidth}px; height: {cropHeight}px;"
534
+ onmousedown={(e) => handleMouseDown(e)}
535
+ ontouchstart={(e) => handleTouchStart(e)}
536
+ >
537
+ <div class="crop-grid">
538
+ <div class="grid-line grid-line-v" style="left: 33.33%;"></div>
539
+ <div class="grid-line grid-line-v" style="left: 66.66%;"></div>
540
+ <div class="grid-line grid-line-h" style="top: 33.33%;"></div>
541
+ <div class="grid-line grid-line-h" style="top: 66.66%;"></div>
542
+ </div>
543
+
544
+ <div
545
+ class="resize-handle handle-nw"
546
+ onmousedown={(e) => handleMouseDown(e, 'nw')}
547
+ ontouchstart={(e) => handleTouchStart(e, 'nw')}
548
+ ></div>
549
+ <div
550
+ class="resize-handle handle-n"
551
+ onmousedown={(e) => handleMouseDown(e, 'n')}
552
+ ontouchstart={(e) => handleTouchStart(e, 'n')}
553
+ ></div>
554
+ <div
555
+ class="resize-handle handle-ne"
556
+ onmousedown={(e) => handleMouseDown(e, 'ne')}
557
+ ontouchstart={(e) => handleTouchStart(e, 'ne')}
558
+ ></div>
559
+ <div
560
+ class="resize-handle handle-e"
561
+ onmousedown={(e) => handleMouseDown(e, 'e')}
562
+ ontouchstart={(e) => handleTouchStart(e, 'e')}
563
+ ></div>
564
+ <div
565
+ class="resize-handle handle-se"
566
+ onmousedown={(e) => handleMouseDown(e, 'se')}
567
+ ontouchstart={(e) => handleTouchStart(e, 'se')}
568
+ ></div>
569
+ <div
570
+ class="resize-handle handle-s"
571
+ onmousedown={(e) => handleMouseDown(e, 's')}
572
+ ontouchstart={(e) => handleTouchStart(e, 's')}
573
+ ></div>
574
+ <div
575
+ class="resize-handle handle-sw"
576
+ onmousedown={(e) => handleMouseDown(e, 'sw')}
577
+ ontouchstart={(e) => handleTouchStart(e, 'sw')}
578
+ ></div>
579
+ <div
580
+ class="resize-handle handle-w"
581
+ onmousedown={(e) => handleMouseDown(e, 'w')}
582
+ ontouchstart={(e) => handleTouchStart(e, 'w')}
583
+ ></div>
584
+ </div>
243
585
  </div>
244
- </div>
245
- {/if}
246
- </div>
586
+ {/if}
587
+ </div>
247
588
 
248
- <div class="image-cropper-controls">
249
- <button class="btn is-md is-outlined" onclick={handleReset}> refresh icon </button>
250
- <button class="btn is-md is-primary is-solid" onclick={handleCrop}> crop icon </button>
589
+ <canvas bind:this={canvas} class="image-cropper-canvas"></canvas>
251
590
  </div>
252
591
 
253
- <canvas bind:this={canvas} class="image-cropper-canvas"></canvas>
254
- </div>
592
+ {#snippet footer()}
593
+ <div class="image-cropper-modal-footer">
594
+ <Button
595
+ color="muted"
596
+ variant="ghost"
597
+ size="sm"
598
+ onclick={handleReset}
599
+ startIcon={ArrowResetRegularIcon}
600
+ >
601
+ Reset
602
+ </Button>
603
+ <Button color="muted" variant="outlined" size="sm" onclick={closeModal}>Cancel</Button>
604
+ <Button
605
+ color="primary"
606
+ variant="solid"
607
+ size="sm"
608
+ onclick={handleCrop}
609
+ startIcon={CropRegularIcon}
610
+ >
611
+ Apply
612
+ </Button>
613
+ </div>
614
+ {/snippet}
615
+ </Modal>
@@ -1,13 +1,27 @@
1
+ import type { IconData } from '../display/Icon.svelte';
1
2
  type Props = {
2
- src: string;
3
+ src?: string;
3
4
  alt?: string;
4
5
  aspectRatio?: number;
5
6
  minWidth?: number;
6
7
  minHeight?: number;
7
8
  maxWidth?: number;
8
9
  maxHeight?: number;
10
+ accept?: string;
11
+ placeholder?: string;
12
+ icon?: IconData;
13
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
14
+ shape?: 'circle' | 'square';
15
+ color?: 'primary' | 'secondary' | 'muted' | 'success' | 'info' | 'danger' | 'warning';
16
+ variant?: 'solid' | 'soft' | 'outlined' | 'ghost';
17
+ disabled?: boolean;
9
18
  onCrop?: (croppedImage: string) => void;
19
+ onChange?: (file: File | null) => void;
10
20
  class?: string;
21
+ name?: string;
22
+ label?: string;
23
+ helpText?: string;
24
+ errorText?: string;
11
25
  };
12
26
  declare const ImageCropper: import("svelte").Component<Props, {}, "">;
13
27
  type ImageCropper = ReturnType<typeof ImageCropper>;