@mezzanine-ui/react 1.0.0-beta.6 → 1.0.0-beta.7

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 (142) hide show
  1. package/Accordion/Accordion.d.ts +23 -1
  2. package/Accordion/Accordion.js +59 -11
  3. package/Accordion/AccordionActions.d.ts +13 -0
  4. package/Accordion/AccordionActions.js +24 -0
  5. package/Accordion/AccordionContent.d.ts +9 -0
  6. package/Accordion/{AccordionDetails.js → AccordionContent.js} +4 -6
  7. package/Accordion/AccordionControlContext.d.ts +2 -2
  8. package/Accordion/AccordionGroup.d.ts +10 -0
  9. package/Accordion/AccordionGroup.js +26 -0
  10. package/Accordion/AccordionTitle.d.ts +14 -0
  11. package/Accordion/AccordionTitle.js +56 -0
  12. package/Accordion/index.d.ts +8 -4
  13. package/Accordion/index.js +4 -2
  14. package/AutoComplete/AutoComplete.d.ts +20 -6
  15. package/AutoComplete/AutoComplete.js +118 -30
  16. package/Backdrop/Backdrop.js +15 -19
  17. package/Calendar/CalendarDays.js +1 -1
  18. package/Card/BaseCard.d.ts +11 -0
  19. package/Card/BaseCard.js +48 -0
  20. package/Card/BaseCardSkeleton.d.ts +14 -0
  21. package/Card/BaseCardSkeleton.js +18 -0
  22. package/Card/CardGroup.d.ts +47 -0
  23. package/Card/CardGroup.js +147 -0
  24. package/Card/FourThumbnailCard.d.ts +14 -0
  25. package/Card/FourThumbnailCard.js +73 -0
  26. package/Card/FourThumbnailCardSkeleton.d.ts +14 -0
  27. package/Card/FourThumbnailCardSkeleton.js +20 -0
  28. package/Card/QuickActionCard.d.ts +12 -0
  29. package/Card/QuickActionCard.js +23 -0
  30. package/Card/QuickActionCardSkeleton.d.ts +14 -0
  31. package/Card/QuickActionCardSkeleton.js +18 -0
  32. package/Card/SingleThumbnailCard.d.ts +13 -0
  33. package/Card/SingleThumbnailCard.js +44 -0
  34. package/Card/SingleThumbnailCardSkeleton.d.ts +19 -0
  35. package/Card/SingleThumbnailCardSkeleton.js +18 -0
  36. package/Card/Thumbnail.d.ts +12 -0
  37. package/Card/Thumbnail.js +18 -0
  38. package/Card/ThumbnailCardInfo.d.ts +34 -0
  39. package/Card/ThumbnailCardInfo.js +43 -0
  40. package/Card/index.d.ts +43 -4
  41. package/Card/index.js +19 -2
  42. package/Card/typings.d.ts +442 -0
  43. package/Checkbox/Checkbox.d.ts +8 -0
  44. package/Checkbox/Checkbox.js +3 -2
  45. package/Checkbox/CheckboxGroup.js +1 -1
  46. package/ContentHeader/ContentHeader.d.ts +22 -70
  47. package/ContentHeader/ContentHeader.js +1 -1
  48. package/ContentHeader/ContentHeaderResponsive.d.ts +9 -0
  49. package/ContentHeader/ContentHeaderResponsive.js +7 -0
  50. package/ContentHeader/utils.d.ts +3 -3
  51. package/ContentHeader/utils.js +66 -20
  52. package/Cropper/Cropper.d.ts +66 -0
  53. package/Cropper/Cropper.js +115 -0
  54. package/Cropper/CropperElement.d.ts +10 -0
  55. package/Cropper/CropperElement.js +892 -0
  56. package/Cropper/index.d.ts +18 -0
  57. package/Cropper/index.js +8 -0
  58. package/Cropper/tools.d.ts +90 -0
  59. package/Cropper/tools.js +143 -0
  60. package/Cropper/typings.d.ts +69 -0
  61. package/Cropper/utils/cropper-calculations.d.ts +39 -0
  62. package/Cropper/utils/cropper-calculations.js +95 -0
  63. package/DateTimePicker/DateTimePicker.d.ts +1 -1
  64. package/DateTimePicker/DateTimePicker.js +14 -1
  65. package/Dropdown/Dropdown.d.ts +7 -1
  66. package/Dropdown/Dropdown.js +31 -14
  67. package/Dropdown/DropdownItem.d.ts +7 -1
  68. package/Dropdown/DropdownItem.js +36 -6
  69. package/Dropdown/DropdownItemCard.js +2 -1
  70. package/FloatingButton/FloatingButton.d.ts +21 -0
  71. package/FloatingButton/FloatingButton.js +18 -0
  72. package/FloatingButton/index.d.ts +2 -0
  73. package/FloatingButton/index.js +1 -0
  74. package/Form/FormField.d.ts +21 -10
  75. package/Form/FormField.js +12 -4
  76. package/Input/Input.js +9 -2
  77. package/Message/Message.js +1 -1
  78. package/MultipleDatePicker/MultipleDatePicker.js +2 -2
  79. package/Navigation/NavigationHeader.js +1 -1
  80. package/Picker/FormattedInput.d.ts +1 -1
  81. package/Picker/FormattedInput.js +2 -1
  82. package/Picker/PickerTriggerWithSeparator.d.ts +10 -0
  83. package/Picker/PickerTriggerWithSeparator.js +2 -2
  84. package/Picker/useDateInputFormatter.d.ts +6 -0
  85. package/Picker/useDateInputFormatter.js +4 -1
  86. package/Select/Select.d.ts +2 -8
  87. package/Select/Select.js +12 -33
  88. package/Select/SelectTrigger.js +21 -7
  89. package/Select/index.d.ts +0 -4
  90. package/Select/index.js +0 -2
  91. package/Select/typings.d.ts +0 -4
  92. package/Select/useSelectTriggerTags.d.ts +1 -1
  93. package/Select/useSelectTriggerTags.js +9 -6
  94. package/Separator/Separator.d.ts +14 -0
  95. package/Separator/Separator.js +17 -0
  96. package/Separator/index.d.ts +2 -0
  97. package/Separator/index.js +1 -0
  98. package/Table/utils/useTableRowSelection.js +6 -0
  99. package/Tag/TagGroup.d.ts +4 -2
  100. package/Tag/TagGroup.js +7 -4
  101. package/TextField/TextField.d.ts +1 -1
  102. package/TextField/TextField.js +63 -9
  103. package/TimePanel/TimePanelColumn.js +19 -12
  104. package/index.d.ts +27 -28
  105. package/index.js +23 -25
  106. package/package.json +4 -4
  107. package/Accordion/AccordionDetails.d.ts +0 -9
  108. package/Accordion/AccordionSummary.d.ts +0 -18
  109. package/Accordion/AccordionSummary.js +0 -51
  110. package/Alert/Alert.d.ts +0 -20
  111. package/Alert/Alert.js +0 -18
  112. package/Alert/index.d.ts +0 -3
  113. package/Alert/index.js +0 -1
  114. package/Card/Card.d.ts +0 -51
  115. package/Card/Card.js +0 -20
  116. package/Card/CardActions.d.ts +0 -34
  117. package/Card/CardActions.js +0 -15
  118. package/ConfirmActions/ConfirmActions.d.ts +0 -46
  119. package/ConfirmActions/ConfirmActions.js +0 -15
  120. package/ConfirmActions/index.d.ts +0 -2
  121. package/ConfirmActions/index.js +0 -1
  122. package/Select/Option.d.ts +0 -18
  123. package/Select/Option.js +0 -45
  124. package/Select/TreeSelect.d.ts +0 -72
  125. package/Select/TreeSelect.js +0 -205
  126. package/Tree/Tree.d.ts +0 -70
  127. package/Tree/Tree.js +0 -139
  128. package/Tree/TreeNode.d.ts +0 -40
  129. package/Tree/TreeNode.js +0 -50
  130. package/Tree/TreeNodeList.d.ts +0 -24
  131. package/Tree/TreeNodeList.js +0 -28
  132. package/Tree/getTreeNodeEntities.d.ts +0 -11
  133. package/Tree/getTreeNodeEntities.js +0 -92
  134. package/Tree/index.d.ts +0 -13
  135. package/Tree/index.js +0 -7
  136. package/Tree/toggleValue.d.ts +0 -4
  137. package/Tree/toggleValue.js +0 -19
  138. package/Tree/traverseTree.d.ts +0 -2
  139. package/Tree/traverseTree.js +0 -11
  140. package/Tree/typings.d.ts +0 -16
  141. package/Tree/useTreeExpandedValue.d.ts +0 -14
  142. package/Tree/useTreeExpandedValue.js +0 -33
