@ikatec/nebula-react 1.0.26 → 1.0.27

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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React8 from 'react';
2
- import React8__default, { createContext, forwardRef, useMemo, useState, useEffect, useCallback, useContext, useRef } from 'react';
2
+ import React8__default, { createContext, forwardRef, useMemo, useState, useEffect, useCallback, useContext, useRef, useId } from 'react';
3
3
  import { Slot } from '@radix-ui/react-slot';
4
4
  import { cva } from 'class-variance-authority';
5
5
  import { extendTailwindMerge } from 'tailwind-merge';
@@ -7,7 +7,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
  import * as PopoverPrimitive from '@radix-ui/react-popover';
8
8
  import * as LabelPrimitive from '@radix-ui/react-label';
9
9
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
10
- import { ChevronRight, Check, Circle, CircleX, Eye, EyeOff, X, Minus, ClockIcon, ChevronsLeft, ChevronLeft, ChevronsRight, MoreHorizontal, ChevronDown, ChevronLeftIcon, ChevronDownIcon, ChevronRightIcon, CalendarIcon, ImageUpIcon, XIcon, UserIcon, PhoneIcon, FileTextIcon, FileAudioIcon, FileVideoIcon, Info, CircleCheckBig } from 'lucide-react';
10
+ import { ChevronRight, Check, Circle, CircleX, Eye, EyeOff, X, Minus, ClockIcon, ChevronsLeft, ChevronLeft, ChevronsRight, MoreHorizontal, ChevronDown, ChevronLeftIcon, ChevronDownIcon, ChevronRightIcon, CalendarIcon, ImageUpIcon, XIcon, UserIcon, MinusIcon, PlusIcon, PhoneIcon, FileTextIcon, FileAudioIcon, FileVideoIcon, Info, CircleCheckBig } from 'lucide-react';
11
11
  import * as SeparatorPrimitive from '@radix-ui/react-separator';
12
12
  import Select, { components } from 'react-select';
13
13
  import Creatable from 'react-select/creatable';
@@ -27,6 +27,7 @@ import { formatDate, isValid, addMonths, isSameDay, isBefore, isAfter } from 'da
27
27
  import { DayPicker } from 'react-day-picker';
28
28
  import { ptBR, enUS, es } from 'react-day-picker/locale';
29
29
  import { useMask } from '@react-input/mask';
30
+ import * as SliderPrimitive from '@radix-ui/react-slider';
30
31
 
31
32
  // src/button.tsx
32
33
 
@@ -1117,8 +1118,16 @@ var messages5 = {
1117
1118
  };
1118
1119
  var file_upload_default = messages5;
1119
1120
 
1121
+ // src/i18n/messages/en/cropper.ts
1122
+ var messages6 = {
1123
+ applyButtonLabel: "Apply",
1124
+ dialogTitle: "Adjust image"
1125
+ };
1126
+ var cropper_default = messages6;
1127
+
1120
1128
  // src/i18n/messages/en/index.ts