@@ -0,0 +1,892 @@
1
+ 'use client';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { forwardRef, useRef, useState, useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
4
+ import { cropperClasses } from '@mezzanine-ui/core/cropper';
5
+ import { MinusIcon, PlusIcon } from '@mezzanine-ui/icons';
6
+ import { useDocumentEvents } from '../hooks/useDocumentEvents.js';
7
+ import Typography from '../Typography/Typography.js';
8
+ import { getCSSVariableValue } from '../utils/get-css-variable-value.js';
9
+ import { isCropAreaSimilar, isImagePositionSimilar, calculateInitialCropArea, getBaseDisplaySize, constrainImagePosition, getBaseScale } from './utils/cropper-calculations.js';
10
+ import Slider from '../Slider/Slider.js';
11
+ import cx from 'clsx';
12
+
13
+ // Constants
14
+ const DEFAULT_MIN_WIDTH = 50;
15
+ const DEFAULT_MIN_HEIGHT = 50;
16
+ const MIN_SCALE = 1;
17
+ const MAX_SCALE = 2;
18
+ const SCALE_STEP = 0.01;
19
+ const BORDER_WIDTH = 2;
20
+ const TAG_INSET_PX = 10;
21
+ const CROP_AREA_SIMILARITY_THRESHOLD = 0.5;
22
+ const IMAGE_POSITION_SIMILARITY_THRESHOLD = 0.1;
23
+ /**
24
+ * The react component for `mezzanine` cropper element (canvas).
25
+ */
26
+ const CropperElement = forwardRef(function CropperElement(props, ref) {
27
+ const { children, className, component: Component = 'canvas', size = 'main', imageSrc, onCropChange, onCropDragEnd, onImageDragEnd, onScaleChange, onImageLoad, onImageError, initialCropArea, aspectRatio, minWidth = DEFAULT_MIN_WIDTH, minHeight = DEFAULT_MIN_HEIGHT, ...rest } = props;
28
+ // Refs
29
+ const canvasRef = useRef(null);
30
+ const elementRef = useRef(null);
31
+ const imageRef = useRef(null);
32
+ const imageLoadIdRef = useRef(0);
33
+ // State - Core
34
+ const [cropArea, setCropArea] = useState(initialCropArea || null);
35
+ const [imageLoaded, setImageLoaded] = useState(false);
36
+ const [initReady, setInitReady] = useState(false);
37
+ const [scale, setScale] = useState(1);
38
+ const [imagePosition, setImagePosition] = useState({
39
+ offsetX: 0,
40
+ offsetY: 0,
41
+ });
42
+ // State - UI
43
+ const [tagPosition, setTagPosition] = useState(null);
44
+ const [tagCropArea, setTagCropArea] = useState(null);
45
+ // State - Interactions
46
+ const [isDragging, setIsDragging] = useState(false);
47
+ const [isDraggingImage, setIsDraggingImage] = useState(false);
48
+ const [dragHandle, setDragHandle] = useState(null);
49
+ const [dragStart, setDragStart] = useState(null);
50
+ const [imageDragStart, setImageDragStart] = useState(null);
51
+ // Refs - Cache for optimization
52
+ const baseDisplaySizeRef = useRef(null);
53
+ const lastCanvasSizeRef = useRef(null);
54
+ const lastMeasuredSizeRef = useRef(null);
55
+ const lastTagPositionRef = useRef(null);
56
+ const lastCropAreaRef = useRef(null);
57
+ const lastImagePositionRef = useRef(null);
58
+ const lastDrawTriggerRef = useRef(null);
59
+ // Refs - Animation & Control
60
+ const rafIdRef = useRef(null);
61
+ const skipDrawRef = useRef(false);
62
+ const resizeRafIdRef = useRef(null);
63
+ const dragRafIdRef = useRef(null);
64
+ const composedRef = useCallback((node) => {
65
+ canvasRef.current = node;
66
+ if (typeof ref === 'function') {
67
+ ref(node);
68
+ }
69
+ else if (ref) {
70
+ ref.current = node;
71
+ }
72
+ }, [ref]);
73
+ // State management with debouncing
74
+ const setCropAreaIfChanged = useCallback((nextCropArea) => {
75
+ if (!isCropAreaSimilar(lastCropAreaRef.current, nextCropArea, CROP_AREA_SIMILARITY_THRESHOLD)) {
76
+ lastCropAreaRef.current = nextCropArea;
77
+ setCropArea(nextCropArea);
78
+ }
79
+ }, []);
80
+ const setImagePositionIfChanged = useCallback((nextImagePosition) => {
81
+ if (!isImagePositionSimilar(lastImagePositionRef.current, nextImagePosition, IMAGE_POSITION_SIMILARITY_THRESHOLD)) {
82
+ lastImagePositionRef.current = nextImagePosition;
83
+ setImagePosition(nextImagePosition);
84
+ }
85
+ }, []);
86
+ // Calculation helpers
87
+ const calculateInitialCropArea$1 = useCallback((img, rect) => {
88
+ return calculateInitialCropArea(img, rect, aspectRatio);
89
+ }, [aspectRatio]);
90
+ const updateTagPosition = useCallback(() => {
91
+ if (!cropArea || !canvasRef.current || !elementRef.current) {
92
+ if (lastTagPositionRef.current) {
93
+ lastTagPositionRef.current = null;
94
+ setTagPosition(null);
95
+ }
96
+ return;
97
+ }
98
+ const canvasRect = canvasRef.current.getBoundingClientRect();
99
+ const elementRect = elementRef.current.getBoundingClientRect();
100
+ const nextPosition = {
101
+ left: canvasRect.left -
102
+ elementRect.left +
103
+ cropArea.x +
104
+ cropArea.width -
105
+ TAG_INSET_PX -
106
+ BORDER_WIDTH,
107
+ top: canvasRect.top -
108
+ elementRect.top +
109
+ cropArea.y +
110
+ cropArea.height -
111
+ TAG_INSET_PX -
112
+ BORDER_WIDTH,
113
+ };
114
+ const prevPosition = lastTagPositionRef.current;
115
+ if (!prevPosition ||
116
+ prevPosition.left !== nextPosition.left ||
117
+ prevPosition.top !== nextPosition.top) {
118
+ lastTagPositionRef.current = nextPosition;
119
+ setTagPosition(nextPosition);
120
+ }
121
+ }, [cropArea]);
122
+ // Load image
123
+ useEffect(() => {
124
+ const loadId = imageLoadIdRef.current + 1;
125
+ imageLoadIdRef.current = loadId;
126
+ setInitReady(false);
127
+ if (!imageSrc) {
128
+ setImageLoaded(false);
129
+ imageRef.current = null;
130
+ return undefined;
131
+ }
132
+ setImageLoaded(false);
133
+ imageRef.current = null;
134
+ let objectUrl = null;
135
+ const img = new Image();
136
+ img.crossOrigin = 'anonymous';
137
+ const loadImage = async () => {
138
+ try {
139
+ if (typeof imageSrc === 'string') {
140
+ img.src = imageSrc;
141
+ }
142
+ else {
143
+ objectUrl = URL.createObjectURL(imageSrc);
144
+ img.src = objectUrl;
145
+ }
146
+ await new Promise((resolve, reject) => {
147
+ img.onload = resolve;
148
+ img.onerror = reject;
149
+ });
150
+ if (imageLoadIdRef.current !== loadId) {
151
+ return;
152
+ }
153
+ imageRef.current = img;
154
+ setImageLoaded(true);
155
+ onImageLoad === null || onImageLoad === void 0 ? void 0 : onImageLoad();
156
+ // Initialize crop area if not provided
157
+ if (!initialCropArea && canvasRef.current) {
158
+ const canvas = canvasRef.current;
159
+ const rect = canvas.getBoundingClientRect();
160
+ const { baseDisplayHeight, baseDisplayWidth, cropArea: nextCropArea, imagePosition: nextImagePosition, } = calculateInitialCropArea$1(img, rect);
161
+ baseDisplaySizeRef.current = {
162
+ height: baseDisplayHeight,
163
+ width: baseDisplayWidth,
164
+ };
165
+ setCropAreaIfChanged(nextCropArea);
166
+ setImagePosition(nextImagePosition);
167
+ }
168
+ }
169
+ catch (error) {
170
+ if (imageLoadIdRef.current !== loadId) {
171
+ return;
172
+ }
173
+ console.error('Failed to load image:', error);
174
+ setImageLoaded(false);
175
+ onImageError === null || onImageError === void 0 ? void 0 : onImageError(error instanceof Error ? error : new Error(String(error)));
176
+ }
177
+ };
178
+ loadImage();
179
+ return () => {
180
+ if (objectUrl) {
181
+ URL.revokeObjectURL(objectUrl);
182
+ }
183
+ };
184
+ }, [
185
+ imageSrc,
186
+ initialCropArea,
187
+ aspectRatio,
188
+ calculateInitialCropArea$1,
189
+ setCropAreaIfChanged,
190
+ onImageLoad,
191
+ onImageError,
192
+ ]);
193
+ useLayoutEffect(() => {
194
+ updateTagPosition();
195
+ }, [updateTagPosition]);
196
+ // Calculate base display size
197
+ const getBaseDisplaySize$1 = useCallback(() => {
198
+ if (!canvasRef.current || !imageRef.current)
199
+ return null;
200
+ const rect = canvasRef.current.getBoundingClientRect();
201
+ return getBaseDisplaySize(rect, imageRef.current);
202
+ }, []);
203
+ // Draw canvas
204
+ const drawCanvas = useCallback(() => {
205
+ const canvas = canvasRef.current;
206
+ const img = imageRef.current;
207
+ const crop = cropArea;
208
+ if (!canvas || !img || !crop)
209
+ return;
210
+ const ctx = canvas.getContext('2d');
211
+ if (!ctx)
212
+ return;
213
+ const rect = canvas.getBoundingClientRect();
214
+ const dpr = window.devicePixelRatio || 1;
215
+ canvas.width = rect.width * dpr;
216
+ canvas.height = rect.height * dpr;
217
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
218
+ // Calculate display size with zoom
219
+ const baseSize = getBaseDisplaySize(rect, img);
220
+ if (!baseSize)
221
+ return;
222
+ const baseDisplayWidth = baseSize.width;
223
+ const baseDisplayHeight = baseSize.height;
224
+ if (!baseDisplaySizeRef.current) {
225
+ baseDisplaySizeRef.current = {
226
+ width: baseDisplayWidth,
227
+ height: baseDisplayHeight,
228
+ };
229
+ }
230
+ const displayWidth = baseDisplayWidth * scale;
231
+ const displayHeight = baseDisplayHeight * scale;
232
+ // Constrain image position
233
+ const constrainedPosition = constrainImagePosition(imagePosition.offsetX, imagePosition.offsetY, displayWidth, displayHeight, crop);
234
+ const finalOffsetX = constrainedPosition.offsetX;
235
+ const finalOffsetY = constrainedPosition.offsetY;
236
+ // Clear canvas
237
+ ctx.clearRect(0, 0, rect.width, rect.height);
238
+ // Draw image
239
+ ctx.drawImage(img, finalOffsetX, finalOffsetY, displayWidth, displayHeight);
240
+ // Draw overlay (darken outside crop area only)
241
+ // Use clip to exclude crop area from overlay
242
+ ctx.save();
243
+ // Create a path that covers the entire canvas except the crop area
244
+ // Using even-odd rule to create a hole
245
+ const path = new Path2D();
246
+ path.rect(0, 0, rect.width, rect.height);
247
+ path.rect(crop.x, crop.y, crop.width, crop.height);
248
+ ctx.clip(path, 'evenodd');
249
+ // Get overlay color from CSS variable with fallback
250
+ const overlayColor = getCSSVariableValue('--mzn-color-overlay-strong') ||
251
+ 'rgba(0, 0, 0, 0.60)';
252
+ ctx.fillStyle = overlayColor;
253
+ ctx.fillRect(0, 0, rect.width, rect.height);
254
+ ctx.restore();
255
+ // Draw crop border
256
+ ctx.strokeStyle =
257
+ getCSSVariableValue('--mzn-color-border-brand') || '#5D74E9';
258
+ ctx.lineWidth = BORDER_WIDTH;
259
+ ctx.strokeRect(crop.x, crop.y, crop.width, crop.height);
260
+ }, [cropArea, scale, imagePosition]);
261
+ const scheduleDraw = useCallback(() => {
262
+ if (rafIdRef.current !== null)
263
+ return;
264
+ rafIdRef.current = window.requestAnimationFrame(() => {
265
+ rafIdRef.current = null;
266
+ drawCanvas();
267
+ });
268
+ }, [drawCanvas]);
269
+ useEffect(() => {
270
+ return () => {
271
+ if (rafIdRef.current !== null) {
272
+ window.cancelAnimationFrame(rafIdRef.current);
273
+ rafIdRef.current = null;
274
+ }
275
+ };
276
+ }, []);
277
+ useEffect(() => {
278
+ if (!elementRef.current || !canvasRef.current)
279
+ return undefined;
280
+ const resizeObserver = new ResizeObserver(() => {
281
+ if (resizeRafIdRef.current !== null)
282
+ return;
283
+ resizeRafIdRef.current = window.requestAnimationFrame(() => {
284
+ var _a;
285
+ resizeRafIdRef.current = null;
286
+ if (imageLoaded) {
287
+ const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
288
+ if (!rect || !rect.height)
289
+ return;
290
+ const isSizeChanged = !lastMeasuredSizeRef.current ||
291
+ rect.height !== lastMeasuredSizeRef.current.height;
292
+ let shouldSkipDraw = false;
293
+ if (isSizeChanged) {
294
+ lastMeasuredSizeRef.current = {
295
+ height: rect.height,
296
+ width: rect.width,
297
+ };
298
+ lastCanvasSizeRef.current = {
299
+ height: rect.height,
300
+ width: rect.width,
301
+ };
302
+ setInitReady(false);
303
+ const baseSize = getBaseDisplaySize$1();
304
+ if (baseSize) {
305
+ baseDisplaySizeRef.current = baseSize;
306
+ }
307
+ if (!initialCropArea && canvasRef.current && imageRef.current) {
308
+ shouldSkipDraw = true;
309
+ skipDrawRef.current = true;
310
+ const { cropArea: nextCropArea, imagePosition: nextImagePosition, } = calculateInitialCropArea$1(imageRef.current, rect);
311
+ setCropAreaIfChanged(nextCropArea);
312
+ setImagePosition(nextImagePosition);
313
+ }
314
+ }
315
+ if (!shouldSkipDraw) {
316
+ scheduleDraw();
317
+ }
318
+ }
319
+ });
320
+ });
321
+ resizeObserver.observe(elementRef.current);
322
+ resizeObserver.observe(canvasRef.current);
323
+ window.addEventListener('resize', updateTagPosition);
324
+ return () => {
325
+ resizeObserver.disconnect();
326
+ window.removeEventListener('resize', updateTagPosition);
327
+ if (resizeRafIdRef.current !== null) {
328
+ window.cancelAnimationFrame(resizeRafIdRef.current);
329
+ resizeRafIdRef.current = null;
330
+ }
331
+ };
332
+ }, [
333
+ calculateInitialCropArea$1,
334
+ getBaseDisplaySize$1,
335
+ imageLoaded,
336
+ initialCropArea,
337
+ scheduleDraw,
338
+ updateTagPosition,
339
+ setCropAreaIfChanged,
340
+ ]);
341
+ useEffect(() => {
342
+ if (imageLoaded) {
343
+ const baseSize = getBaseDisplaySize$1();
344
+ if (baseSize) {
345
+ baseDisplaySizeRef.current = baseSize;
346
+ }
347
+ }
348
+ }, [imageLoaded, getBaseDisplaySize$1]);
349
+ useEffect(() => {
350
+ if (!cropArea || !imageLoaded || !canvasRef.current || !imageRef.current)
351
+ return;
352
+ const rect = canvasRef.current.getBoundingClientRect();
353
+ const baseSize = getBaseDisplaySize(rect, imageRef.current);
354
+ if (!baseSize)
355
+ return;
356
+ const displayWidth = baseSize.width * scale;
357
+ const displayHeight = baseSize.height * scale;
358
+ const constrainedPosition = constrainImagePosition(imagePosition.offsetX, imagePosition.offsetY, displayWidth, displayHeight, cropArea);
359
+ if (constrainedPosition.offsetX !== imagePosition.offsetX ||
360
+ constrainedPosition.offsetY !== imagePosition.offsetY) {
361
+ skipDrawRef.current = true;
362
+ setImagePositionIfChanged(constrainedPosition);
363
+ }
364
+ else {
365
+ skipDrawRef.current = false;
366
+ }
367
+ }, [
368
+ cropArea,
369
+ imageLoaded,
370
+ imagePosition,
371
+ scale,
372
+ setImagePositionIfChanged,
373
+ ]);
374
+ useEffect(() => {
375
+ if (cropArea) {
376
+ lastCropAreaRef.current = cropArea;
377
+ }
378
+ }, [cropArea]);
379
+ useEffect(() => {
380
+ lastImagePositionRef.current = imagePosition;
381
+ }, [imagePosition]);
382
+ useEffect(() => {
383
+ if (!imageLoaded || !cropArea)
384
+ return;
385
+ if (!lastCanvasSizeRef.current)
386
+ return;
387
+ setInitReady(true);
388
+ }, [cropArea, imageLoaded]);
389
+ useEffect(() => {
390
+ if (!imageLoaded || !initReady)
391
+ return;
392
+ if (skipDrawRef.current) {
393
+ skipDrawRef.current = false;
394
+ return;
395
+ }
396
+ const lastTrigger = lastDrawTriggerRef.current;
397
+ const hasChanged = !lastTrigger ||
398
+ !cropArea ||
399
+ !isCropAreaSimilar(lastTrigger.cropArea, cropArea, CROP_AREA_SIMILARITY_THRESHOLD) ||
400
+ !isImagePositionSimilar(lastTrigger.imagePosition, imagePosition, IMAGE_POSITION_SIMILARITY_THRESHOLD) ||
401
+ lastTrigger.scale !== scale;
402
+ if (!hasChanged) {
403
+ return;
404
+ }
405
+ lastDrawTriggerRef.current = {
406
+ cropArea,
407
+ imagePosition,
408
+ scale,
409
+ };
410
+ // Skip tag position update during drag for better performance
411
+ if (!isDraggingImage && !isDragging) {
412
+ updateTagPosition();
413
+ }
414
+ scheduleDraw();
415
+ }, [
416
+ cropArea,
417
+ imageLoaded,
418
+ initReady,
419
+ imagePosition,
420
+ scale,
421
+ updateTagPosition,
422
+ scheduleDraw,
423
+ isDraggingImage,
424
+ isDragging,
425
+ ]);
426
+ const emitCropChange = useCallback((nextCropArea) => {
427
+ if (!onCropChange)
428
+ return;
429
+ if (!canvasRef.current || !imageRef.current) {
430
+ onCropChange(nextCropArea);
431
+ setTagCropArea(nextCropArea);
432
+ return;
433
+ }
434
+ const rect = canvasRef.current.getBoundingClientRect();
435
+ const img = imageRef.current;
436
+ const baseScale = getBaseScale(rect, img);
437
+ const imageScale = baseScale / scale;
438
+ const rawWidth = nextCropArea.width * imageScale;
439
+ const rawHeight = nextCropArea.height * imageScale;
440
+ const rawX = (nextCropArea.x - imagePosition.offsetX) * imageScale;
441
+ const rawY = (nextCropArea.y - imagePosition.offsetY) * imageScale;
442
+ const clampedWidth = Math.min(img.width, Math.max(0, rawWidth));
443
+ const clampedHeight = Math.min(img.height, Math.max(0, rawHeight));
444
+ const clampedX = Math.min(img.width - clampedWidth, Math.max(0, rawX));
445
+ const clampedY = Math.min(img.height - clampedHeight, Math.max(0, rawY));
446
+ const emittedCropArea = {
447
+ height: clampedHeight,
448
+ width: clampedWidth,
449
+ x: clampedX,
450
+ y: clampedY,
451
+ };
452
+ onCropChange(emittedCropArea);
453
+ setTagCropArea(emittedCropArea);
454
+ }, [imagePosition.offsetX, imagePosition.offsetY, onCropChange, scale]);
455
+ // Update crop area based on drag
456
+ const updateCropArea = useCallback((handle, deltaX, deltaY) => {
457
+ if (!cropArea)
458
+ return;
459
+ let x = cropArea.x;
460
+ let y = cropArea.y;
461
+ let width = cropArea.width;
462
+ let height = cropArea.height;
463
+ switch (handle.type) {
464
+ case 'move':
465
+ x = Math.max(0, cropArea.x + deltaX);
466
+ y = Math.max(0, cropArea.y + deltaY);
467
+ if (canvasRef.current) {
468
+ const rect = canvasRef.current.getBoundingClientRect();
469
+ x = Math.min(x, rect.width - width);
470
+ y = Math.min(y, rect.height - height);
471
+ }
472
+ break;
473
+ case 'nw':
474
+ width = cropArea.width - deltaX;
475
+ height = cropArea.height - deltaY;
476
+ x = cropArea.x + deltaX;
477
+ y = cropArea.y + deltaY;
478
+ break;
479
+ case 'ne':
480
+ width = cropArea.width + deltaX;
481
+ height = cropArea.height - deltaY;
482
+ y = cropArea.y + deltaY;
483
+ break;
484
+ case 'sw':
485
+ width = cropArea.width - deltaX;
486
+ height = cropArea.height + deltaY;
487
+ x = cropArea.x + deltaX;
488
+ break;
489
+ case 'se':
490
+ width = cropArea.width + deltaX;
491
+ height = cropArea.height + deltaY;
492
+ break;
493
+ case 'n':
494
+ height = cropArea.height - deltaY;
495
+ y = cropArea.y + deltaY;
496
+ break;
497
+ case 's':
498
+ height = cropArea.height + deltaY;
499
+ break;
500
+ case 'w':
501
+ width = cropArea.width - deltaX;
502
+ x = cropArea.x + deltaX;
503
+ break;
504
+ case 'e':
505
+ width = cropArea.width + deltaX;
506
+ break;
507
+ }
508
+ const anchor = {
509
+ bottom: y + height,
510
+ centerX: x + width / 2,
511
+ centerY: y + height / 2,
512
+ left: x,
513
+ right: x + width,
514
+ top: y,
515
+ };
516
+ // Apply aspect ratio if provided
517
+ if (aspectRatio && handle.type !== 'move') {
518
+ const currentAspect = width / height;
519
+ if (Math.abs(currentAspect - aspectRatio) > 0.01) {
520
+ if (handle.type.includes('w') || handle.type.includes('e')) {
521
+ height = width / aspectRatio;
522
+ }
523
+ else {
524
+ width = height * aspectRatio;
525
+ }
526
+ }
527
+ switch (handle.type) {
528
+ case 'nw':
529
+ x = anchor.right - width;
530
+ y = anchor.bottom - height;
531
+ break;
532
+ case 'ne':
533
+ x = anchor.left;
534
+ y = anchor.bottom - height;
535
+ break;
536
+ case 'sw':
537
+ x = anchor.right - width;
538
+ y = anchor.top;
539
+ break;
540
+ case 'se':
541
+ x = anchor.left;
542
+ y = anchor.top;
543
+ break;
544
+ case 'n':
545
+ x = anchor.centerX - width / 2;
546
+ y = anchor.bottom - height;
547
+ break;
548
+ case 's':
549
+ x = anchor.centerX - width / 2;
550
+ y = anchor.top;
551
+ break;
552
+ case 'w':
553
+ x = anchor.right - width;
554
+ y = anchor.centerY - height / 2;
555
+ break;
556
+ case 'e':
557
+ x = anchor.left;
558
+ y = anchor.centerY - height / 2;
559
+ break;
560
+ }
561
+ }
562
+ // Apply constraints - allow crop area to extend to image bounds
563
+ if (canvasRef.current && imageRef.current) {
564
+ const rect = canvasRef.current.getBoundingClientRect();
565
+ const baseSize = getBaseDisplaySize(rect, imageRef.current);
566
+ if (!baseSize) {
567
+ const newCrop = { x, y, width, height };
568
+ setCropArea(newCrop);
569
+ if (onCropChange) {
570
+ emitCropChange(newCrop);
571
+ }
572
+ return;
573
+ }
574
+ const baseDisplayWidth = baseSize.width;
575
+ const baseDisplayHeight = baseSize.height;
576
+ const displayWidth = baseDisplayWidth * scale;
577
+ const displayHeight = baseDisplayHeight * scale;
578
+ const imageOffsetX = imagePosition.offsetX;
579
+ const imageOffsetY = imagePosition.offsetY;
580
+ // Image display bounds
581
+ const imageLeft = imageOffsetX;
582
+ const imageRight = imageOffsetX + displayWidth;
583
+ const imageTop = imageOffsetY;
584
+ const imageBottom = imageOffsetY + displayHeight;
585
+ if (aspectRatio) {
586
+ // For aspect ratio, calculate maximum size within image bounds
587
+ // First, calculate max dimensions based on image bounds
588
+ const maxWidthFromImage = Math.min(imageRight - Math.max(x, imageLeft), Math.max(x + width, imageRight) - imageLeft);
589
+ const maxHeightFromImage = Math.min(imageBottom - Math.max(y, imageTop), Math.max(y + height, imageBottom) - imageTop);
590
+ // Calculate which dimension limits the size
591
+ const maxWidthByAspect = maxHeightFromImage * aspectRatio;
592
+ const maxHeightByAspect = maxWidthFromImage / aspectRatio;
593
+ if (maxWidthByAspect <= maxWidthFromImage) {
594
+ // Height is the limiting factor
595
+ width = Math.max(minWidth, Math.min(width, maxWidthByAspect));
596
+ height = width / aspectRatio;
597
+ }
598
+ else {
599
+ // Width is the limiting factor
600
+ height = Math.max(minHeight, Math.min(height, maxHeightByAspect));
601
+ width = height * aspectRatio;
602
+ }
603
+ // Ensure crop area stays within image bounds
604
+ if (x < imageLeft) {
605
+ x = imageLeft;
606
+ }
607
+ else if (x + width > imageRight) {
608
+ x = imageRight - width;
609
+ }
610
+ if (y < imageTop) {
611
+ y = imageTop;
612
+ }
613
+ else if (y + height > imageBottom) {
614
+ y = imageBottom - height;
615
+ }
616
+ // Also ensure crop area doesn't go outside canvas bounds
617
+ x = Math.max(0, Math.min(x, rect.width - width));
618
+ y = Math.max(0, Math.min(y, rect.height - height));
619
+ }
620
+ else {
621
+ // No aspect ratio - constrain to both image and canvas bounds
622
+ const maxWidth = Math.min(rect.width - x, imageRight - Math.max(x, imageLeft));
623
+ const maxHeight = Math.min(rect.height - y, imageBottom - Math.max(y, imageTop));
624
+ width = Math.max(minWidth, Math.min(width, maxWidth));
625
+ height = Math.max(minHeight, Math.min(height, maxHeight));
626
+ // Ensure crop area is within image bounds
627
+ if (x < imageLeft) {
628
+ x = imageLeft;
629
+ }
630
+ else if (x + width > imageRight) {
631
+ x = imageRight - width;
632
+ }
633
+ if (y < imageTop) {
634
+ y = imageTop;
635
+ }
636
+ else if (y + height > imageBottom) {
637
+ y = imageBottom - height;
638
+ }
639
+ // Also ensure crop area doesn't go outside canvas bounds
640
+ x = Math.max(0, Math.min(x, rect.width - width));
641
+ y = Math.max(0, Math.min(y, rect.height - height));
642
+ }
643
+ }
644
+ else if (canvasRef.current) {
645
+ // Fallback if image not loaded
646
+ const rect = canvasRef.current.getBoundingClientRect();
647
+ width = Math.max(minWidth, Math.min(width, rect.width - x));
648
+ height = Math.max(minHeight, Math.min(height, rect.height - y));
649
+ x = Math.max(0, Math.min(x, rect.width - width));
650
+ y = Math.max(0, Math.min(y, rect.height - height));
651
+ }
652
+ const newCrop = { x, y, width, height };
653
+ setCropArea(newCrop);
654
+ if (onCropChange) {
655
+ emitCropChange(newCrop);
656
+ }
657
+ }, [
658
+ cropArea,
659
+ aspectRatio,
660
+ minWidth,
661
+ minHeight,
662
+ imagePosition.offsetX,
663
+ imagePosition.offsetY,
664
+ scale,
665
+ emitCropChange,
666
+ onCropChange,
667
+ ]);
668
+ useEffect(() => {
669
+ if (!cropArea || !imageLoaded)
670
+ return;
671
+ // Skip emitting during drag to avoid performance issues
672
+ if (isDraggingImage || isDragging)
673
+ return;
674
+ emitCropChange(cropArea);
675
+ }, [cropArea, emitCropChange, imageLoaded, isDraggingImage, isDragging]);
676
+ // Check if point is on image
677
+ const isPointOnImage = useCallback((x, y) => {
678
+ if (!cropArea || !canvasRef.current || !imageRef.current)
679
+ return false;
680
+ const rect = canvasRef.current.getBoundingClientRect();
681
+ const baseSize = getBaseDisplaySize(rect, imageRef.current);
682
+ if (!baseSize)
683
+ return false;
684
+ const displayWidth = baseSize.width * scale;
685
+ const displayHeight = baseSize.height * scale;
686
+ return (x >= imagePosition.offsetX &&
687
+ x <= imagePosition.offsetX + displayWidth &&
688
+ y >= imagePosition.offsetY &&
689
+ y <= imagePosition.offsetY + displayHeight);
690
+ }, [cropArea, scale, imagePosition]);
691
+ // Mouse down handler
692
+ const handleMouseDown = useCallback((e) => {
693
+ if (!cropArea || !canvasRef.current)
694
+ return;
695
+ const rect = canvasRef.current.getBoundingClientRect();
696
+ const x = e.clientX - rect.left;
697
+ const y = e.clientY - rect.top;
698
+ // Check if clicking on image (for dragging image)
699
+ if (isPointOnImage(x, y)) {
700
+ setIsDraggingImage(true);
701
+ setImageDragStart({
702
+ x: e.clientX,
703
+ y: e.clientY,
704
+ offsetX: imagePosition.offsetX,
705
+ offsetY: imagePosition.offsetY,
706
+ });
707
+ return;
708
+ }
709
+ // Crop area is fixed; no resize or move handlers.
710
+ }, [cropArea, isPointOnImage, imagePosition]);
711
+ // Mouse move handler for crop area
712
+ const handleMouseMove = useCallback((e) => {
713
+ if (!isDragging || !dragHandle || !dragStart || !cropArea)
714
+ return;
715
+ const deltaX = e.clientX - dragStart.x;
716
+ const deltaY = e.clientY - dragStart.y;
717
+ updateCropArea(dragHandle, deltaX, deltaY);
718
+ setDragStart({ x: e.clientX, y: e.clientY });
719
+ }, [isDragging, dragHandle, dragStart, cropArea, updateCropArea]);
720
+ // Mouse move handler for image dragging
721
+ const handleImageMouseMove = useCallback((e) => {
722
+ if (!isDraggingImage ||
723
+ !imageDragStart ||
724
+ !canvasRef.current ||
725
+ !imageRef.current ||
726
+ !cropArea)
727
+ return;
728
+ // Cancel previous RAF if exists
729
+ if (dragRafIdRef.current !== null) {
730
+ window.cancelAnimationFrame(dragRafIdRef.current);
731
+ }
732
+ dragRafIdRef.current = window.requestAnimationFrame(() => {
733
+ var _a;
734
+ dragRafIdRef.current = null;
735
+ const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
736
+ const img = imageRef.current;
737
+ if (!rect || !img)
738
+ return;
739
+ const deltaX = e.clientX - imageDragStart.x;
740
+ const deltaY = e.clientY - imageDragStart.y;
741
+ const newOffsetX = imageDragStart.offsetX + deltaX;
742
+ const newOffsetY = imageDragStart.offsetY + deltaY;
743
+ // Calculate display size
744
+ const baseSize = getBaseDisplaySize(rect, img);
745
+ if (!baseSize)
746
+ return;
747
+ const displayWidth = baseSize.width * scale;
748
+ const displayHeight = baseSize.height * scale;
749
+ // Constrain position
750
+ const constrained = constrainImagePosition(newOffsetX, newOffsetY, displayWidth, displayHeight, cropArea);
751
+ // Check if position actually changed (avoid updates when at boundary)
752
+ // Use ref to get latest position value to avoid closure issues
753
+ const currentPosition = lastImagePositionRef.current || imagePosition;
754
+ if (!isImagePositionSimilar(currentPosition, constrained, IMAGE_POSITION_SIMILARITY_THRESHOLD)) {
755
+ setImagePosition(constrained);
756
+ scheduleDraw();
757
+ }
758
+ });
759
+ }, [
760
+ isDraggingImage,
761
+ imageDragStart,
762
+ scale,
763
+ cropArea,
764
+ scheduleDraw,
765
+ imagePosition,
766
+ ]);
767
+ // Mouse up handler
768
+ const handleMouseUp = useCallback(() => {
769
+ // Cancel any pending drag RAF
770
+ if (dragRafIdRef.current !== null) {
771
+ window.cancelAnimationFrame(dragRafIdRef.current);
772
+ dragRafIdRef.current = null;
773
+ }
774
+ const wasDraggingCrop = isDragging;
775
+ const wasDraggingImage = isDraggingImage;
776
+ setIsDragging(false);
777
+ setDragHandle(null);
778
+ setDragStart(null);
779
+ setIsDraggingImage(false);
780
+ setImageDragStart(null);
781
+ // Update tag position and emit crop change after drag ends
782
+ if (wasDraggingCrop || wasDraggingImage) {
783
+ updateTagPosition();
784
+ if (cropArea && imageLoaded) {
785
+ emitCropChange(cropArea);
786
+ }
787
+ }
788
+ // Call drag end callbacks
789
+ if (wasDraggingCrop && cropArea && onCropDragEnd) {
790
+ onCropDragEnd(cropArea);
791
+ }
792
+ if (wasDraggingImage && onImageDragEnd) {
793
+ onImageDragEnd();
794
+ }
795
+ }, [
796
+ isDragging,
797
+ isDraggingImage,
798
+ cropArea,
799
+ imageLoaded,
800
+ emitCropChange,
801
+ updateTagPosition,
802
+ onCropDragEnd,
803
+ onImageDragEnd,
804
+ ]);
805
+ // Document events for dragging
806
+ useDocumentEvents(() => {
807
+ if (isDragging) {
808
+ return {
809
+ mousemove: handleMouseMove,
810
+ mouseup: handleMouseUp,
811
+ };
812
+ }
813
+ if (isDraggingImage) {
814
+ return {
815
+ mousemove: handleImageMouseMove,
816
+ mouseup: handleMouseUp,
817
+ };
818
+ }
819
+ return undefined;
820
+ }, [isDragging, isDraggingImage, handleMouseMove, handleImageMouseMove, handleMouseUp]);
821
+ // Handle scale change with center point preservation
822
+ const handleScaleChange = useCallback((newScale) => {
823
+ if (!canvasRef.current || !imageRef.current || !cropArea) {
824
+ setScale(newScale);
825
+ onScaleChange === null || onScaleChange === void 0 ? void 0 : onScaleChange(newScale);
826
+ return;
827
+ }
828
+ const rect = canvasRef.current.getBoundingClientRect();
829
+ const baseSize = getBaseDisplaySize(rect, imageRef.current);
830
+ if (!baseSize) {
831
+ setScale(newScale);
832
+ onScaleChange === null || onScaleChange === void 0 ? void 0 : onScaleChange(newScale);
833
+ return;
834
+ }
835
+ // Calculate center point of crop area in canvas coordinates
836
+ const centerX = cropArea.x + cropArea.width / 2;
837
+ const centerY = cropArea.y + cropArea.height / 2;
838
+ // Calculate current image position relative to center
839
+ const currentDisplayWidth = baseSize.width * scale;
840
+ const currentDisplayHeight = baseSize.height * scale;
841
+ const currentCenterOffsetX = centerX - (imagePosition.offsetX + currentDisplayWidth / 2);
842
+ const currentCenterOffsetY = centerY - (imagePosition.offsetY + currentDisplayHeight / 2);
843
+ // Calculate new display size
844
+ const newDisplayWidth = baseSize.width * newScale;
845
+ const newDisplayHeight = baseSize.height * newScale;
846
+ // Calculate new position to keep center point
847
+ const newOffsetX = centerX - newDisplayWidth / 2 - currentCenterOffsetX;
848
+ const newOffsetY = centerY - newDisplayHeight / 2 - currentCenterOffsetY;
849
+ // Constrain new position
850
+ const constrained = constrainImagePosition(newOffsetX, newOffsetY, newDisplayWidth, newDisplayHeight, cropArea);
851
+ setScale(newScale);
852
+ setImagePosition(constrained);
853
+ onScaleChange === null || onScaleChange === void 0 ? void 0 : onScaleChange(newScale);
854
+ }, [cropArea, imagePosition, scale, onScaleChange]);
855
+ // Handle slider change
856
+ const handleSliderChange = useCallback((value) => {
857
+ handleScaleChange(value);
858
+ }, [handleScaleChange]);
859
+ // Handle wheel (trackpad) zoom
860
+ const handleWheel = useCallback((e) => {
861
+ if (!cropArea)
862
+ return;
863
+ e.preventDefault();
864
+ const delta = e.deltaY > 0 ? -0.05 : 0.05;
865
+ const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale + delta));
866
+ handleScaleChange(newScale);
867
+ }, [scale, cropArea, handleScaleChange]);
868
+ // Add wheel event listener with passive: false to allow preventDefault
869
+ useEffect(() => {
870
+ const canvas = canvasRef.current;
871
+ if (!canvas)
872
+ return;
873
+ canvas.addEventListener('wheel', handleWheel, { passive: false });
874
+ return () => {
875
+ canvas.removeEventListener('wheel', handleWheel);
876
+ };
877
+ }, [handleWheel]);
878
+ // Update cursor style
879
+ const cursorStyle = useMemo(() => {
880
+ if (!cropArea)
881
+ return 'default';
882
+ if (isDragging || isDraggingImage)
883
+ return 'grabbing';
884
+ return 'default';
885
+ }, [cropArea, isDragging, isDraggingImage]);
886
+ return (jsxs("div", { className: cropperClasses.element, ref: elementRef, children: [jsx(Component, { ...rest, className: cx(cropperClasses.host, cropperClasses.size(size), className), onMouseDown: handleMouseDown, ref: composedRef, style: { cursor: cursorStyle, width: '100%', ...rest.style }, children: children }), cropArea && tagPosition && (jsxs(Typography, { className: cropperClasses.tag, color: "text-fixed-light", style: {
887
+ left: tagPosition.left,
888
+ top: tagPosition.top,
889
+ }, children: [Math.round((tagCropArea || cropArea).width), " \u00D7", ' ', Math.round((tagCropArea || cropArea).height), " px"] })), jsx("div", { className: cropperClasses.controls, children: jsx(Slider, { min: MIN_SCALE, max: MAX_SCALE, step: SCALE_STEP, value: scale, onChange: handleSliderChange, suffixIcon: PlusIcon, prefixIcon: MinusIcon }) })] }));
890
+ });
891
+
892
+ export { CropperElement as default };