1121
1129
  var enMessages = {
1130
+ cropper: cropper_default,
1122
1131
  pagination: pagination_default,
1123
1132
  inputSelect: input_select_default,
1124
1133
  inputPhone: input_phone_default,
@@ -1127,7 +1136,7 @@ var enMessages = {
1127
1136
  };
1128
1137
 
1129
1138
  // src/i18n/messages/es/pagination.ts
1130
- var messages6 = {
1139
+ var messages7 = {
1131
1140
  totalResultsLabel(pagesSize, totalResults) {
1132
1141
  if (totalResults < pagesSize) {
1133
1142
  pagesSize = totalResults;
@@ -1138,16 +1147,16 @@ var messages6 = {
1138
1147
  return `P\xE1gina ${currentPage} de ${totalPages}`;
1139
1148
  }
1140
1149
  };
1141
- var pagination_default2 = messages6;
1150
+ var pagination_default2 = messages7;
1142
1151
 
1143
1152
  // src/i18n/messages/es/input-select.ts
1144
- var messages7 = {
1153
+ var messages8 = {
1145
1154
  noOptions: "No hay opciones disponibles"
1146
1155
  };
1147
- var input_select_default2 = messages7;
1156
+ var input_select_default2 = messages8;
1148
1157
 
1149
1158
  // src/i18n/messages/es/input-phone.ts
1150
- var messages8 = {
1159
+ var messages9 = {
1151
1160
  countries: {
1152
1161
  empty: "Seleccionar",
1153
1162
  AF: "Afganist\xE1n",
@@ -1397,22 +1406,30 @@ var messages8 = {
1397
1406
  ZW: "Zimbabue"
1398
1407
  }
1399
1408
  };
1400
- var input_phone_default2 = messages8;
1409
+ var input_phone_default2 = messages9;
1401
1410
 
1402
1411
  // src/i18n/messages/es/time-picker.ts
1403
- var messages9 = {
1412
+ var messages10 = {
1404
1413
  label: "Tiempo"
1405
1414
  };
1406
- var time_picker_default2 = messages9;
1415
+ var time_picker_default2 = messages10;
1407
1416
 
1408
1417
  // src/i18n/messages/es/file-upload.ts
1409
- var messages10 = {
1418
+ var messages11 = {
1410
1419
  deleteAll: "Remover todos"
1411
1420
  };
1412
- var file_upload_default2 = messages10;
1421
+ var file_upload_default2 = messages11;
1422
+
1423
+ // src/i18n/messages/es/cropper.ts
1424
+ var messages12 = {
1425
+ applyButtonLabel: "Aplicar",
1426
+ dialogTitle: "Ajustar la imagen"
1427
+ };
1428
+ var cropper_default2 = messages12;
1413
1429
 
1414
1430
  // src/i18n/messages/es/index.ts
1415
1431
  var esMessages = {
1432
+ cropper: cropper_default2,
1416
1433
  pagination: pagination_default2,
1417
1434
  inputSelect: input_select_default2,
1418
1435
  inputPhone: input_phone_default2,
@@ -1421,7 +1438,7 @@ var esMessages = {
1421
1438
  };
1422
1439
 
1423
1440
  // src/i18n/messages/pt-br/pagination.ts
1424
- var messages11 = {
1441
+ var messages13 = {
1425
1442
  totalResultsLabel(pagesSize, totalResults) {
1426
1443
  if (totalResults < pagesSize) {
1427
1444
  pagesSize = totalResults;
@@ -1432,16 +1449,16 @@ var messages11 = {
1432
1449
  return `P\xE1gina ${currentPage} de ${totalPages}`;
1433
1450
  }
1434
1451
  };
1435
- var pagination_default3 = messages11;
1452
+ var pagination_default3 = messages13;
1436
1453
 
1437
1454
  // src/i18n/messages/pt-br/input-select.ts
1438
- var messages12 = {
1455
+ var messages14 = {
1439
1456
  noOptions: "Nenhuma op\xE7\xE3o dispon\xEDvel"
1440
1457
  };
1441
- var input_select_default3 = messages12;
1458
+ var input_select_default3 = messages14;
1442
1459
 
1443
1460
  // src/i18n/messages/pt-br/input-phone.ts
1444
- var messages13 = {
1461
+ var messages15 = {
1445
1462
  countries: {
1446
1463
  empty: "Selecione",
1447
1464
  AF: "Afeganist\xE3o",
@@ -1691,19 +1708,26 @@ var messages13 = {
1691
1708
  ZW: "Zimb\xE1bue"
1692
1709
  }
1693
1710
  };
1694
- var input_phone_default3 = messages13;
1711
+ var input_phone_default3 = messages15;
1695
1712
 
1696
1713
  // src/i18n/messages/pt-br/time-picker.ts
1697
- var messages14 = {
1714
+ var messages16 = {
1698
1715
  label: "Hor\xE1rio"
1699
1716
  };
1700
- var time_picker_default3 = messages14;
1717
+ var time_picker_default3 = messages16;
1701
1718
 
1702
1719
  // src/i18n/messages/pt-br/file-upload.ts
1703
- var messages15 = {
1720
+ var messages17 = {
1704
1721
  deleteAll: "Remover todos"
1705
1722
  };
1706
- var file_upload_default3 = messages15;
1723
+ var file_upload_default3 = messages17;
1724
+
1725
+ // src/i18n/messages/pt-br/cropper.ts
1726
+ var messages18 = {
1727
+ applyButtonLabel: "Aplicar",
1728
+ dialogTitle: "Ajustar imagem"
1729
+ };
1730
+ var cropper_default3 = messages18;
1707
1731
 
1708
1732
  // src/i18n/messages/pt-br/index.ts
1709
1733
  var ptBrMessages = {
@@ -1711,7 +1735,8 @@ var ptBrMessages = {
1711
1735
  inputSelect: input_select_default3,
1712
1736
  inputPhone: input_phone_default3,
1713
1737
  timePicker: time_picker_default3,
1714
- fileUpload: file_upload_default3
1738
+ fileUpload: file_upload_default3,
1739
+ cropper: cropper_default3
1715
1740
  };
1716
1741
 
1717
1742
  // src/i18n/message-storage-handler.ts
@@ -1736,7 +1761,7 @@ var setNebulaLanguage = (language) => {
1736
1761
  }
1737
1762
  localStorage.setItem(getNebulaI18nStorageKey(), language);
1738
1763
  };
1739
- var messages16 = /* @__PURE__ */ new Map([
1764
+ var messages19 = /* @__PURE__ */ new Map([
1740
1765
  [null, enMessages],
1741
1766
  [void 0, enMessages],
1742
1767
  ["en-US", enMessages],
@@ -1754,14 +1779,14 @@ var NebulaI18nProvider = ({
1754
1779
  () => customI18nStorageKey ?? localStorageKey,
1755
1780
  [customI18nStorageKey]
1756
1781
  );
1757
- const [messages17, setMessages] = useState(
1758
- messages16.get(getNebulaLanguage()) ?? messages16.get("en-US")
1782
+ const [messages20, setMessages] = useState(
1783
+ messages19.get(getNebulaLanguage()) ?? messages19.get("en-US")
1759
1784
  );
1760
1785
  const handleStorageChange = useCallback(
1761
1786
  ({ detail }) => {
1762
1787
  if (detail.key === storageKey) {
1763
1788
  setMessages(
1764
- messages16.get(detail.value) ?? messages16.get("en-US")
1789
+ messages19.get(detail.value) ?? messages19.get("en-US")
1765
1790
  );
1766
1791
  }
1767
1792
  },
@@ -1805,7 +1830,7 @@ var NebulaI18nProvider = ({
1805
1830
  NebulaI18nContext.Provider,
1806
1831
  {
1807
1832
  value: {
1808
- messages: messages17,
1833
+ messages: messages20,
1809
1834
  locale: getNebulaLanguage()
1810
1835
  },
1811
1836
  children
@@ -1827,7 +1852,7 @@ var Pagination = ({
1827
1852
  onChangePage,
1828
1853
  ...props
1829
1854
  }) => {
1830
- const { messages: messages17 } = useNebulaI18n();
1855
+ const { messages: messages20 } = useNebulaI18n();
1831
1856
  const totalPages = useMemo(() => {
1832
1857
  return Math.ceil(total / (pageSize || 1));
1833
1858
  }, [total, pageSize]);
@@ -1860,13 +1885,13 @@ var Pagination = ({
1860
1885
  }, [totalPages, pageSize, total]);
1861
1886
  const totalResultsLabel = useMemo(() => {
1862
1887
  if (page === totalPages) {
1863
- return messages17.pagination.totalResultsLabel(lastPageSize, total);
1888
+ return messages20.pagination.totalResultsLabel(lastPageSize, total);
1864
1889
  }
1865
- return messages17.pagination.totalResultsLabel(pageSize, total);
1866
- }, [messages17.pagination, pageSize, total, page, totalPages, lastPageSize]);
1890
+ return messages20.pagination.totalResultsLabel(pageSize, total);
1891
+ }, [messages20.pagination, pageSize, total, page, totalPages, lastPageSize]);
1867
1892
  const currentPageLabel = useMemo(
1868
- () => messages17.pagination.currentPageLabel(normalizedPage, totalPages),
1869
- [messages17.pagination, normalizedPage, totalPages]
1893
+ () => messages20.pagination.currentPageLabel(normalizedPage, totalPages),
1894
+ [messages20.pagination, normalizedPage, totalPages]
1870
1895
  );
1871
1896
  return /* @__PURE__ */ jsxs(
1872
1897
  "nav",
@@ -2139,7 +2164,7 @@ var createStyledSelect = (BaseSelect, displayName) => {
2139
2164
  isError = false,
2140
2165
  ...props
2141
2166
  }) => {
2142
- const { messages: messages17 } = useNebulaI18n();
2167
+ const { messages: messages20 } = useNebulaI18n();
2143
2168
  const customClassNames = useMemo(() => {
2144
2169
  return {
2145
2170
  control: (props2) => controlStyles(props2, isError),
@@ -2186,7 +2211,7 @@ var createStyledSelect = (BaseSelect, displayName) => {
2186
2211
  isDisabled: disabled,
2187
2212
  components: customComponents,
2188
2213
  classNames: customClassNames,
2189
- noOptionsMessage: () => /* @__PURE__ */ jsx("p", { children: messages17.inputSelect.noOptions }),
2214
+ noOptionsMessage: () => /* @__PURE__ */ jsx("p", { children: messages20.inputSelect.noOptions }),
2190
2215
  ...props
2191
2216
  }
2192
2217
  );
@@ -2332,16 +2357,17 @@ var DialogOverlay = React8.forwardRef(({ className, ...props }, ref) => /* @__PU
2332
2357
  }
2333
2358
  ));
2334
2359
  DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
2335
- var DialogContent = React8.forwardRef(({ className, children, portal = false, ...props }, ref) => {
2336
- const Comp = portal ? DialogPortal : React8.Fragment;
2337
- return /* @__PURE__ */ jsxs(Comp, { children: [
2338
- /* @__PURE__ */ jsx(DialogOverlay, {}),
2339
- /* @__PURE__ */ jsxs(
2340
- DialogPrimitive.Content,
2341
- {
2342
- ref,
2343
- className: cn(
2344
- `rounded-2xl
2360
+ var DialogContent = React8.forwardRef(
2361
+ ({ className, children, portal = false, showClose = true, ...props }, ref) => {
2362
+ const Comp = portal ? DialogPortal : React8.Fragment;
2363
+ return /* @__PURE__ */ jsxs(Comp, { children: [
2364
+ /* @__PURE__ */ jsx(DialogOverlay, {}),
2365
+ /* @__PURE__ */ jsxs(
2366
+ DialogPrimitive.Content,
2367
+ {
2368
+ ref,
2369
+ className: cn(
2370
+ `rounded-2xl
2345
2371
  fixed
2346
2372
  left-[50%]
2347
2373
  top-[50%]
@@ -2366,29 +2392,30 @@ var DialogContent = React8.forwardRef(({ className, children, portal = false, ..
2366
2392
  data-[state=closed]:slide-out-to-top-[48%]
2367
2393
  data-[state=open]:slide-in-from-left-1/2
2368
2394
  data-[state=open]:slide-in-from-top-[48%]`,
2369
- className
2370
- ),
2371
- ...props,
2372
- children: [
2373
- children,
2374
- /* @__PURE__ */ jsx(
2375
- DialogPrimitive.Close,
2376
- {
2377
- asChild: true,
2378
- className: `absolute
2379
- right-4
2380
- top-4`,
2381
- children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", icon: true, children: [
2382
- /* @__PURE__ */ jsx(X, { className: "nebula-ds !h-4 !w-4 !text-dialog-icon" }),
2383
- /* @__PURE__ */ jsx("span", { className: "nebula-ds sr-only", children: "Close" })
2384
- ] })
2385
- }
2386
- )
2387
- ]
2388
- }
2389
- )
2390
- ] });
2391
- });
2395
+ className
2396
+ ),
2397
+ ...props,
2398
+ children: [
2399
+ children,
2400
+ showClose && /* @__PURE__ */ jsx(
2401
+ DialogPrimitive.Close,
2402
+ {
2403
+ asChild: true,
2404
+ className: `absolute
2405
+ right-4
2406
+ top-4`,
2407
+ children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", icon: true, children: [
2408
+ /* @__PURE__ */ jsx(X, { className: "nebula-ds !h-4 !w-4 !text-dialog-icon" }),
2409
+ /* @__PURE__ */ jsx("span", { className: "nebula-ds sr-only", children: "Close" })
2410
+ ] })
2411
+ }
2412
+ )
2413
+ ]
2414
+ }
2415
+ )
2416
+ ] });
2417
+ }
2418
+ );
2392
2419
  DialogContent.displayName = DialogPrimitive.Content.displayName;
2393
2420
  var DialogHeader = ({
2394
2421
  className,
@@ -3204,13 +3231,13 @@ function custom(message, options) {
3204
3231
  }
3205
3232
  );
3206
3233
  }
3207
- async function promise(promise2, messages17, options) {
3234
+ async function promise(promise2, messages20, options) {
3208
3235
  const loadingToast = toast$1.custom(
3209
3236
  (t) => /* @__PURE__ */ jsx(
3210
3237
  ToastComponent,
3211
3238
  {
3212
3239
  type: "info",
3213
- message: messages17.loading,
3240
+ message: messages20.loading,
3214
3241
  options,
3215
3242
  t
3216
3243
  }
@@ -3224,7 +3251,7 @@ async function promise(promise2, messages17, options) {
3224
3251
  ToastComponent,
3225
3252
  {
3226
3253
  type: "success",
3227
- message: messages17.success,
3254
+ message: messages20.success,
3228
3255
  options,
3229
3256
  t
3230
3257
  }
@@ -3239,7 +3266,7 @@ async function promise(promise2, messages17, options) {
3239
3266
  ToastComponent,
3240
3267
  {
3241
3268
  type: "error",
3242
- message: messages17.error,
3269
+ message: messages20.error,
3243
3270
  options,
3244
3271
  t
3245
3272
  }
@@ -3409,8 +3436,8 @@ var CountrySelect = ({
3409
3436
  const handleSelect = (event) => {
3410
3437
  onChange(event.target.value);
3411
3438
  };
3412
- const { messages: messages17 } = useNebulaI18n();
3413
- const { countries } = messages17.inputPhone;
3439
+ const { messages: messages20 } = useNebulaI18n();
3440
+ const { countries } = messages20.inputPhone;
3414
3441
  return /* @__PURE__ */ jsxs("div", { className: "nebula-ds rounded-s-[20px] relative inline-flex items-center self-stretch py-2 ps-4 pe-2 transition-[color,box-shadow] outline-none has-disabled:pointer-events-none has-disabled:opacity-50", children: [
3415
3442
  /* @__PURE__ */ jsxs("div", { className: "nebula-ds inline-flex items-center gap-1", "aria-hidden": "true", children: [
3416
3443
  /* @__PURE__ */ jsx(FlagComponent, { country: value, countryName: value, "aria-hidden": "true" }),
@@ -4230,7 +4257,7 @@ var InputDateTimePickerSingle = ({
4230
4257
  formattedDateByLanguage ? new Date(formattedDateByLanguage) : void 0
4231
4258
  );
4232
4259
  const [popoverIsOpen, setPopoverIsOpen] = useState(false);
4233
- const { locale, messages: messages17 } = useNebulaI18n();
4260
+ const { locale, messages: messages20 } = useNebulaI18n();
4234
4261
  const [month, setMonth] = useState(/* @__PURE__ */ new Date());
4235
4262
  const inputTimeRef = useRef(null);
4236
4263
  const handleClearValue = () => {
@@ -4402,7 +4429,7 @@ var InputDateTimePickerSingle = ({
4402
4429
  disabled: disabledDates,
4403
4430
  footer: /* @__PURE__ */ jsxs(Space, { className: "nebula-ds items-center", children: [
4404
4431
  /* @__PURE__ */ jsxs(Label, { children: [
4405
- messages17.timePicker.label,
4432
+ messages20.timePicker.label,
4406
4433
  ":"
4407
4434
  ] }),
4408
4435
  /* @__PURE__ */ jsx(
@@ -4944,9 +4971,11 @@ var ProfileImage = ({
4944
4971
  onError,
4945
4972
  maxFiles = Infinity,
4946
4973
  onRemove,
4974
+ image,
4947
4975
  ...rest
4948
4976
  }) => {
4949
4977
  const maxSize = maxSizeMB * 1024 * 1024;
4978
+ const id = useId();
4950
4979
  const [
4951
4980
  { files, isDragging, errors },
4952
4981
  {
@@ -4959,6 +4988,15 @@ var ProfileImage = ({
4959
4988
  getInputProps
4960
4989
  }
4961
4990
  ] = useFileUpload({
4991
+ initialFiles: image ? [
4992
+ {
4993
+ id,
4994
+ url: image,
4995
+ name: image,
4996
+ size: 0,
4997
+ type: "image"
4998
+ }
4999
+ ] : [],
4962
5000
  multiple: false,
4963
5001
  maxSize: maxSize > 0 ? maxSize : void 0,
4964
5002
  accept: "image/*",
@@ -5034,6 +5072,1026 @@ var ProfileImage = ({
5034
5072
  ] });
5035
5073
  };
5036
5074
  ProfileImage.displayName = "ProfileImage";
5075
+ function clamp(value, min, max) {
5076
+ return Math.min(Math.max(value, min), max);
5077
+ }
5078
+ var CropperContext = createContext(null);
5079
+ var useCropperContext = () => {
5080
+ const context = useContext(CropperContext);
5081
+ if (!context) {
5082
+ throw new Error("useCropperContext must be used within a Cropper.Root");
5083
+ }
5084
+ return context;
5085
+ };
5086
+ var CropperRoot = ({
5087
+ image,
5088
+ cropPadding = 25,
5089
+ aspectRatio = 1,
5090
+ minZoom = 1,
5091
+ maxZoom = 3,
5092
+ zoomSensitivity = 5e-3,
5093
+ keyboardStep = 10,
5094
+ className,
5095
+ style,
5096
+ zoom: zoomProp,
5097
+ onCropChange,
5098
+ onZoomChange,
5099
+ children,
5100
+ ...restProps
5101
+ }) => {
5102
+ const descriptionId = useId();
5103
+ const [imgWidth, setImgWidth] = useState(null);
5104
+ const [imgHeight, setImgHeight] = useState(null);
5105
+ const containerRef = useRef(null);
5106
+ const [cropAreaWidth, setCropAreaWidth] = useState(0);
5107
+ const [cropAreaHeight, setCropAreaHeight] = useState(0);
5108
+ const [imageWrapperWidth, setImageWrapperWidth] = useState(0);
5109
+ const [imageWrapperHeight, setImageWrapperHeight] = useState(0);
5110
+ const [offsetX, setOffsetX] = useState(0);
5111
+ const [offsetY, setOffsetY] = useState(0);
5112
+ const [internalZoom, setInternalZoom] = useState(minZoom);
5113
+ const [isDragging, setIsDragging] = useState(false);
5114
+ const dragStartPointRef = useRef({ x: 0, y: 0 });
5115
+ const dragStartOffsetRef = useRef({ x: 0, y: 0 });
5116
+ const latestRestrictedOffsetRef = useRef({
5117
+ x: offsetX,
5118
+ y: offsetY
5119
+ });
5120
+ const latestZoomRef = useRef(internalZoom);
5121
+ const isInitialSetupDoneRef = useRef(false);
5122
+ const initialPinchDistanceRef = useRef(0);
5123
+ const initialPinchZoomRef = useRef(1);
5124
+ const isPinchingRef = useRef(false);
5125
+ const hasWarnedRef = useRef(false);
5126
+ const isZoomControlled = zoomProp !== void 0;
5127
+ const effectiveZoom = isZoomControlled ? zoomProp : internalZoom;
5128
+ const updateZoom = useCallback(
5129
+ (newZoomValue) => {
5130
+ const clampedZoom = clamp(newZoomValue, minZoom, maxZoom);
5131
+ if (onZoomChange) {
5132
+ onZoomChange(clampedZoom);
5133
+ } else if (!isZoomControlled) {
5134
+ setInternalZoom(clampedZoom);
5135
+ }
5136
+ return clampedZoom;
5137
+ },
5138
+ [minZoom, maxZoom, onZoomChange, isZoomControlled]
5139
+ );
5140
+ useEffect(() => {
5141
+ latestZoomRef.current = effectiveZoom;
5142
+ }, [effectiveZoom]);
5143
+ useEffect(() => {
5144
+ setOffsetX(0);
5145
+ setOffsetY(0);
5146
+ if (!isZoomControlled) {
5147
+ setInternalZoom(minZoom);
5148
+ }
5149
+ isInitialSetupDoneRef.current = false;
5150
+ if (!image) {
5151
+ setImgWidth(null);
5152
+ setImgHeight(null);
5153
+ return;
5154
+ }
5155
+ let isMounted = true;
5156
+ const img = new Image();
5157
+ img.onload = () => {
5158
+ if (isMounted) {
5159
+ setImgWidth(img.naturalWidth);
5160
+ setImgHeight(img.naturalHeight);
5161
+ }
5162
+ };
5163
+ img.onerror = () => {
5164
+ if (isMounted) {
5165
+ setImgWidth(null);
5166
+ setImgHeight(null);
5167
+ }
5168
+ };
5169
+ img.src = image;
5170
+ return () => {
5171
+ isMounted = false;
5172
+ };
5173
+ }, [image, minZoom, isZoomControlled]);
5174
+ const updateCropAreaDimensions = useCallback(
5175
+ (containerWidth, containerHeight) => {
5176
+ if (containerWidth <= 0 || containerHeight <= 0) {
5177
+ setCropAreaWidth(0);
5178
+ setCropAreaHeight(0);
5179
+ return;
5180
+ }
5181
+ const maxPossibleWidth = Math.max(0, containerWidth - cropPadding * 2);
5182
+ const maxPossibleHeight = Math.max(0, containerHeight - cropPadding * 2);
5183
+ let targetCropW = 0;
5184
+ let targetCropH = 0;
5185
+ if (maxPossibleWidth / aspectRatio >= maxPossibleHeight) {
5186
+ targetCropH = maxPossibleHeight;
5187
+ targetCropW = targetCropH * aspectRatio;
5188
+ } else {
5189
+ targetCropW = maxPossibleWidth;
5190
+ targetCropH = targetCropW / aspectRatio;
5191
+ }
5192
+ setCropAreaWidth(targetCropW);
5193
+ setCropAreaHeight(targetCropH);
5194
+ },
5195
+ [aspectRatio, cropPadding]
5196
+ );
5197
+ useEffect(() => {
5198
+ const element = containerRef.current;
5199
+ if (!element) return;
5200
+ const observer = new ResizeObserver((entries) => {
5201
+ for (const entry of entries) {
5202
+ const { width, height } = entry.contentRect;
5203
+ if (width > 0 && height > 0) updateCropAreaDimensions(width, height);
5204
+ }
5205
+ });
5206
+ observer.observe(element);
5207
+ const initialWidth = element.clientWidth;
5208
+ const initialHeight = element.clientHeight;
5209
+ if (initialWidth > 0 && initialHeight > 0)
5210
+ updateCropAreaDimensions(initialWidth, initialHeight);
5211
+ return () => observer.disconnect();
5212
+ }, [updateCropAreaDimensions]);
5213
+ useEffect(() => {
5214
+ if (cropAreaWidth <= 0 || cropAreaHeight <= 0 || !imgWidth || !imgHeight) {
5215
+ setImageWrapperWidth(0);
5216
+ setImageWrapperHeight(0);
5217
+ return;
5218
+ }
5219
+ const naturalAspect = imgWidth / imgHeight;
5220
+ const cropAspect = cropAreaWidth / cropAreaHeight;
5221
+ let targetWrapperWidth = 0;
5222
+ let targetWrapperHeight = 0;
5223
+ if (naturalAspect >= cropAspect) {
5224
+ targetWrapperHeight = cropAreaHeight;
5225
+ targetWrapperWidth = targetWrapperHeight * naturalAspect;
5226
+ } else {
5227
+ targetWrapperWidth = cropAreaWidth;
5228
+ targetWrapperHeight = targetWrapperWidth / naturalAspect;
5229
+ }
5230
+ setImageWrapperWidth(targetWrapperWidth);
5231
+ setImageWrapperHeight(targetWrapperHeight);
5232
+ }, [cropAreaWidth, cropAreaHeight, imgWidth, imgHeight]);
5233
+ const restrictOffset = useCallback(
5234
+ (dragOffsetX, dragOffsetY, currentZoom) => {
5235
+ if (imageWrapperWidth <= 0 || imageWrapperHeight <= 0 || cropAreaWidth <= 0 || cropAreaHeight <= 0)
5236
+ return { x: 0, y: 0 };
5237
+ const effectiveWrapperWidth = imageWrapperWidth * currentZoom;
5238
+ const effectiveWrapperHeight = imageWrapperHeight * currentZoom;
5239
+ const maxDragX = Math.max(0, (effectiveWrapperWidth - cropAreaWidth) / 2);
5240
+ const maxDragY = Math.max(
5241
+ 0,
5242
+ (effectiveWrapperHeight - cropAreaHeight) / 2
5243
+ );
5244
+ return {
5245
+ x: clamp(dragOffsetX, -maxDragX, maxDragX),
5246
+ y: clamp(dragOffsetY, -maxDragY, maxDragY)
5247
+ };
5248
+ },
5249
+ [imageWrapperWidth, imageWrapperHeight, cropAreaWidth, cropAreaHeight]
5250
+ );
5251
+ const calculateCropData = useCallback(
5252
+ (finalOffsetX, finalOffsetY, finalZoom) => {
5253
+ const currentOffsetX = finalOffsetX !== void 0 ? finalOffsetX : latestRestrictedOffsetRef.current.x;
5254
+ const currentOffsetY = finalOffsetY !== void 0 ? finalOffsetY : latestRestrictedOffsetRef.current.y;
5255
+ const currentZoom = finalZoom !== void 0 ? finalZoom : effectiveZoom;
5256
+ if (!imgWidth || !imgHeight || imageWrapperWidth <= 0 || imageWrapperHeight <= 0 || cropAreaWidth <= 0 || cropAreaHeight <= 0)
5257
+ return null;
5258
+ const scaledWrapperWidth = imageWrapperWidth * currentZoom;
5259
+ const scaledWrapperHeight = imageWrapperHeight * currentZoom;
5260
+ const topLeftOffsetX = currentOffsetX + (cropAreaWidth - scaledWrapperWidth) / 2;
5261
+ const topLeftOffsetY = currentOffsetY + (cropAreaHeight - scaledWrapperHeight) / 2;
5262
+ const baseScale = imgWidth / imageWrapperWidth;
5263
+ if (isNaN(baseScale) || baseScale === 0) return null;
5264
+ const sx = -topLeftOffsetX * baseScale / currentZoom;
5265
+ const sy = -topLeftOffsetY * baseScale / currentZoom;
5266
+ const sWidth = cropAreaWidth * baseScale / currentZoom;
5267
+ const sHeight = cropAreaHeight * baseScale / currentZoom;
5268
+ const finalX = clamp(Math.round(sx), 0, imgWidth);
5269
+ const finalY = clamp(Math.round(sy), 0, imgHeight);
5270
+ const finalWidth = clamp(Math.round(sWidth), 0, imgWidth - finalX);
5271
+ const finalHeight = clamp(Math.round(sHeight), 0, imgHeight - finalY);
5272
+ if (finalWidth <= 0 || finalHeight <= 0) return null;
5273
+ return { x: finalX, y: finalY, width: finalWidth, height: finalHeight };
5274
+ },
5275
+ [
5276
+ imgWidth,
5277
+ imgHeight,
5278
+ imageWrapperWidth,
5279
+ imageWrapperHeight,
5280
+ cropAreaWidth,
5281
+ cropAreaHeight,
5282
+ effectiveZoom
5283
+ ]
5284
+ );
5285
+ useEffect(() => {
5286
+ if (imageWrapperWidth > 0 && imageWrapperHeight > 0 && cropAreaWidth > 0 && cropAreaHeight > 0) {
5287
+ const currentZoomForSetup = effectiveZoom;
5288
+ if (!isInitialSetupDoneRef.current) {
5289
+ const initialX = 0;
5290
+ const initialY = 0;
5291
+ const restrictedInitial = restrictOffset(
5292
+ initialX,
5293
+ initialY,
5294
+ currentZoomForSetup
5295
+ );
5296
+ setOffsetX(restrictedInitial.x);
5297
+ setOffsetY(restrictedInitial.y);
5298
+ if (!isZoomControlled) setInternalZoom(currentZoomForSetup);
5299
+ dragStartOffsetRef.current = restrictedInitial;
5300
+ latestRestrictedOffsetRef.current = restrictedInitial;
5301
+ latestZoomRef.current = currentZoomForSetup;
5302
+ if (onCropChange)
5303
+ onCropChange(
5304
+ calculateCropData(
5305
+ restrictedInitial.x,
5306
+ restrictedInitial.y,
5307
+ currentZoomForSetup
5308
+ )
5309
+ );
5310
+ isInitialSetupDoneRef.current = true;
5311
+ } else {
5312
+ const restrictedCurrent = restrictOffset(
5313
+ latestRestrictedOffsetRef.current.x,
5314
+ latestRestrictedOffsetRef.current.y,
5315
+ currentZoomForSetup
5316
+ );
5317
+ if (restrictedCurrent.x !== latestRestrictedOffsetRef.current.x || restrictedCurrent.y !== latestRestrictedOffsetRef.current.y) {
5318
+ setOffsetX(restrictedCurrent.x);
5319
+ setOffsetY(restrictedCurrent.y);
5320
+ latestRestrictedOffsetRef.current = restrictedCurrent;
5321
+ dragStartOffsetRef.current = restrictedCurrent;
5322
+ }
5323
+ if (onCropChange)
5324
+ onCropChange(
5325
+ calculateCropData(
5326
+ restrictedCurrent.x,
5327
+ restrictedCurrent.y,
5328
+ currentZoomForSetup
5329
+ )
5330
+ );
5331
+ }
5332
+ } else {
5333
+ isInitialSetupDoneRef.current = false;
5334
+ setOffsetX(0);
5335
+ setOffsetY(0);
5336
+ if (!isZoomControlled) setInternalZoom(minZoom);
5337
+ dragStartOffsetRef.current = { x: 0, y: 0 };
5338
+ latestRestrictedOffsetRef.current = { x: 0, y: 0 };
5339
+ latestZoomRef.current = effectiveZoom;
5340
+ if (onCropChange) onCropChange(null);
5341
+ }
5342
+ }, [
5343
+ imageWrapperWidth,
5344
+ imgHeight,
5345
+ cropAreaWidth,
5346
+ cropAreaHeight,
5347
+ restrictOffset,
5348
+ onCropChange,
5349
+ calculateCropData,
5350
+ minZoom,
5351
+ effectiveZoom,
5352
+ isZoomControlled,
5353
+ updateZoom,
5354
+ imageWrapperHeight
5355
+ ]);
5356
+ useEffect(() => {
5357
+ const checkTimeout = setTimeout(() => {
5358
+ if (containerRef.current && !hasWarnedRef.current) {
5359
+ const hasDescription = document.getElementById(descriptionId);
5360
+ if (!hasDescription) {
5361
+ hasWarnedRef.current = true;
5362
+ }
5363
+ }
5364
+ }, 100);
5365
+ return () => clearTimeout(checkTimeout);
5366
+ }, [descriptionId]);
5367
+ const handleInteractionEnd = useCallback(() => {
5368
+ if (onCropChange) {
5369
+ const finalData = calculateCropData(
5370
+ latestRestrictedOffsetRef.current.x,
5371
+ latestRestrictedOffsetRef.current.y,
5372
+ effectiveZoom
5373
+ );
5374
+ onCropChange(finalData);
5375
+ }
5376
+ }, [onCropChange, calculateCropData, effectiveZoom]);
5377
+ const handleMouseDown = useCallback(
5378
+ (e) => {
5379
+ if (e.button !== 0 || !containerRef.current) return;
5380
+ e.preventDefault();
5381
+ setIsDragging(true);
5382
+ isPinchingRef.current = false;
5383
+ dragStartPointRef.current = { x: e.clientX, y: e.clientY };
5384
+ dragStartOffsetRef.current = {
5385
+ x: latestRestrictedOffsetRef.current.x,
5386
+ y: latestRestrictedOffsetRef.current.y
5387
+ };
5388
+ const handleMouseMove = (ev) => {
5389
+ const deltaX = ev.clientX - dragStartPointRef.current.x;
5390
+ const deltaY = ev.clientY - dragStartPointRef.current.y;
5391
+ const targetOffsetX = dragStartOffsetRef.current.x + deltaX;
5392
+ const targetOffsetY = dragStartOffsetRef.current.y + deltaY;
5393
+ const restricted = restrictOffset(
5394
+ targetOffsetX,
5395
+ targetOffsetY,
5396
+ effectiveZoom
5397
+ );
5398
+ latestRestrictedOffsetRef.current = restricted;
5399
+ setOffsetX(restricted.x);
5400
+ setOffsetY(restricted.y);
5401
+ };
5402
+ const handleMouseUp = () => {
5403
+ setIsDragging(false);
5404
+ window.removeEventListener("mousemove", handleMouseMove);
5405
+ window.removeEventListener("mouseup", handleMouseUp);
5406
+ handleInteractionEnd();
5407
+ };
5408
+ window.addEventListener("mousemove", handleMouseMove);
5409
+ window.addEventListener("mouseup", handleMouseUp);
5410
+ },
5411
+ [restrictOffset, effectiveZoom, handleInteractionEnd]
5412
+ );
5413
+ const handleWheel = useCallback(
5414
+ (e) => {
5415
+ e.preventDefault();
5416
+ e.stopPropagation();
5417
+ if (!containerRef.current || imageWrapperWidth <= 0 || imageWrapperHeight <= 0)
5418
+ return;
5419
+ const currentZoom = latestZoomRef.current;
5420
+ const currentOffsetX = latestRestrictedOffsetRef.current.x;
5421
+ const currentOffsetY = latestRestrictedOffsetRef.current.y;
5422
+ const delta = e.deltaY * -zoomSensitivity;
5423
+ const targetZoom = currentZoom + delta;
5424
+ if (clamp(targetZoom, minZoom, maxZoom) === currentZoom) return;
5425
+ const rect = containerRef.current.getBoundingClientRect();
5426
+ const pointerX = e.clientX - rect.left - rect.width / 2;
5427
+ const pointerY = e.clientY - rect.top - rect.height / 2;
5428
+ const imagePointX = (pointerX - currentOffsetX) / currentZoom;
5429
+ const imagePointY = (pointerY - currentOffsetY) / currentZoom;
5430
+ const finalNewZoom = updateZoom(targetZoom);
5431
+ const newOffsetX = pointerX - imagePointX * finalNewZoom;
5432
+ const newOffsetY = pointerY - imagePointY * finalNewZoom;
5433
+ const restrictedNewOffset = restrictOffset(
5434
+ newOffsetX,
5435
+ newOffsetY,
5436
+ finalNewZoom
5437
+ );
5438
+ setOffsetX(restrictedNewOffset.x);
5439
+ setOffsetY(restrictedNewOffset.y);
5440
+ latestRestrictedOffsetRef.current = restrictedNewOffset;
5441
+ if (onCropChange) {
5442
+ const finalData = calculateCropData(
5443
+ restrictedNewOffset.x,
5444
+ restrictedNewOffset.y,
5445
+ finalNewZoom
5446
+ );
5447
+ onCropChange(finalData);
5448
+ }
5449
+ },
5450
+ [
5451
+ restrictOffset,
5452
+ calculateCropData,
5453
+ imageWrapperWidth,
5454
+ imageWrapperHeight,
5455
+ onCropChange,
5456
+ minZoom,
5457
+ maxZoom,
5458
+ zoomSensitivity,
5459
+ updateZoom
5460
+ ]
5461
+ );
5462
+ const getPinchDistance = (touches) => Math.sqrt(
5463
+ Math.pow(touches[1].clientX - touches[0].clientX, 2) + Math.pow(touches[1].clientY - touches[0].clientY, 2)
5464
+ );
5465
+ const getPinchCenter = (touches) => ({
5466
+ x: (touches[0].clientX + touches[1].clientX) / 2,
5467
+ y: (touches[0].clientY + touches[1].clientY) / 2
5468
+ });
5469
+ const handleTouchStart = useCallback(
5470
+ (e) => {
5471
+ if (!containerRef.current || imageWrapperWidth <= 0 || imageWrapperHeight <= 0)
5472
+ return;
5473
+ e.preventDefault();
5474
+ const touches = e.touches;
5475
+ if (touches.length === 1) {
5476
+ setIsDragging(true);
5477
+ isPinchingRef.current = false;
5478
+ dragStartPointRef.current = {
5479
+ x: touches[0].clientX,
5480
+ y: touches[0].clientY
5481
+ };
5482
+ dragStartOffsetRef.current = {
5483
+ x: latestRestrictedOffsetRef.current.x,
5484
+ y: latestRestrictedOffsetRef.current.y
5485
+ };
5486
+ } else if (touches.length === 2) {
5487
+ setIsDragging(false);
5488
+ isPinchingRef.current = true;
5489
+ initialPinchDistanceRef.current = getPinchDistance(touches);
5490
+ initialPinchZoomRef.current = latestZoomRef.current;
5491
+ dragStartOffsetRef.current = {
5492
+ x: latestRestrictedOffsetRef.current.x,
5493
+ y: latestRestrictedOffsetRef.current.y
5494
+ };
5495
+ }
5496
+ },
5497
+ [imageWrapperWidth, imageWrapperHeight]
5498
+ );
5499
+ const handleTouchMove = useCallback(
5500
+ (e) => {
5501
+ if (!containerRef.current || imageWrapperWidth <= 0 || imageWrapperHeight <= 0)
5502
+ return;
5503
+ e.preventDefault();
5504
+ const touches = e.touches;
5505
+ if (touches.length === 1 && isDragging && !isPinchingRef.current) {
5506
+ const deltaX = touches[0].clientX - dragStartPointRef.current.x;
5507
+ const deltaY = touches[0].clientY - dragStartPointRef.current.y;
5508
+ const targetOffsetX = dragStartOffsetRef.current.x + deltaX;
5509
+ const targetOffsetY = dragStartOffsetRef.current.y + deltaY;
5510
+ const restricted = restrictOffset(
5511
+ targetOffsetX,
5512
+ targetOffsetY,
5513
+ effectiveZoom
5514
+ );
5515
+ latestRestrictedOffsetRef.current = restricted;
5516
+ setOffsetX(restricted.x);
5517
+ setOffsetY(restricted.y);
5518
+ } else if (touches.length === 2 && isPinchingRef.current) {
5519
+ const currentPinchDistance = getPinchDistance(touches);
5520
+ const scale = currentPinchDistance / initialPinchDistanceRef.current;
5521
+ const currentZoom = initialPinchZoomRef.current;
5522
+ const targetZoom = currentZoom * scale;
5523
+ if (clamp(targetZoom, minZoom, maxZoom) === latestZoomRef.current)
5524
+ return;
5525
+ const pinchCenter = getPinchCenter(touches);
5526
+ const rect = containerRef.current.getBoundingClientRect();
5527
+ const pinchCenterX = pinchCenter.x - rect.left - rect.width / 2;
5528
+ const pinchCenterY = pinchCenter.y - rect.top - rect.height / 2;
5529
+ const currentOffsetX = dragStartOffsetRef.current.x;
5530
+ const currentOffsetY = dragStartOffsetRef.current.y;
5531
+ const imagePointX = (pinchCenterX - currentOffsetX) / currentZoom;
5532
+ const imagePointY = (pinchCenterY - currentOffsetY) / currentZoom;
5533
+ const finalNewZoom = updateZoom(targetZoom);
5534
+ const newOffsetX = pinchCenterX - imagePointX * finalNewZoom;
5535
+ const newOffsetY = pinchCenterY - imagePointY * finalNewZoom;
5536
+ const restrictedNewOffset = restrictOffset(
5537
+ newOffsetX,
5538
+ newOffsetY,
5539
+ finalNewZoom
5540
+ );
5541
+ setOffsetX(restrictedNewOffset.x);
5542
+ setOffsetY(restrictedNewOffset.y);
5543
+ latestRestrictedOffsetRef.current = restrictedNewOffset;
5544
+ if (onCropChange) {
5545
+ const finalData = calculateCropData(
5546
+ restrictedNewOffset.x,
5547
+ restrictedNewOffset.y,
5548
+ finalNewZoom
5549
+ );
5550
+ onCropChange(finalData);
5551
+ }
5552
+ }
5553
+ },
5554
+ [
5555
+ isDragging,
5556
+ restrictOffset,
5557
+ minZoom,
5558
+ maxZoom,
5559
+ imageWrapperWidth,
5560
+ imageWrapperHeight,
5561
+ effectiveZoom,
5562
+ updateZoom,
5563
+ onCropChange,
5564
+ calculateCropData
5565
+ ]
5566
+ );
5567
+ const handleTouchEnd = useCallback(
5568
+ (e) => {
5569
+ e.preventDefault();
5570
+ const touches = e.touches;
5571
+ if (isPinchingRef.current && touches.length < 2) {
5572
+ isPinchingRef.current = false;
5573
+ if (touches.length === 1) {
5574
+ setIsDragging(true);
5575
+ dragStartPointRef.current = {
5576
+ x: touches[0].clientX,
5577
+ y: touches[0].clientY
5578
+ };
5579
+ dragStartOffsetRef.current = {
5580
+ x: latestRestrictedOffsetRef.current.x,
5581
+ y: latestRestrictedOffsetRef.current.y
5582
+ };
5583
+ } else {
5584
+ setIsDragging(false);
5585
+ handleInteractionEnd();
5586
+ }
5587
+ } else if (isDragging && touches.length === 0) {
5588
+ setIsDragging(false);
5589
+ handleInteractionEnd();
5590
+ }
5591
+ },
5592
+ [isDragging, handleInteractionEnd]
5593
+ );
5594
+ const handleKeyDown = useCallback(
5595
+ (e) => {
5596
+ if (imageWrapperWidth <= 0) return;
5597
+ let targetOffsetX = latestRestrictedOffsetRef.current.x;
5598
+ let targetOffsetY = latestRestrictedOffsetRef.current.y;
5599
+ let moved = false;
5600
+ switch (e.key) {
5601
+ case "ArrowUp":
5602
+ targetOffsetY += keyboardStep;
5603
+ moved = true;
5604
+ break;
5605
+ case "ArrowDown":
5606
+ targetOffsetY -= keyboardStep;
5607
+ moved = true;
5608
+ break;
5609
+ case "ArrowLeft":
5610
+ targetOffsetX += keyboardStep;
5611
+ moved = true;
5612
+ break;
5613
+ case "ArrowRight":
5614
+ targetOffsetX -= keyboardStep;
5615
+ moved = true;
5616
+ break;
5617
+ default:
5618
+ return;
5619
+ }
5620
+ if (moved) {
5621
+ e.preventDefault();
5622
+ const restricted = restrictOffset(
5623
+ targetOffsetX,
5624
+ targetOffsetY,
5625
+ effectiveZoom
5626
+ );
5627
+ if (restricted.x !== latestRestrictedOffsetRef.current.x || restricted.y !== latestRestrictedOffsetRef.current.y) {
5628
+ latestRestrictedOffsetRef.current = restricted;
5629
+ setOffsetX(restricted.x);
5630
+ setOffsetY(restricted.y);
5631
+ }
5632
+ }
5633
+ },
5634
+ [keyboardStep, imageWrapperWidth, restrictOffset, effectiveZoom]
5635
+ );
5636
+ const handleKeyUp = useCallback(
5637
+ (e) => {
5638
+ if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
5639
+ handleInteractionEnd();
5640
+ }
5641
+ },
5642
+ [handleInteractionEnd]
5643
+ );
5644
+ useEffect(() => {
5645
+ const node = containerRef.current;
5646
+ if (!node) return;
5647
+ const options = { passive: false };
5648
+ node.addEventListener("wheel", handleWheel, options);
5649
+ node.addEventListener("touchstart", handleTouchStart, options);
5650
+ node.addEventListener("touchmove", handleTouchMove, options);
5651
+ node.addEventListener("touchend", handleTouchEnd, options);
5652
+ node.addEventListener("touchcancel", handleTouchEnd, options);
5653
+ return () => {
5654
+ node.removeEventListener("wheel", handleWheel, options);
5655
+ node.removeEventListener("touchstart", handleTouchStart, options);
5656
+ node.removeEventListener("touchmove", handleTouchMove, options);
5657
+ node.removeEventListener("touchend", handleTouchEnd, options);
5658
+ node.removeEventListener("touchcancel", handleTouchEnd, options);
5659
+ };
5660
+ }, [handleWheel, handleTouchStart, handleTouchMove, handleTouchEnd]);
5661
+ const getRootProps = useCallback(
5662
+ () => ({
5663
+ className,
5664
+ style,
5665
+ onMouseDown: handleMouseDown,
5666
+ onKeyDown: handleKeyDown,
5667
+ onKeyUp: handleKeyUp,
5668
+ tabIndex: 0,
5669
+ role: "application",
5670
+ "aria-label": "Interactive image cropper",
5671
+ "aria-describedby": descriptionId,
5672
+ "aria-valuemin": minZoom,
5673
+ "aria-valuemax": maxZoom,
5674
+ "aria-valuenow": effectiveZoom,
5675
+ "aria-valuetext": `Zoom: ${Math.round(effectiveZoom * 100)}%`
5676
+ }),
5677
+ [
5678
+ className,
5679
+ style,
5680
+ handleMouseDown,
5681
+ handleKeyDown,
5682
+ handleKeyUp,
5683
+ descriptionId,
5684
+ minZoom,
5685
+ maxZoom,
5686
+ effectiveZoom
5687
+ ]
5688
+ );
5689
+ const getImageWrapperStyle = useCallback(
5690
+ () => ({
5691
+ width: imageWrapperWidth,
5692
+ height: imageWrapperHeight,
5693
+ transform: `translate3d(${offsetX}px, ${offsetY}px, 0px) scale(${effectiveZoom})`,
5694
+ position: "absolute",
5695
+ left: `calc(50% - ${imageWrapperWidth / 2}px)`,
5696
+ top: `calc(50% - ${imageWrapperHeight / 2}px)`,
5697
+ willChange: "transform"
5698
+ }),
5699
+ [imageWrapperWidth, imageWrapperHeight, offsetX, offsetY, effectiveZoom]
5700
+ );
5701
+ const getImageProps = useCallback(
5702
+ () => ({
5703
+ src: image,
5704
+ alt: "Image being cropped",
5705
+ draggable: false,
5706
+ "aria-hidden": true
5707
+ }),
5708
+ [image]
5709
+ );
5710
+ const getCropAreaStyle = useCallback(
5711
+ () => ({
5712
+ width: cropAreaWidth,
5713
+ height: cropAreaHeight
5714
+ }),
5715
+ [cropAreaWidth, cropAreaHeight]
5716
+ );
5717
+ const getCropAreaProps = useCallback(
5718
+ () => ({
5719
+ style: getCropAreaStyle(),
5720
+ "aria-hidden": true
5721
+ }),
5722
+ [getCropAreaStyle]
5723
+ );
5724
+ const contextValue = {
5725
+ containerRef,
5726
+ image,
5727
+ imgWidth,
5728
+ imgHeight,
5729
+ cropAreaWidth,
5730
+ cropAreaHeight,
5731
+ imageWrapperWidth,
5732
+ imageWrapperHeight,
5733
+ offsetX,
5734
+ offsetY,
5735
+ effectiveZoom,
5736
+ minZoom,
5737
+ maxZoom,
5738
+ getRootProps,
5739
+ getImageProps,
5740
+ getImageWrapperStyle,
5741
+ getCropAreaProps,
5742
+ getCropAreaStyle,
5743
+ descriptionId
5744
+ };
5745
+ return /* @__PURE__ */ jsx(CropperContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { ref: containerRef, ...getRootProps(), ...restProps, children }) });
5746
+ };
5747
+ var CropperImage = ({ className, ...restProps }) => {
5748
+ const { image, getImageProps, getImageWrapperStyle } = useCropperContext();
5749
+ if (!image) return null;
5750
+ const imageProps = getImageProps();
5751
+ return /* @__PURE__ */ jsx("div", { style: getImageWrapperStyle(), children: /* @__PURE__ */ jsx("img", { ...imageProps, className, ...restProps }) });
5752
+ };
5753
+ var CropperCropArea = ({
5754
+ className,
5755
+ style,
5756
+ ...restProps
5757
+ }) => {
5758
+ const { cropAreaWidth, cropAreaHeight, getCropAreaProps, getCropAreaStyle } = useCropperContext();
5759
+ if (cropAreaWidth <= 0 || cropAreaHeight <= 0) return null;
5760
+ const areaProps = getCropAreaProps();
5761
+ const areaStyle = getCropAreaStyle();
5762
+ return /* @__PURE__ */ jsx(
5763
+ "div",
5764
+ {
5765
+ ...areaProps,
5766
+ style: { ...areaProps.style, ...style, ...areaStyle },
5767
+ className,
5768
+ ...restProps
5769
+ }
5770
+ );
5771
+ };
5772
+ var CropperDescription = ({
5773
+ children,
5774
+ className,
5775
+ ...restProps
5776
+ }) => {
5777
+ const { descriptionId } = useCropperContext();
5778
+ return /* @__PURE__ */ jsx("div", { id: descriptionId, className, ...restProps, children: children ?? // Default description if none provided by user
5779
+ "Use mouse wheel or pinch gesture to zoom. Drag with mouse or touch, or use arrow keys to pan the image within the crop area." });
5780
+ };
5781
+ var Cropper = {
5782
+ Root: CropperRoot,
5783
+ Image: CropperImage,
5784
+ CropArea: CropperCropArea,
5785
+ Description: CropperDescription
5786
+ };
5787
+ function Slider({
5788
+ className,
5789
+ defaultValue,
5790
+ value,
5791
+ min = 0,
5792
+ max = 100,
5793
+ ...props
5794
+ }) {
5795
+ const [internalValues, setInternalValues] = React8.useState(
5796
+ Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]
5797
+ );
5798
+ React8.useEffect(() => {
5799
+ if (value !== void 0) {
5800
+ setInternalValues(Array.isArray(value) ? value : [value]);
5801
+ }
5802
+ }, [value]);
5803
+ const handleValueChange = (newValue) => {
5804
+ setInternalValues(newValue);
5805
+ props.onValueChange?.(newValue);
5806
+ };
5807
+ const renderThumb = () => {
5808
+ const thumb = /* @__PURE__ */ jsx(
5809
+ SliderPrimitive.Thumb,
5810
+ {
5811
+ "data-testid": "slider-thumb",
5812
+ "data-slot": "slider-thumb",
5813
+ className: "nebula-ds border-2 border-slider-rangeColor bg-slider-thumbColor ring-ring/50 block size-4 shrink-0 rounded-full transition-[color,box-shadow] outline-none hover:ring-4 focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50"
5814
+ }
5815
+ );
5816
+ return thumb;
5817
+ };
5818
+ return /* @__PURE__ */ jsxs(
5819
+ SliderPrimitive.Root,
5820
+ {
5821
+ "data-slot": "slider",
5822
+ defaultValue,
5823
+ value,
5824
+ min,
5825
+ max,
5826
+ className: cn(
5827
+ "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
5828
+ className
5829
+ ),
5830
+ onValueChange: handleValueChange,
5831
+ ...props,
5832
+ children: [
5833
+ /* @__PURE__ */ jsx(
5834
+ SliderPrimitive.Track,
5835
+ {
5836
+ "data-slot": "slider-track",
5837
+ className: cn(
5838
+ "bg-slider-trackColor relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
5839
+ ),
5840
+ children: /* @__PURE__ */ jsx(
5841
+ SliderPrimitive.Range,
5842
+ {
5843
+ "data-slot": "slider-range",
5844
+ className: cn(
5845
+ "bg-slider-rangeColor absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
5846
+ )
5847
+ }
5848
+ )
5849
+ }
5850
+ ),
5851
+ Array.from({ length: internalValues.length }, (_, index) => /* @__PURE__ */ jsx(React8.Fragment, { children: renderThumb() }, index))
5852
+ ]
5853
+ }
5854
+ );
5855
+ }
5856
+ function CropperRoot2({
5857
+ className,
5858
+ ...props
5859
+ }) {
5860
+ return /* @__PURE__ */ jsx(
5861
+ Cropper.Root,
5862
+ {
5863
+ "data-slot": "cropper",
5864
+ className: cn(
5865
+ "relative flex w-full cursor-move touch-none items-center justify-center overflow-hidden focus:outline-none",
5866
+ className
5867
+ ),
5868
+ ...props
5869
+ }
5870
+ );
5871
+ }
5872
+ function CropperDescription2({
5873
+ className,
5874
+ ...props
5875
+ }) {
5876
+ return /* @__PURE__ */ jsx(
5877
+ Cropper.Description,
5878
+ {
5879
+ "data-slot": "cropper-description",
5880
+ className: cn("sr-only", className),
5881
+ ...props
5882
+ }
5883
+ );
5884
+ }
5885
+ function CropperImage2({
5886
+ className,
5887
+ ...props
5888
+ }) {
5889
+ return /* @__PURE__ */ jsx(
5890
+ Cropper.Image,
5891
+ {
5892
+ "data-slot": "cropper-image",
5893
+ className: cn(
5894
+ "pointer-events-none h-full w-full object-cover",
5895
+ className
5896
+ ),
5897
+ ...props
5898
+ }
5899
+ );
5900
+ }
5901
+ function CropperCropArea2({
5902
+ className,
5903
+ rounded,
5904
+ ...props
5905
+ }) {
5906
+ return /* @__PURE__ */ jsx(
5907
+ Cropper.CropArea,
5908
+ {
5909
+ "data-slot": "cropper-crop-area",
5910
+ className: cn(
5911
+ "pointer-events-none absolute shadow-[0_0_0_9999px_rgba(0,0,0,0.5)] in-[[data-slot=cropper]:focus-visible]:ring-[2px] in-[[data-slot=cropper]:focus-visible]:ring-white",
5912
+ rounded && "rounded-full",
5913
+ className,
5914
+ "border-2 border-cropper-cropAreaBorderColor"
5915
+ ),
5916
+ ...props
5917
+ }
5918
+ );
5919
+ }
5920
+ var createImage = (url) => new Promise((resolve, reject) => {
5921
+ const image = new Image();
5922
+ image.addEventListener("load", () => resolve(image));
5923
+ image.addEventListener("error", (error2) => reject(error2));
5924
+ image.setAttribute("crossOrigin", "anonymous");
5925
+ image.src = url;
5926
+ });
5927
+ async function getCroppedImg(imageSrc, pixelCrop, outputWidth = pixelCrop.width, outputHeight = pixelCrop.height) {
5928
+ try {
5929
+ const image = await createImage(imageSrc);
5930
+ const canvas = document.createElement("canvas");
5931
+ const ctx = canvas.getContext("2d");
5932
+ if (!ctx) {
5933
+ return null;
5934
+ }
5935
+ canvas.width = outputWidth;
5936
+ canvas.height = outputHeight;
5937
+ ctx.drawImage(
5938
+ image,
5939
+ pixelCrop.x,
5940
+ pixelCrop.y,
5941
+ pixelCrop.width,
5942
+ pixelCrop.height,
5943
+ 0,
5944
+ 0,
5945
+ outputWidth,
5946
+ // Draw onto the output size
5947
+ outputHeight
5948
+ );
5949
+ return new Promise((resolve) => {
5950
+ canvas.toBlob((blob) => {
5951
+ resolve(blob);
5952
+ }, "image/jpeg");
5953
+ });
5954
+ } catch (error2) {
5955
+ return null;
5956
+ }
5957
+ }
5958
+ function Cropper2({
5959
+ onOpenChange,
5960
+ open,
5961
+ previewUrl,
5962
+ onRemove: removeFile,
5963
+ onApply,
5964
+ rounded = false,
5965
+ portal = false
5966
+ }) {
5967
+ const [finalImageUrl, setFinalImageUrl] = useState(null);
5968
+ const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
5969
+ const [zoom, setZoom] = useState(1);
5970
+ const handleCropChange = useCallback((pixels) => {
5971
+ setCroppedAreaPixels(pixels);
5972
+ }, []);
5973
+ const handleApply = async () => {
5974
+ if (!previewUrl || !croppedAreaPixels) {
5975
+ if (previewUrl) {
5976
+ removeFile();
5977
+ setCroppedAreaPixels(null);
5978
+ }
5979
+ return;
5980
+ }
5981
+ try {
5982
+ const croppedBlob = await getCroppedImg(previewUrl, croppedAreaPixels);
5983
+ if (!croppedBlob) {
5984
+ throw new Error("Failed to generate cropped image blob.");
5985
+ }
5986
+ const newFinalUrl = URL.createObjectURL(croppedBlob);
5987
+ if (finalImageUrl) {
5988
+ URL.revokeObjectURL(finalImageUrl);
5989
+ }
5990
+ setFinalImageUrl(newFinalUrl);
5991
+ onOpenChange(false);
5992
+ onApply(newFinalUrl, croppedBlob);
5993
+ } catch (error2) {
5994
+ onOpenChange(false);
5995
+ }
5996
+ };
5997
+ useEffect(() => {
5998
+ const currentFinalUrl = finalImageUrl;
5999
+ return () => {
6000
+ if (currentFinalUrl && currentFinalUrl.startsWith("blob:")) {
6001
+ URL.revokeObjectURL(currentFinalUrl);
6002
+ }
6003
+ };
6004
+ }, [finalImageUrl]);
6005
+ const { cropper } = useNebulaI18n().messages;
6006
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
6007
+ DialogContent,
6008
+ {
6009
+ className: "nebula-ds gap-0 p-0 sm:max-w-140 *:[button]:hidden border border-cropper-dialogBorderColor",
6010
+ portal,
6011
+ showClose: false,
6012
+ onOpenAutoFocus: (e) => e.preventDefault(),
6013
+ children: [
6014
+ /* @__PURE__ */ jsx(DialogHeader, { className: "nebula-ds contents space-y-0 text-left", children: /* @__PURE__ */ jsxs(DialogTitle, { className: "nebula-ds flex items-center justify-between p-4 text-base", children: [
6015
+ /* @__PURE__ */ jsxs("div", { className: "nebula-ds flex items-center gap-2", children: [
6016
+ /* @__PURE__ */ jsx(
6017
+ Button,
6018
+ {
6019
+ icon: true,
6020
+ type: "button",
6021
+ variant: "ghost",
6022
+ onClick: () => onOpenChange(false),
6023
+ "aria-label": "close-cropper-dialog",
6024
+ "data-testid": "close-cropper-dialog",
6025
+ size: "sm",
6026
+ children: /* @__PURE__ */ jsx(XIcon, { "aria-hidden": "true" })
6027
+ }
6028
+ ),
6029
+ cropper.dialogTitle
6030
+ ] }),
6031
+ /* @__PURE__ */ jsx(
6032
+ Button,
6033
+ {
6034
+ autoFocus: false,
6035
+ onClick: handleApply,
6036
+ disabled: !previewUrl,
6037
+ variant: "primary",
6038
+ size: "sm",
6039
+ type: "button",
6040
+ "aria-label": "apply-cropper-image",
6041
+ "data-testid": "apply-cropper-image",
6042
+ children: cropper.applyButtonLabel
6043
+ }
6044
+ )
6045
+ ] }) }),
6046
+ previewUrl && /* @__PURE__ */ jsxs(
6047
+ CropperRoot2,
6048
+ {
6049
+ className: "nebula-ds h-96 sm:h-120",
6050
+ image: previewUrl,
6051
+ zoom,
6052
+ onCropChange: handleCropChange,
6053
+ onZoomChange: setZoom,
6054
+ children: [
6055
+ /* @__PURE__ */ jsx(CropperDescription2, {}),
6056
+ /* @__PURE__ */ jsx(CropperImage2, {}),
6057
+ /* @__PURE__ */ jsx(CropperCropArea2, { rounded })
6058
+ ]
6059
+ }
6060
+ ),
6061
+ /* @__PURE__ */ jsx(DialogFooter, { className: "nebula-ds border-t border-t-cropper-dialogBorderColor px-4 py-6", children: /* @__PURE__ */ jsxs("div", { className: "nebula-ds mx-auto flex w-full max-w-80 items-center gap-4", children: [
6062
+ /* @__PURE__ */ jsx(
6063
+ MinusIcon,
6064
+ {
6065
+ className: "nebula-ds shrink-0 size-5 text-cropper-sliderIconColor",
6066
+ size: 16,
6067
+ "aria-hidden": "true"
6068
+ }
6069
+ ),
6070
+ /* @__PURE__ */ jsx(
6071
+ Slider,
6072
+ {
6073
+ defaultValue: [1],
6074
+ value: [zoom],
6075
+ min: 1,
6076
+ max: 3,
6077
+ step: 0.1,
6078
+ onValueChange: (value) => setZoom(value[0]),
6079
+ "aria-label": "Zoom slider"
6080
+ }
6081
+ ),
6082
+ /* @__PURE__ */ jsx(
6083
+ PlusIcon,
6084
+ {
6085
+ className: "nebula-ds shrink-0 size-5 text-cropper-sliderIconColor",
6086
+ size: 16,
6087
+ "aria-hidden": "true"
6088
+ }
6089
+ )
6090
+ ] }) })
6091
+ ]
6092
+ }
6093
+ ) });
6094
+ }
5037
6095
 
5038
6096
  // src/tailwind.ts
5039
6097
  function content({ base = "./" } = {}) {
@@ -5045,4 +6103,4 @@ var tailwind = {
5045
6103
  // plugin: () => require("tailwindcss")("node_modules/@nebulareact/dist/tailwind.config.js"),
5046
6104
  };
5047
6105
 
5048
- export { Accordion, AccordionContent, AccordionDescription, AccordionItem, AccordionTitle, AccordionTrigger, ActionBar, ActionBarButton, ActionBarClose, ActionBarContent, ActionBarDivider, ActionBarPortal, ActionBarTrigger, Alert, AlertButton, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, AlertTitle, StyledAsync as Async, StyledAsyncCreatable as AsyncCreatable, Badge, Box, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Calendar, Caption, Checkbox, StyledCreatable as Creatable, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FileUpload, FileUploadError, Heading, InputDatePickerSingle, InputDateTimePickerSingle, InputPhone, InputText, InputTime, Label, Link, NebulaI18nProvider, Pagination, Paragraph, Popover, PopoverContent, PopoverTrigger, ProfileImage, StyledSelect as Select, Separator2 as Separator, Skeleton, Space, SpaceDirectionEnum, SpaceSizeEnum, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TextArea, Toaster, Tooltip, alertVariants, badgeSizeEnum, badgeVariantEnum, buttonSizeEnum, buttonVariantEnum, buttonVariantsConfig, dateIsAvailable, formatBytes, getNebulaLanguage, localeByi18nKey, messages16 as messages, separatorVariants, setNebulaLanguage, tagVariantsEnum, tailwind, toast, useClickOutside, useFileUpload, useKeyPress, useNebulaI18n };
6106
+ export { Accordion, AccordionContent, AccordionDescription, AccordionItem, AccordionTitle, AccordionTrigger, ActionBar, ActionBarButton, ActionBarClose, ActionBarContent, ActionBarDivider, ActionBarPortal, ActionBarTrigger, Alert, AlertButton, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, AlertTitle, StyledAsync as Async, StyledAsyncCreatable as AsyncCreatable, Badge, Box, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Calendar, Caption, Checkbox, StyledCreatable as Creatable, Cropper2 as Cropper, CropperCropArea2 as CropperCropArea, CropperDescription2 as CropperDescription, CropperImage2 as CropperImage, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FileUpload, FileUploadError, Heading, InputDatePickerSingle, InputDateTimePickerSingle, InputPhone, InputText, InputTime, Label, Link, NebulaI18nProvider, Pagination, Paragraph, Popover, PopoverContent, PopoverTrigger, ProfileImage, StyledSelect as Select, Separator2 as Separator, Skeleton, Slider, Space, SpaceDirectionEnum, SpaceSizeEnum, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TextArea, Toaster, Tooltip, alertVariants, badgeSizeEnum, badgeVariantEnum, buttonSizeEnum, buttonVariantEnum, buttonVariantsConfig, dateIsAvailable, formatBytes, getNebulaLanguage, localeByi18nKey, messages19 as messages, separatorVariants, setNebulaLanguage, tagVariantsEnum, tailwind, toast, useClickOutside, useFileUpload, useKeyPress, useNebulaI18n };