@ikatec/nebula-react 1.0.26 → 1.0.28

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, MinusIcon, PlusIcon, UserIcon, LoaderCircleIcon, CheckIcon, 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,8 @@ 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';
31
+ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
30
32
 
31
33
  // src/button.tsx
32
34
 
@@ -1117,8 +1119,16 @@ var messages5 = {
1117
1119
  };
1118
1120
  var file_upload_default = messages5;
1119
1121
 
1122
+ // src/i18n/messages/en/cropper.ts
1123
+ var messages6 = {
1124
+ applyButtonLabel: "Apply",
1125
+ dialogTitle: "Adjust image"
1126
+ };
1127
+ var cropper_default = messages6;
1128
+
1120
1129
  // src/i18n/messages/en/index.ts
1121
1130
  var enMessages = {
1131
+ cropper: cropper_default,
1122
1132
  pagination: pagination_default,
1123
1133
  inputSelect: input_select_default,
1124
1134
  inputPhone: input_phone_default,
@@ -1127,7 +1137,7 @@ var enMessages = {
1127
1137
  };
1128
1138
 
1129
1139
  // src/i18n/messages/es/pagination.ts
1130
- var messages6 = {
1140
+ var messages7 = {
1131
1141
  totalResultsLabel(pagesSize, totalResults) {
1132
1142
  if (totalResults < pagesSize) {
1133
1143
  pagesSize = totalResults;
@@ -1138,16 +1148,16 @@ var messages6 = {
1138
1148
  return `P\xE1gina ${currentPage} de ${totalPages}`;
1139
1149
  }
1140
1150
  };
1141
- var pagination_default2 = messages6;
1151
+ var pagination_default2 = messages7;
1142
1152
 
1143
1153
  // src/i18n/messages/es/input-select.ts
1144
- var messages7 = {
1154
+ var messages8 = {
1145
1155
  noOptions: "No hay opciones disponibles"
1146
1156
  };
1147
- var input_select_default2 = messages7;
1157
+ var input_select_default2 = messages8;
1148
1158
 
1149
1159
  // src/i18n/messages/es/input-phone.ts
1150
- var messages8 = {
1160
+ var messages9 = {
1151
1161
  countries: {
1152
1162
  empty: "Seleccionar",
1153
1163
  AF: "Afganist\xE1n",
@@ -1397,22 +1407,30 @@ var messages8 = {
1397
1407
  ZW: "Zimbabue"
1398
1408
  }
1399
1409
  };
1400
- var input_phone_default2 = messages8;
1410
+ var input_phone_default2 = messages9;
1401
1411
 
1402
1412
  // src/i18n/messages/es/time-picker.ts
1403
- var messages9 = {
1413
+ var messages10 = {
1404
1414
  label: "Tiempo"
1405
1415
  };
1406
- var time_picker_default2 = messages9;
1416
+ var time_picker_default2 = messages10;
1407
1417
 
1408
1418
  // src/i18n/messages/es/file-upload.ts
1409
- var messages10 = {
1419
+ var messages11 = {
1410
1420
  deleteAll: "Remover todos"
1411
1421
  };
1412
- var file_upload_default2 = messages10;
1422
+ var file_upload_default2 = messages11;
1423
+
1424
+ // src/i18n/messages/es/cropper.ts
1425
+ var messages12 = {
1426
+ applyButtonLabel: "Aplicar",
1427
+ dialogTitle: "Ajustar la imagen"
1428
+ };
1429
+ var cropper_default2 = messages12;
1413
1430
 
1414
1431
  // src/i18n/messages/es/index.ts
1415
1432
  var esMessages = {
1433
+ cropper: cropper_default2,
1416
1434
  pagination: pagination_default2,
1417
1435
  inputSelect: input_select_default2,
1418
1436
  inputPhone: input_phone_default2,
@@ -1421,7 +1439,7 @@ var esMessages = {
1421
1439
  };
1422
1440
 
1423
1441
  // src/i18n/messages/pt-br/pagination.ts
1424
- var messages11 = {
1442
+ var messages13 = {
1425
1443
  totalResultsLabel(pagesSize, totalResults) {
1426
1444
  if (totalResults < pagesSize) {
1427
1445
  pagesSize = totalResults;
@@ -1432,16 +1450,16 @@ var messages11 = {
1432
1450
  return `P\xE1gina ${currentPage} de ${totalPages}`;
1433
1451
  }
1434
1452
  };
1435
- var pagination_default3 = messages11;
1453
+ var pagination_default3 = messages13;
1436
1454
 
1437
1455
  // src/i18n/messages/pt-br/input-select.ts
1438
- var messages12 = {
1456
+ var messages14 = {
1439
1457
  noOptions: "Nenhuma op\xE7\xE3o dispon\xEDvel"
1440
1458
  };
1441
- var input_select_default3 = messages12;
1459
+ var input_select_default3 = messages14;
1442
1460
 
1443
1461
  // src/i18n/messages/pt-br/input-phone.ts
1444
- var messages13 = {
1462
+ var messages15 = {
1445
1463
  countries: {
1446
1464
  empty: "Selecione",
1447
1465
  AF: "Afeganist\xE3o",
@@ -1691,19 +1709,26 @@ var messages13 = {
1691
1709
  ZW: "Zimb\xE1bue"
1692
1710
  }
1693
1711
  };
1694
- var input_phone_default3 = messages13;
1712
+ var input_phone_default3 = messages15;
1695
1713
 
1696
1714
  // src/i18n/messages/pt-br/time-picker.ts
1697
- var messages14 = {
1715
+ var messages16 = {
1698
1716
  label: "Hor\xE1rio"
1699
1717
  };
1700
- var time_picker_default3 = messages14;
1718
+ var time_picker_default3 = messages16;
1701
1719
 
1702
1720
  // src/i18n/messages/pt-br/file-upload.ts
1703
- var messages15 = {
1721
+ var messages17 = {
1704
1722
  deleteAll: "Remover todos"
1705
1723
  };
1706
- var file_upload_default3 = messages15;
1724
+ var file_upload_default3 = messages17;
1725
+
1726
+ // src/i18n/messages/pt-br/cropper.ts
1727
+ var messages18 = {
1728
+ applyButtonLabel: "Aplicar",
1729
+ dialogTitle: "Ajustar imagem"
1730
+ };
1731
+ var cropper_default3 = messages18;
1707
1732
 
1708
1733
  // src/i18n/messages/pt-br/index.ts
1709
1734
  var ptBrMessages = {
@@ -1711,7 +1736,8 @@ var ptBrMessages = {
1711
1736
  inputSelect: input_select_default3,
1712
1737
  inputPhone: input_phone_default3,
1713
1738
  timePicker: time_picker_default3,
1714
- fileUpload: file_upload_default3
1739
+ fileUpload: file_upload_default3,
1740
+ cropper: cropper_default3
1715
1741
  };
1716
1742
 
1717
1743
  // src/i18n/message-storage-handler.ts
@@ -1736,7 +1762,7 @@ var setNebulaLanguage = (language) => {
1736
1762
  }
1737
1763
  localStorage.setItem(getNebulaI18nStorageKey(), language);
1738
1764
  };
1739
- var messages16 = /* @__PURE__ */ new Map([
1765
+ var messages19 = /* @__PURE__ */ new Map([
1740
1766
  [null, enMessages],
1741
1767
  [void 0, enMessages],
1742
1768
  ["en-US", enMessages],
@@ -1754,14 +1780,14 @@ var NebulaI18nProvider = ({
1754
1780
  () => customI18nStorageKey ?? localStorageKey,
1755
1781
  [customI18nStorageKey]
1756
1782
  );
1757
- const [messages17, setMessages] = useState(
1758
- messages16.get(getNebulaLanguage()) ?? messages16.get("en-US")
1783
+ const [messages20, setMessages] = useState(
1784
+ messages19.get(getNebulaLanguage()) ?? messages19.get("en-US")
1759
1785
  );
1760
1786
  const handleStorageChange = useCallback(
1761
1787
  ({ detail }) => {
1762
1788
  if (detail.key === storageKey) {
1763
1789
  setMessages(
1764
- messages16.get(detail.value) ?? messages16.get("en-US")
1790
+ messages19.get(detail.value) ?? messages19.get("en-US")
1765
1791
  );
1766
1792
  }
1767
1793
  },
@@ -1805,7 +1831,7 @@ var NebulaI18nProvider = ({
1805
1831
  NebulaI18nContext.Provider,
1806
1832
  {
1807
1833
  value: {
1808
- messages: messages17,
1834
+ messages: messages20,
1809
1835
  locale: getNebulaLanguage()
1810
1836
  },
1811
1837
  children
@@ -1827,7 +1853,7 @@ var Pagination = ({
1827
1853
  onChangePage,
1828
1854
  ...props
1829
1855
  }) => {
1830
- const { messages: messages17 } = useNebulaI18n();
1856
+ const { messages: messages20 } = useNebulaI18n();
1831
1857
  const totalPages = useMemo(() => {
1832
1858
  return Math.ceil(total / (pageSize || 1));
1833
1859
  }, [total, pageSize]);
@@ -1860,13 +1886,13 @@ var Pagination = ({
1860
1886
  }, [totalPages, pageSize, total]);
1861
1887
  const totalResultsLabel = useMemo(() => {
1862
1888
  if (page === totalPages) {
1863
- return messages17.pagination.totalResultsLabel(lastPageSize, total);
1889
+ return messages20.pagination.totalResultsLabel(lastPageSize, total);
1864
1890
  }
1865
- return messages17.pagination.totalResultsLabel(pageSize, total);
1866
- }, [messages17.pagination, pageSize, total, page, totalPages, lastPageSize]);
1891
+ return messages20.pagination.totalResultsLabel(pageSize, total);
1892
+ }, [messages20.pagination, pageSize, total, page, totalPages, lastPageSize]);
1867
1893
  const currentPageLabel = useMemo(
1868
- () => messages17.pagination.currentPageLabel(normalizedPage, totalPages),
1869
- [messages17.pagination, normalizedPage, totalPages]
1894
+ () => messages20.pagination.currentPageLabel(normalizedPage, totalPages),
1895
+ [messages20.pagination, normalizedPage, totalPages]
1870
1896
  );
1871
1897
  return /* @__PURE__ */ jsxs(
1872
1898
  "nav",
@@ -2139,7 +2165,7 @@ var createStyledSelect = (BaseSelect, displayName) => {
2139
2165
  isError = false,
2140
2166
  ...props
2141
2167
  }) => {
2142
- const { messages: messages17 } = useNebulaI18n();
2168
+ const { messages: messages20 } = useNebulaI18n();
2143
2169
  const customClassNames = useMemo(() => {
2144
2170
  return {
2145
2171
  control: (props2) => controlStyles(props2, isError),
@@ -2186,7 +2212,7 @@ var createStyledSelect = (BaseSelect, displayName) => {
2186
2212
  isDisabled: disabled,
2187
2213
  components: customComponents,
2188
2214
  classNames: customClassNames,
2189
- noOptionsMessage: () => /* @__PURE__ */ jsx("p", { children: messages17.inputSelect.noOptions }),
2215
+ noOptionsMessage: () => /* @__PURE__ */ jsx("p", { children: messages20.inputSelect.noOptions }),
2190
2216
  ...props
2191
2217
  }
2192
2218
  );
@@ -2216,12 +2242,13 @@ var InputText = React8.forwardRef(
2216
2242
  "div",
2217
2243
  {
2218
2244
  className: cn(
2219
- "w-full flex outline-none",
2245
+ "flex outline-none",
2220
2246
  "rounded-input",
2221
2247
  "border border-inputText-border-default focus-within:ring-[3px] focus-within:ring-inputText-border-focus focus-within:border-inputText-border-focus",
2222
2248
  "focus-within:text-inputText-text-focus placeholder:text-inputText-text-default disabled:text-inputText-text-disabled",
2223
2249
  isError && "border-inputText-border-danger focus-within:border-inputText-border-danger focus-within:ring-button-danger-border-focus",
2224
- disabled && "pointer-events-none"
2250
+ disabled && "pointer-events-none",
2251
+ className
2225
2252
  ),
2226
2253
  children: [
2227
2254
  /* @__PURE__ */ jsxs("div", { className: "nebula-ds relative w-full", children: [
@@ -2230,7 +2257,7 @@ var InputText = React8.forwardRef(
2230
2257
  {
2231
2258
  ref,
2232
2259
  className: cn(
2233
- "w-full h-10 outline-none text-sm leading-none font-medium",
2260
+ "h-10 outline-none text-sm leading-none font-medium",
2234
2261
  "bg-inputText-background-default disabled:bg-inputText-background-disabled",
2235
2262
  "text-inputText-text-filled",
2236
2263
  "disabled:cursor-not-allowed",
@@ -2242,7 +2269,7 @@ var InputText = React8.forwardRef(
2242
2269
  "pr-10": initialInputType === "password",
2243
2270
  "pr-10 pl-4": !!icon && iconPlacement === "end" && initialInputType !== "password"
2244
2271
  },
2245
- className
2272
+ "!w-full"
2246
2273
  ),
2247
2274
  ...{ ...props, icon: void 0, iconPlacement: void 0 },
2248
2275
  disabled,
@@ -2332,16 +2359,17 @@ var DialogOverlay = React8.forwardRef(({ className, ...props }, ref) => /* @__PU
2332
2359
  }
2333
2360
  ));
2334
2361
  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
2362
+ var DialogContent = React8.forwardRef(
2363
+ ({ className, children, portal = false, showClose = true, ...props }, ref) => {
2364
+ const Comp = portal ? DialogPortal : React8.Fragment;
2365
+ return /* @__PURE__ */ jsxs(Comp, { children: [
2366
+ /* @__PURE__ */ jsx(DialogOverlay, {}),
2367
+ /* @__PURE__ */ jsxs(
2368
+ DialogPrimitive.Content,
2369
+ {
2370
+ ref,
2371
+ className: cn(
2372
+ `rounded-2xl
2345
2373
  fixed
2346
2374
  left-[50%]
2347
2375
  top-[50%]
@@ -2366,29 +2394,30 @@ var DialogContent = React8.forwardRef(({ className, children, portal = false, ..
2366
2394
  data-[state=closed]:slide-out-to-top-[48%]
2367
2395
  data-[state=open]:slide-in-from-left-1/2
2368
2396
  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
- });
2397
+ className
2398
+ ),
2399
+ ...props,
2400
+ children: [
2401
+ children,
2402
+ showClose && /* @__PURE__ */ jsx(
2403
+ DialogPrimitive.Close,
2404
+ {
2405
+ asChild: true,
2406
+ className: `absolute
2407
+ right-4
2408
+ top-4`,
2409
+ children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", icon: true, children: [
2410
+ /* @__PURE__ */ jsx(X, { className: "nebula-ds !h-4 !w-4 !text-dialog-icon" }),
2411
+ /* @__PURE__ */ jsx("span", { className: "nebula-ds sr-only", children: "Close" })
2412
+ ] })
2413
+ }
2414
+ )
2415
+ ]
2416
+ }
2417
+ )
2418
+ ] });
2419
+ }
2420
+ );
2392
2421
  DialogContent.displayName = DialogPrimitive.Content.displayName;
2393
2422
  var DialogHeader = ({
2394
2423
  className,
@@ -3204,13 +3233,13 @@ function custom(message, options) {
3204
3233
  }
3205
3234
  );
3206
3235
  }
3207
- async function promise(promise2, messages17, options) {
3236
+ async function promise(promise2, messages20, options) {
3208
3237
  const loadingToast = toast$1.custom(
3209
3238
  (t) => /* @__PURE__ */ jsx(
3210
3239
  ToastComponent,
3211
3240
  {
3212
3241
  type: "info",
3213
- message: messages17.loading,
3242
+ message: messages20.loading,
3214
3243
  options,
3215
3244
  t
3216
3245
  }
@@ -3224,7 +3253,7 @@ async function promise(promise2, messages17, options) {
3224
3253
  ToastComponent,
3225
3254
  {
3226
3255
  type: "success",
3227
- message: messages17.success,
3256
+ message: messages20.success,
3228
3257
  options,
3229
3258
  t
3230
3259
  }
@@ -3239,7 +3268,7 @@ async function promise(promise2, messages17, options) {
3239
3268
  ToastComponent,
3240
3269
  {
3241
3270
  type: "error",
3242
- message: messages17.error,
3271
+ message: messages20.error,
3243
3272
  options,
3244
3273
  t
3245
3274
  }
@@ -3409,8 +3438,8 @@ var CountrySelect = ({
3409
3438
  const handleSelect = (event) => {
3410
3439
  onChange(event.target.value);
3411
3440
  };
3412
- const { messages: messages17 } = useNebulaI18n();
3413
- const { countries } = messages17.inputPhone;
3441
+ const { messages: messages20 } = useNebulaI18n();
3442
+ const { countries } = messages20.inputPhone;
3414
3443
  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
3444
  /* @__PURE__ */ jsxs("div", { className: "nebula-ds inline-flex items-center gap-1", "aria-hidden": "true", children: [
3416
3445
  /* @__PURE__ */ jsx(FlagComponent, { country: value, countryName: value, "aria-hidden": "true" }),
@@ -3449,10 +3478,11 @@ var PhoneInput = React8__default.forwardRef(({ className, ...props }, ref) => {
3449
3478
  "data-slot": "phone-input",
3450
3479
  ref,
3451
3480
  className: cn(
3452
- "-ms-px rounded-s-none shadow-none focus-visible:z-10 ring-0 focus:ring-0 border-0 w-full h-auto",
3481
+ "-ms-px rounded-s-none shadow-none focus-visible:z-10 ring-0 focus-within:ring-0 border-0 w-full h-auto",
3453
3482
  className
3454
3483
  ),
3455
- ...props
3484
+ ...props,
3485
+ type: "text"
3456
3486
  }
3457
3487
  )
3458
3488
  ] });
@@ -4230,7 +4260,7 @@ var InputDateTimePickerSingle = ({
4230
4260
  formattedDateByLanguage ? new Date(formattedDateByLanguage) : void 0
4231
4261
  );
4232
4262
  const [popoverIsOpen, setPopoverIsOpen] = useState(false);
4233
- const { locale, messages: messages17 } = useNebulaI18n();
4263
+ const { locale, messages: messages20 } = useNebulaI18n();
4234
4264
  const [month, setMonth] = useState(/* @__PURE__ */ new Date());
4235
4265
  const inputTimeRef = useRef(null);
4236
4266
  const handleClearValue = () => {
@@ -4402,7 +4432,7 @@ var InputDateTimePickerSingle = ({
4402
4432
  disabled: disabledDates,
4403
4433
  footer: /* @__PURE__ */ jsxs(Space, { className: "nebula-ds items-center", children: [
4404
4434
  /* @__PURE__ */ jsxs(Label, { children: [
4405
- messages17.timePicker.label,
4435
+ messages20.timePicker.label,
4406
4436
  ":"
4407
4437
  ] }),
4408
4438
  /* @__PURE__ */ jsx(
@@ -4938,102 +4968,1587 @@ var TextArea = React8.forwardRef(
4938
4968
  }
4939
4969
  );
4940
4970
  TextArea.displayName = "TextArea";
4941
- var ProfileImage = ({
4942
- maxSizeMB = 2,
4943
- subtitle,
4944
- onError,
4945
- maxFiles = Infinity,
4946
- onRemove,
4947
- ...rest
4971
+ function clamp(value, min, max) {
4972
+ return Math.min(Math.max(value, min), max);
4973
+ }
4974
+ var CropperContext = createContext(null);
4975
+ var useCropperContext = () => {
4976
+ const context = useContext(CropperContext);
4977
+ if (!context) {
4978
+ throw new Error("useCropperContext must be used within a Cropper.Root");
4979
+ }
4980
+ return context;
4981
+ };
4982
+ var CropperRoot = ({
4983
+ image,
4984
+ cropPadding = 25,
4985
+ aspectRatio = 1,
4986
+ minZoom = 1,
4987
+ maxZoom = 3,
4988
+ zoomSensitivity = 5e-3,
4989
+ keyboardStep = 10,
4990
+ className,
4991
+ style,
4992
+ zoom: zoomProp,
4993
+ onCropChange,
4994
+ onZoomChange,
4995
+ children,
4996
+ ...restProps
4948
4997
  }) => {
4949
- const maxSize = maxSizeMB * 1024 * 1024;
4950
- const [
4951
- { files, isDragging, errors },
4952
- {
4953
- handleDragEnter,
4954
- handleDragLeave,
4955
- handleDragOver,
4956
- handleDrop,
4957
- openFileDialog,
4958
- removeFile,
4959
- getInputProps
4998
+ const descriptionId = useId();
4999
+ const [imgWidth, setImgWidth] = useState(null);
5000
+ const [imgHeight, setImgHeight] = useState(null);
5001
+ const containerRef = useRef(null);
5002
+ const [cropAreaWidth, setCropAreaWidth] = useState(0);
5003
+ const [cropAreaHeight, setCropAreaHeight] = useState(0);
5004
+ const [imageWrapperWidth, setImageWrapperWidth] = useState(0);
5005
+ const [imageWrapperHeight, setImageWrapperHeight] = useState(0);
5006
+ const [offsetX, setOffsetX] = useState(0);
5007
+ const [offsetY, setOffsetY] = useState(0);
5008
+ const [internalZoom, setInternalZoom] = useState(minZoom);
5009
+ const [isDragging, setIsDragging] = useState(false);
5010
+ const dragStartPointRef = useRef({ x: 0, y: 0 });
5011
+ const dragStartOffsetRef = useRef({ x: 0, y: 0 });
5012
+ const latestRestrictedOffsetRef = useRef({
5013
+ x: offsetX,
5014
+ y: offsetY
5015
+ });
5016
+ const latestZoomRef = useRef(internalZoom);
5017
+ const isInitialSetupDoneRef = useRef(false);
5018
+ const initialPinchDistanceRef = useRef(0);
5019
+ const initialPinchZoomRef = useRef(1);
5020
+ const isPinchingRef = useRef(false);
5021
+ const hasWarnedRef = useRef(false);
5022
+ const isZoomControlled = zoomProp !== void 0;
5023
+ const effectiveZoom = isZoomControlled ? zoomProp : internalZoom;
5024
+ const updateZoom = useCallback(
5025
+ (newZoomValue) => {
5026
+ const clampedZoom = clamp(newZoomValue, minZoom, maxZoom);
5027
+ if (onZoomChange) {
5028
+ onZoomChange(clampedZoom);
5029
+ } else if (!isZoomControlled) {
5030
+ setInternalZoom(clampedZoom);
5031
+ }
5032
+ return clampedZoom;
5033
+ },
5034
+ [minZoom, maxZoom, onZoomChange, isZoomControlled]
5035
+ );
5036
+ useEffect(() => {
5037
+ latestZoomRef.current = effectiveZoom;
5038
+ }, [effectiveZoom]);
5039
+ useEffect(() => {
5040
+ setOffsetX(0);
5041
+ setOffsetY(0);
5042
+ if (!isZoomControlled) {
5043
+ setInternalZoom(minZoom);
4960
5044
  }
4961
- ] = useFileUpload({
4962
- multiple: false,
4963
- maxSize: maxSize > 0 ? maxSize : void 0,
4964
- accept: "image/*",
4965
- ...rest,
4966
- maxFiles
5045
+ isInitialSetupDoneRef.current = false;
5046
+ if (!image) {
5047
+ setImgWidth(null);
5048
+ setImgHeight(null);
5049
+ return;
5050
+ }
5051
+ let isMounted = true;
5052
+ const img = new Image();
5053
+ img.onload = () => {
5054
+ if (isMounted) {
5055
+ setImgWidth(img.naturalWidth);
5056
+ setImgHeight(img.naturalHeight);
5057
+ }
5058
+ };
5059
+ img.onerror = () => {
5060
+ if (isMounted) {
5061
+ setImgWidth(null);
5062
+ setImgHeight(null);
5063
+ }
5064
+ };
5065
+ img.src = image;
5066
+ return () => {
5067
+ isMounted = false;
5068
+ };
5069
+ }, [image, minZoom, isZoomControlled]);
5070
+ const updateCropAreaDimensions = useCallback(
5071
+ (containerWidth, containerHeight) => {
5072
+ if (containerWidth <= 0 || containerHeight <= 0) {
5073
+ setCropAreaWidth(0);
5074
+ setCropAreaHeight(0);
5075
+ return;
5076
+ }
5077
+ const maxPossibleWidth = Math.max(0, containerWidth - cropPadding * 2);
5078
+ const maxPossibleHeight = Math.max(0, containerHeight - cropPadding * 2);
5079
+ let targetCropW = 0;
5080
+ let targetCropH = 0;
5081
+ if (maxPossibleWidth / aspectRatio >= maxPossibleHeight) {
5082
+ targetCropH = maxPossibleHeight;
5083
+ targetCropW = targetCropH * aspectRatio;
5084
+ } else {
5085
+ targetCropW = maxPossibleWidth;
5086
+ targetCropH = targetCropW / aspectRatio;
5087
+ }
5088
+ setCropAreaWidth(targetCropW);
5089
+ setCropAreaHeight(targetCropH);
5090
+ },
5091
+ [aspectRatio, cropPadding]
5092
+ );
5093
+ useEffect(() => {
5094
+ const element = containerRef.current;
5095
+ if (!element) return;
5096
+ const observer = new ResizeObserver((entries) => {
5097
+ for (const entry of entries) {
5098
+ const { width, height } = entry.contentRect;
5099
+ if (width > 0 && height > 0) updateCropAreaDimensions(width, height);
5100
+ }
5101
+ });
5102
+ observer.observe(element);
5103
+ const initialWidth = element.clientWidth;
5104
+ const initialHeight = element.clientHeight;
5105
+ if (initialWidth > 0 && initialHeight > 0)
5106
+ updateCropAreaDimensions(initialWidth, initialHeight);
5107
+ return () => observer.disconnect();
5108
+ }, [updateCropAreaDimensions]);
5109
+ useEffect(() => {
5110
+ if (cropAreaWidth <= 0 || cropAreaHeight <= 0 || !imgWidth || !imgHeight) {
5111
+ setImageWrapperWidth(0);
5112
+ setImageWrapperHeight(0);
5113
+ return;
5114
+ }
5115
+ const naturalAspect = imgWidth / imgHeight;
5116
+ const cropAspect = cropAreaWidth / cropAreaHeight;
5117
+ let targetWrapperWidth = 0;
5118
+ let targetWrapperHeight = 0;
5119
+ if (naturalAspect >= cropAspect) {
5120
+ targetWrapperHeight = cropAreaHeight;
5121
+ targetWrapperWidth = targetWrapperHeight * naturalAspect;
5122
+ } else {
5123
+ targetWrapperWidth = cropAreaWidth;
5124
+ targetWrapperHeight = targetWrapperWidth / naturalAspect;
5125
+ }
5126
+ setImageWrapperWidth(targetWrapperWidth);
5127
+ setImageWrapperHeight(targetWrapperHeight);
5128
+ }, [cropAreaWidth, cropAreaHeight, imgWidth, imgHeight]);
5129
+ const restrictOffset = useCallback(
5130
+ (dragOffsetX, dragOffsetY, currentZoom) => {
5131
+ if (imageWrapperWidth <= 0 || imageWrapperHeight <= 0 || cropAreaWidth <= 0 || cropAreaHeight <= 0)
5132
+ return { x: 0, y: 0 };
5133
+ const effectiveWrapperWidth = imageWrapperWidth * currentZoom;
5134
+ const effectiveWrapperHeight = imageWrapperHeight * currentZoom;
5135
+ const maxDragX = Math.max(0, (effectiveWrapperWidth - cropAreaWidth) / 2);
5136
+ const maxDragY = Math.max(
5137
+ 0,
5138
+ (effectiveWrapperHeight - cropAreaHeight) / 2
5139
+ );
5140
+ return {
5141
+ x: clamp(dragOffsetX, -maxDragX, maxDragX),
5142
+ y: clamp(dragOffsetY, -maxDragY, maxDragY)
5143
+ };
5144
+ },
5145
+ [imageWrapperWidth, imageWrapperHeight, cropAreaWidth, cropAreaHeight]
5146
+ );
5147
+ const calculateCropData = useCallback(
5148
+ (finalOffsetX, finalOffsetY, finalZoom) => {
5149
+ const currentOffsetX = finalOffsetX !== void 0 ? finalOffsetX : latestRestrictedOffsetRef.current.x;
5150
+ const currentOffsetY = finalOffsetY !== void 0 ? finalOffsetY : latestRestrictedOffsetRef.current.y;
5151
+ const currentZoom = finalZoom !== void 0 ? finalZoom : effectiveZoom;
5152
+ if (!imgWidth || !imgHeight || imageWrapperWidth <= 0 || imageWrapperHeight <= 0 || cropAreaWidth <= 0 || cropAreaHeight <= 0)
5153
+ return null;
5154
+ const scaledWrapperWidth = imageWrapperWidth * currentZoom;
5155
+ const scaledWrapperHeight = imageWrapperHeight * currentZoom;
5156
+ const topLeftOffsetX = currentOffsetX + (cropAreaWidth - scaledWrapperWidth) / 2;
5157
+ const topLeftOffsetY = currentOffsetY + (cropAreaHeight - scaledWrapperHeight) / 2;
5158
+ const baseScale = imgWidth / imageWrapperWidth;
5159
+ if (isNaN(baseScale) || baseScale === 0) return null;
5160
+ const sx = -topLeftOffsetX * baseScale / currentZoom;
5161
+ const sy = -topLeftOffsetY * baseScale / currentZoom;
5162
+ const sWidth = cropAreaWidth * baseScale / currentZoom;
5163
+ const sHeight = cropAreaHeight * baseScale / currentZoom;
5164
+ const finalX = clamp(Math.round(sx), 0, imgWidth);
5165
+ const finalY = clamp(Math.round(sy), 0, imgHeight);
5166
+ const finalWidth = clamp(Math.round(sWidth), 0, imgWidth - finalX);
5167
+ const finalHeight = clamp(Math.round(sHeight), 0, imgHeight - finalY);
5168
+ if (finalWidth <= 0 || finalHeight <= 0) return null;
5169
+ return { x: finalX, y: finalY, width: finalWidth, height: finalHeight };
5170
+ },
5171
+ [
5172
+ imgWidth,
5173
+ imgHeight,
5174
+ imageWrapperWidth,
5175
+ imageWrapperHeight,
5176
+ cropAreaWidth,
5177
+ cropAreaHeight,
5178
+ effectiveZoom
5179
+ ]
5180
+ );
5181
+ useEffect(() => {
5182
+ if (imageWrapperWidth > 0 && imageWrapperHeight > 0 && cropAreaWidth > 0 && cropAreaHeight > 0) {
5183
+ const currentZoomForSetup = effectiveZoom;
5184
+ if (!isInitialSetupDoneRef.current) {
5185
+ const initialX = 0;
5186
+ const initialY = 0;
5187
+ const restrictedInitial = restrictOffset(
5188
+ initialX,
5189
+ initialY,
5190
+ currentZoomForSetup
5191
+ );
5192
+ setOffsetX(restrictedInitial.x);
5193
+ setOffsetY(restrictedInitial.y);
5194
+ if (!isZoomControlled) setInternalZoom(currentZoomForSetup);
5195
+ dragStartOffsetRef.current = restrictedInitial;
5196
+ latestRestrictedOffsetRef.current = restrictedInitial;
5197
+ latestZoomRef.current = currentZoomForSetup;
5198
+ if (onCropChange)
5199
+ onCropChange(
5200
+ calculateCropData(
5201
+ restrictedInitial.x,
5202
+ restrictedInitial.y,
5203
+ currentZoomForSetup
5204
+ )
5205
+ );
5206
+ isInitialSetupDoneRef.current = true;
5207
+ } else {
5208
+ const restrictedCurrent = restrictOffset(
5209
+ latestRestrictedOffsetRef.current.x,
5210
+ latestRestrictedOffsetRef.current.y,
5211
+ currentZoomForSetup
5212
+ );
5213
+ if (restrictedCurrent.x !== latestRestrictedOffsetRef.current.x || restrictedCurrent.y !== latestRestrictedOffsetRef.current.y) {
5214
+ setOffsetX(restrictedCurrent.x);
5215
+ setOffsetY(restrictedCurrent.y);
5216
+ latestRestrictedOffsetRef.current = restrictedCurrent;
5217
+ dragStartOffsetRef.current = restrictedCurrent;
5218
+ }
5219
+ if (onCropChange)
5220
+ onCropChange(
5221
+ calculateCropData(
5222
+ restrictedCurrent.x,
5223
+ restrictedCurrent.y,
5224
+ currentZoomForSetup
5225
+ )
5226
+ );
5227
+ }
5228
+ } else {
5229
+ isInitialSetupDoneRef.current = false;
5230
+ setOffsetX(0);
5231
+ setOffsetY(0);
5232
+ if (!isZoomControlled) setInternalZoom(minZoom);
5233
+ dragStartOffsetRef.current = { x: 0, y: 0 };
5234
+ latestRestrictedOffsetRef.current = { x: 0, y: 0 };
5235
+ latestZoomRef.current = effectiveZoom;
5236
+ if (onCropChange) onCropChange(null);
5237
+ }
5238
+ }, [
5239
+ imageWrapperWidth,
5240
+ imgHeight,
5241
+ cropAreaWidth,
5242
+ cropAreaHeight,
5243
+ restrictOffset,
5244
+ onCropChange,
5245
+ calculateCropData,
5246
+ minZoom,
5247
+ effectiveZoom,
5248
+ isZoomControlled,
5249
+ updateZoom,
5250
+ imageWrapperHeight
5251
+ ]);
5252
+ useEffect(() => {
5253
+ const checkTimeout = setTimeout(() => {
5254
+ if (containerRef.current && !hasWarnedRef.current) {
5255
+ const hasDescription = document.getElementById(descriptionId);
5256
+ if (!hasDescription) {
5257
+ hasWarnedRef.current = true;
5258
+ }
5259
+ }
5260
+ }, 100);
5261
+ return () => clearTimeout(checkTimeout);
5262
+ }, [descriptionId]);
5263
+ const handleInteractionEnd = useCallback(() => {
5264
+ if (onCropChange) {
5265
+ const finalData = calculateCropData(
5266
+ latestRestrictedOffsetRef.current.x,
5267
+ latestRestrictedOffsetRef.current.y,
5268
+ effectiveZoom
5269
+ );
5270
+ onCropChange(finalData);
5271
+ }
5272
+ }, [onCropChange, calculateCropData, effectiveZoom]);
5273
+ const handleMouseDown = useCallback(
5274
+ (e) => {
5275
+ if (e.button !== 0 || !containerRef.current) return;
5276
+ e.preventDefault();
5277
+ setIsDragging(true);
5278
+ isPinchingRef.current = false;
5279
+ dragStartPointRef.current = { x: e.clientX, y: e.clientY };
5280
+ dragStartOffsetRef.current = {
5281
+ x: latestRestrictedOffsetRef.current.x,
5282
+ y: latestRestrictedOffsetRef.current.y
5283
+ };
5284
+ const handleMouseMove = (ev) => {
5285
+ const deltaX = ev.clientX - dragStartPointRef.current.x;
5286
+ const deltaY = ev.clientY - dragStartPointRef.current.y;
5287
+ const targetOffsetX = dragStartOffsetRef.current.x + deltaX;
5288
+ const targetOffsetY = dragStartOffsetRef.current.y + deltaY;
5289
+ const restricted = restrictOffset(
5290
+ targetOffsetX,
5291
+ targetOffsetY,
5292
+ effectiveZoom
5293
+ );
5294
+ latestRestrictedOffsetRef.current = restricted;
5295
+ setOffsetX(restricted.x);
5296
+ setOffsetY(restricted.y);
5297
+ };
5298
+ const handleMouseUp = () => {
5299
+ setIsDragging(false);
5300
+ window.removeEventListener("mousemove", handleMouseMove);
5301
+ window.removeEventListener("mouseup", handleMouseUp);
5302
+ handleInteractionEnd();
5303
+ };
5304
+ window.addEventListener("mousemove", handleMouseMove);
5305
+ window.addEventListener("mouseup", handleMouseUp);
5306
+ },
5307
+ [restrictOffset, effectiveZoom, handleInteractionEnd]
5308
+ );
5309
+ const handleWheel = useCallback(
5310
+ (e) => {
5311
+ e.preventDefault();
5312
+ e.stopPropagation();
5313
+ if (!containerRef.current || imageWrapperWidth <= 0 || imageWrapperHeight <= 0)
5314
+ return;
5315
+ const currentZoom = latestZoomRef.current;
5316
+ const currentOffsetX = latestRestrictedOffsetRef.current.x;
5317
+ const currentOffsetY = latestRestrictedOffsetRef.current.y;
5318
+ const delta = e.deltaY * -zoomSensitivity;
5319
+ const targetZoom = currentZoom + delta;
5320
+ if (clamp(targetZoom, minZoom, maxZoom) === currentZoom) return;
5321
+ const rect = containerRef.current.getBoundingClientRect();
5322
+ const pointerX = e.clientX - rect.left - rect.width / 2;
5323
+ const pointerY = e.clientY - rect.top - rect.height / 2;
5324
+ const imagePointX = (pointerX - currentOffsetX) / currentZoom;
5325
+ const imagePointY = (pointerY - currentOffsetY) / currentZoom;
5326
+ const finalNewZoom = updateZoom(targetZoom);
5327
+ const newOffsetX = pointerX - imagePointX * finalNewZoom;
5328
+ const newOffsetY = pointerY - imagePointY * finalNewZoom;
5329
+ const restrictedNewOffset = restrictOffset(
5330
+ newOffsetX,
5331
+ newOffsetY,
5332
+ finalNewZoom
5333
+ );
5334
+ setOffsetX(restrictedNewOffset.x);
5335
+ setOffsetY(restrictedNewOffset.y);
5336
+ latestRestrictedOffsetRef.current = restrictedNewOffset;
5337
+ if (onCropChange) {
5338
+ const finalData = calculateCropData(
5339
+ restrictedNewOffset.x,
5340
+ restrictedNewOffset.y,
5341
+ finalNewZoom
5342
+ );
5343
+ onCropChange(finalData);
5344
+ }
5345
+ },
5346
+ [
5347
+ restrictOffset,
5348
+ calculateCropData,
5349
+ imageWrapperWidth,
5350
+ imageWrapperHeight,
5351
+ onCropChange,
5352
+ minZoom,
5353
+ maxZoom,
5354
+ zoomSensitivity,
5355
+ updateZoom
5356
+ ]
5357
+ );
5358
+ const getPinchDistance = (touches) => Math.sqrt(
5359
+ Math.pow(touches[1].clientX - touches[0].clientX, 2) + Math.pow(touches[1].clientY - touches[0].clientY, 2)
5360
+ );
5361
+ const getPinchCenter = (touches) => ({
5362
+ x: (touches[0].clientX + touches[1].clientX) / 2,
5363
+ y: (touches[0].clientY + touches[1].clientY) / 2
4967
5364
  });
5365
+ const handleTouchStart = useCallback(
5366
+ (e) => {
5367
+ if (!containerRef.current || imageWrapperWidth <= 0 || imageWrapperHeight <= 0)
5368
+ return;
5369
+ e.preventDefault();
5370
+ const touches = e.touches;
5371
+ if (touches.length === 1) {
5372
+ setIsDragging(true);
5373
+ isPinchingRef.current = false;
5374
+ dragStartPointRef.current = {
5375
+ x: touches[0].clientX,
5376
+ y: touches[0].clientY
5377
+ };
5378
+ dragStartOffsetRef.current = {
5379
+ x: latestRestrictedOffsetRef.current.x,
5380
+ y: latestRestrictedOffsetRef.current.y
5381
+ };
5382
+ } else if (touches.length === 2) {
5383
+ setIsDragging(false);
5384
+ isPinchingRef.current = true;
5385
+ initialPinchDistanceRef.current = getPinchDistance(touches);
5386
+ initialPinchZoomRef.current = latestZoomRef.current;
5387
+ dragStartOffsetRef.current = {
5388
+ x: latestRestrictedOffsetRef.current.x,
5389
+ y: latestRestrictedOffsetRef.current.y
5390
+ };
5391
+ }
5392
+ },
5393
+ [imageWrapperWidth, imageWrapperHeight]
5394
+ );
5395
+ const handleTouchMove = useCallback(
5396
+ (e) => {
5397
+ if (!containerRef.current || imageWrapperWidth <= 0 || imageWrapperHeight <= 0)
5398
+ return;
5399
+ e.preventDefault();
5400
+ const touches = e.touches;
5401
+ if (touches.length === 1 && isDragging && !isPinchingRef.current) {
5402
+ const deltaX = touches[0].clientX - dragStartPointRef.current.x;
5403
+ const deltaY = touches[0].clientY - dragStartPointRef.current.y;
5404
+ const targetOffsetX = dragStartOffsetRef.current.x + deltaX;
5405
+ const targetOffsetY = dragStartOffsetRef.current.y + deltaY;
5406
+ const restricted = restrictOffset(
5407
+ targetOffsetX,
5408
+ targetOffsetY,
5409
+ effectiveZoom
5410
+ );
5411
+ latestRestrictedOffsetRef.current = restricted;
5412
+ setOffsetX(restricted.x);
5413
+ setOffsetY(restricted.y);
5414
+ } else if (touches.length === 2 && isPinchingRef.current) {
5415
+ const currentPinchDistance = getPinchDistance(touches);
5416
+ const scale = currentPinchDistance / initialPinchDistanceRef.current;
5417
+ const currentZoom = initialPinchZoomRef.current;
5418
+ const targetZoom = currentZoom * scale;
5419
+ if (clamp(targetZoom, minZoom, maxZoom) === latestZoomRef.current)
5420
+ return;
5421
+ const pinchCenter = getPinchCenter(touches);
5422
+ const rect = containerRef.current.getBoundingClientRect();
5423
+ const pinchCenterX = pinchCenter.x - rect.left - rect.width / 2;
5424
+ const pinchCenterY = pinchCenter.y - rect.top - rect.height / 2;
5425
+ const currentOffsetX = dragStartOffsetRef.current.x;
5426
+ const currentOffsetY = dragStartOffsetRef.current.y;
5427
+ const imagePointX = (pinchCenterX - currentOffsetX) / currentZoom;
5428
+ const imagePointY = (pinchCenterY - currentOffsetY) / currentZoom;
5429
+ const finalNewZoom = updateZoom(targetZoom);
5430
+ const newOffsetX = pinchCenterX - imagePointX * finalNewZoom;
5431
+ const newOffsetY = pinchCenterY - imagePointY * finalNewZoom;
5432
+ const restrictedNewOffset = restrictOffset(
5433
+ newOffsetX,
5434
+ newOffsetY,
5435
+ finalNewZoom
5436
+ );
5437
+ setOffsetX(restrictedNewOffset.x);
5438
+ setOffsetY(restrictedNewOffset.y);
5439
+ latestRestrictedOffsetRef.current = restrictedNewOffset;
5440
+ if (onCropChange) {
5441
+ const finalData = calculateCropData(
5442
+ restrictedNewOffset.x,
5443
+ restrictedNewOffset.y,
5444
+ finalNewZoom
5445
+ );
5446
+ onCropChange(finalData);
5447
+ }
5448
+ }
5449
+ },
5450
+ [
5451
+ isDragging,
5452
+ restrictOffset,
5453
+ minZoom,
5454
+ maxZoom,
5455
+ imageWrapperWidth,
5456
+ imageWrapperHeight,
5457
+ effectiveZoom,
5458
+ updateZoom,
5459
+ onCropChange,
5460
+ calculateCropData
5461
+ ]
5462
+ );
5463
+ const handleTouchEnd = useCallback(
5464
+ (e) => {
5465
+ e.preventDefault();
5466
+ const touches = e.touches;
5467
+ if (isPinchingRef.current && touches.length < 2) {
5468
+ isPinchingRef.current = false;
5469
+ if (touches.length === 1) {
5470
+ setIsDragging(true);
5471
+ dragStartPointRef.current = {
5472
+ x: touches[0].clientX,
5473
+ y: touches[0].clientY
5474
+ };
5475
+ dragStartOffsetRef.current = {
5476
+ x: latestRestrictedOffsetRef.current.x,
5477
+ y: latestRestrictedOffsetRef.current.y
5478
+ };
5479
+ } else {
5480
+ setIsDragging(false);
5481
+ handleInteractionEnd();
5482
+ }
5483
+ } else if (isDragging && touches.length === 0) {
5484
+ setIsDragging(false);
5485
+ handleInteractionEnd();
5486
+ }
5487
+ },
5488
+ [isDragging, handleInteractionEnd]
5489
+ );
5490
+ const handleKeyDown = useCallback(
5491
+ (e) => {
5492
+ if (imageWrapperWidth <= 0) return;
5493
+ let targetOffsetX = latestRestrictedOffsetRef.current.x;
5494
+ let targetOffsetY = latestRestrictedOffsetRef.current.y;
5495
+ let moved = false;
5496
+ switch (e.key) {
5497
+ case "ArrowUp":
5498
+ targetOffsetY += keyboardStep;
5499
+ moved = true;
5500
+ break;
5501
+ case "ArrowDown":
5502
+ targetOffsetY -= keyboardStep;
5503
+ moved = true;
5504
+ break;
5505
+ case "ArrowLeft":
5506
+ targetOffsetX += keyboardStep;
5507
+ moved = true;
5508
+ break;
5509
+ case "ArrowRight":
5510
+ targetOffsetX -= keyboardStep;
5511
+ moved = true;
5512
+ break;
5513
+ default:
5514
+ return;
5515
+ }
5516
+ if (moved) {
5517
+ e.preventDefault();
5518
+ const restricted = restrictOffset(
5519
+ targetOffsetX,
5520
+ targetOffsetY,
5521
+ effectiveZoom
5522
+ );
5523
+ if (restricted.x !== latestRestrictedOffsetRef.current.x || restricted.y !== latestRestrictedOffsetRef.current.y) {
5524
+ latestRestrictedOffsetRef.current = restricted;
5525
+ setOffsetX(restricted.x);
5526
+ setOffsetY(restricted.y);
5527
+ }
5528
+ }
5529
+ },
5530
+ [keyboardStep, imageWrapperWidth, restrictOffset, effectiveZoom]
5531
+ );
5532
+ const handleKeyUp = useCallback(
5533
+ (e) => {
5534
+ if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
5535
+ handleInteractionEnd();
5536
+ }
5537
+ },
5538
+ [handleInteractionEnd]
5539
+ );
4968
5540
  useEffect(() => {
4969
- onError?.(errors);
4970
- }, [errors, onError]);
4971
- const [file] = files;
4972
- return /* @__PURE__ */ jsxs("div", { className: "nebula-ds flex flex-col gap-3", children: [
4973
- /* @__PURE__ */ jsx("div", { className: "nebula-ds flex justify-center", children: /* @__PURE__ */ jsxs(
4974
- "div",
5541
+ const node = containerRef.current;
5542
+ if (!node) return;
5543
+ const options = { passive: false };
5544
+ node.addEventListener("wheel", handleWheel, options);
5545
+ node.addEventListener("touchstart", handleTouchStart, options);
5546
+ node.addEventListener("touchmove", handleTouchMove, options);
5547
+ node.addEventListener("touchend", handleTouchEnd, options);
5548
+ node.addEventListener("touchcancel", handleTouchEnd, options);
5549
+ return () => {
5550
+ node.removeEventListener("wheel", handleWheel, options);
5551
+ node.removeEventListener("touchstart", handleTouchStart, options);
5552
+ node.removeEventListener("touchmove", handleTouchMove, options);
5553
+ node.removeEventListener("touchend", handleTouchEnd, options);
5554
+ node.removeEventListener("touchcancel", handleTouchEnd, options);
5555
+ };
5556
+ }, [handleWheel, handleTouchStart, handleTouchMove, handleTouchEnd]);
5557
+ const getRootProps = useCallback(
5558
+ () => ({
5559
+ className,
5560
+ style,
5561
+ onMouseDown: handleMouseDown,
5562
+ onKeyDown: handleKeyDown,
5563
+ onKeyUp: handleKeyUp,
5564
+ tabIndex: 0,
5565
+ role: "application",
5566
+ "aria-label": "Interactive image cropper",
5567
+ "aria-describedby": descriptionId,
5568
+ "aria-valuemin": minZoom,
5569
+ "aria-valuemax": maxZoom,
5570
+ "aria-valuenow": effectiveZoom,
5571
+ "aria-valuetext": `Zoom: ${Math.round(effectiveZoom * 100)}%`
5572
+ }),
5573
+ [
5574
+ className,
5575
+ style,
5576
+ handleMouseDown,
5577
+ handleKeyDown,
5578
+ handleKeyUp,
5579
+ descriptionId,
5580
+ minZoom,
5581
+ maxZoom,
5582
+ effectiveZoom
5583
+ ]
5584
+ );
5585
+ const getImageWrapperStyle = useCallback(
5586
+ () => ({
5587
+ width: imageWrapperWidth,
5588
+ height: imageWrapperHeight,
5589
+ transform: `translate3d(${offsetX}px, ${offsetY}px, 0px) scale(${effectiveZoom})`,
5590
+ position: "absolute",
5591
+ left: `calc(50% - ${imageWrapperWidth / 2}px)`,
5592
+ top: `calc(50% - ${imageWrapperHeight / 2}px)`,
5593
+ willChange: "transform"
5594
+ }),
5595
+ [imageWrapperWidth, imageWrapperHeight, offsetX, offsetY, effectiveZoom]
5596
+ );
5597
+ const getImageProps = useCallback(
5598
+ () => ({
5599
+ src: image,
5600
+ alt: "Image being cropped",
5601
+ draggable: false,
5602
+ "aria-hidden": true
5603
+ }),
5604
+ [image]
5605
+ );
5606
+ const getCropAreaStyle = useCallback(
5607
+ () => ({
5608
+ width: cropAreaWidth,
5609
+ height: cropAreaHeight
5610
+ }),
5611
+ [cropAreaWidth, cropAreaHeight]
5612
+ );
5613
+ const getCropAreaProps = useCallback(
5614
+ () => ({
5615
+ style: getCropAreaStyle(),
5616
+ "aria-hidden": true
5617
+ }),
5618
+ [getCropAreaStyle]
5619
+ );
5620
+ const contextValue = {
5621
+ containerRef,
5622
+ image,
5623
+ imgWidth,
5624
+ imgHeight,
5625
+ cropAreaWidth,
5626
+ cropAreaHeight,
5627
+ imageWrapperWidth,
5628
+ imageWrapperHeight,
5629
+ offsetX,
5630
+ offsetY,
5631
+ effectiveZoom,
5632
+ minZoom,
5633
+ maxZoom,
5634
+ getRootProps,
5635
+ getImageProps,
5636
+ getImageWrapperStyle,
5637
+ getCropAreaProps,
5638
+ getCropAreaStyle,
5639
+ descriptionId
5640
+ };
5641
+ return /* @__PURE__ */ jsx(CropperContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { ref: containerRef, ...getRootProps(), ...restProps, children }) });
5642
+ };
5643
+ var CropperImage = ({ className, ...restProps }) => {
5644
+ const { image, getImageProps, getImageWrapperStyle } = useCropperContext();
5645
+ if (!image) return null;
5646
+ const imageProps = getImageProps();
5647
+ return /* @__PURE__ */ jsx("div", { style: getImageWrapperStyle(), children: /* @__PURE__ */ jsx("img", { ...imageProps, className, ...restProps }) });
5648
+ };
5649
+ var CropperCropArea = ({
5650
+ className,
5651
+ style,
5652
+ ...restProps
5653
+ }) => {
5654
+ const { cropAreaWidth, cropAreaHeight, getCropAreaProps, getCropAreaStyle } = useCropperContext();
5655
+ if (cropAreaWidth <= 0 || cropAreaHeight <= 0) return null;
5656
+ const areaProps = getCropAreaProps();
5657
+ const areaStyle = getCropAreaStyle();
5658
+ return /* @__PURE__ */ jsx(
5659
+ "div",
5660
+ {
5661
+ ...areaProps,
5662
+ style: { ...areaProps.style, ...style, ...areaStyle },
5663
+ className,
5664
+ ...restProps
5665
+ }
5666
+ );
5667
+ };
5668
+ var CropperDescription = ({
5669
+ children,
5670
+ className,
5671
+ ...restProps
5672
+ }) => {
5673
+ const { descriptionId } = useCropperContext();
5674
+ return /* @__PURE__ */ jsx("div", { id: descriptionId, className, ...restProps, children: children ?? // Default description if none provided by user
5675
+ "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." });
5676
+ };
5677
+ var Cropper = {
5678
+ Root: CropperRoot,
5679
+ Image: CropperImage,
5680
+ CropArea: CropperCropArea,
5681
+ Description: CropperDescription
5682
+ };
5683
+ function Slider({
5684
+ className,
5685
+ defaultValue,
5686
+ value,
5687
+ min = 0,
5688
+ max = 100,
5689
+ ...props
5690
+ }) {
5691
+ const [internalValues, setInternalValues] = React8.useState(
5692
+ Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]
5693
+ );
5694
+ React8.useEffect(() => {
5695
+ if (value !== void 0) {
5696
+ setInternalValues(Array.isArray(value) ? value : [value]);
5697
+ }
5698
+ }, [value]);
5699
+ const handleValueChange = (newValue) => {
5700
+ setInternalValues(newValue);
5701
+ props.onValueChange?.(newValue);
5702
+ };
5703
+ const renderThumb = () => {
5704
+ const thumb = /* @__PURE__ */ jsx(
5705
+ SliderPrimitive.Thumb,
4975
5706
  {
4976
- role: "button",
4977
- onClick: openFileDialog,
4978
- onDragEnter: handleDragEnter,
4979
- onDragLeave: handleDragLeave,
4980
- onDragOver: handleDragOver,
4981
- onDrop: handleDrop,
4982
- "data-dragging": isDragging || void 0,
4983
- className: cn(
4984
- "relative border border-transparent rounded-full size-fit",
4985
- "bg-fileUpload-background hover:bg-fileUpload-backgroundHover transition-colors",
4986
- !file && "border-dashed border-fileUpload-border"
5707
+ "data-testid": "slider-thumb",
5708
+ "data-slot": "slider-thumb",
5709
+ 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"
5710
+ }
5711
+ );
5712
+ return thumb;
5713
+ };
5714
+ return /* @__PURE__ */ jsxs(
5715
+ SliderPrimitive.Root,
5716
+ {
5717
+ "data-slot": "slider",
5718
+ defaultValue,
5719
+ value,
5720
+ min,
5721
+ max,
5722
+ className: cn(
5723
+ "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",
5724
+ className
5725
+ ),
5726
+ onValueChange: handleValueChange,
5727
+ ...props,
5728
+ children: [
5729
+ /* @__PURE__ */ jsx(
5730
+ SliderPrimitive.Track,
5731
+ {
5732
+ "data-slot": "slider-track",
5733
+ className: cn(
5734
+ "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"
5735
+ ),
5736
+ children: /* @__PURE__ */ jsx(
5737
+ SliderPrimitive.Range,
5738
+ {
5739
+ "data-slot": "slider-range",
5740
+ className: cn(
5741
+ "bg-slider-rangeColor absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
5742
+ )
5743
+ }
5744
+ )
5745
+ }
4987
5746
  ),
4988
- "data-testid": "select-image-profile",
4989
- children: [
4990
- /* @__PURE__ */ jsx(
4991
- "input",
4992
- {
4993
- ...getInputProps(),
4994
- className: "nebula-ds sr-only",
4995
- "aria-label": "Upload file"
4996
- }
4997
- ),
4998
- /* @__PURE__ */ jsxs(
4999
- "div",
5000
- {
5001
- className: "nebula-ds flex size-12 shrink-0 items-center justify-center rounded-full",
5002
- "aria-hidden": "true",
5003
- children: [
5004
- file && file.preview && /* @__PURE__ */ jsx(
5005
- "img",
5747
+ Array.from({ length: internalValues.length }, (_, index) => /* @__PURE__ */ jsx(React8.Fragment, { children: renderThumb() }, index))
5748
+ ]
5749
+ }
5750
+ );
5751
+ }
5752
+ function CropperRoot2({
5753
+ className,
5754
+ ...props
5755
+ }) {
5756
+ return /* @__PURE__ */ jsx(
5757
+ Cropper.Root,
5758
+ {
5759
+ "data-slot": "cropper",
5760
+ className: cn(
5761
+ "relative flex w-full cursor-move touch-none items-center justify-center overflow-hidden focus:outline-none",
5762
+ className
5763
+ ),
5764
+ ...props
5765
+ }
5766
+ );
5767
+ }
5768
+ function CropperDescription2({
5769
+ className,
5770
+ ...props
5771
+ }) {
5772
+ return /* @__PURE__ */ jsx(
5773
+ Cropper.Description,
5774
+ {
5775
+ "data-slot": "cropper-description",
5776
+ className: cn("sr-only", className),
5777
+ ...props
5778
+ }
5779
+ );
5780
+ }
5781
+ function CropperImage2({
5782
+ className,
5783
+ ...props
5784
+ }) {
5785
+ return /* @__PURE__ */ jsx(
5786
+ Cropper.Image,
5787
+ {
5788
+ "data-slot": "cropper-image",
5789
+ className: cn(
5790
+ "pointer-events-none h-full w-full object-cover",
5791
+ className
5792
+ ),
5793
+ ...props
5794
+ }
5795
+ );
5796
+ }
5797
+ function CropperCropArea2({
5798
+ className,
5799
+ rounded,
5800
+ ...props
5801
+ }) {
5802
+ return /* @__PURE__ */ jsx(
5803
+ Cropper.CropArea,
5804
+ {
5805
+ "data-slot": "cropper-crop-area",
5806
+ className: cn(
5807
+ "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",
5808
+ rounded && "rounded-full",
5809
+ className,
5810
+ "border-2 border-cropper-cropAreaBorderColor"
5811
+ ),
5812
+ ...props
5813
+ }
5814
+ );
5815
+ }
5816
+ var createImage = (url) => new Promise((resolve, reject) => {
5817
+ const image = new Image();
5818
+ image.addEventListener("load", () => resolve(image));
5819
+ image.addEventListener("error", (error2) => reject(error2));
5820
+ image.setAttribute("crossOrigin", "anonymous");
5821
+ image.src = url;
5822
+ });
5823
+ async function getCroppedImg(imageSrc, pixelCrop, outputWidth = pixelCrop.width, outputHeight = pixelCrop.height) {
5824
+ try {
5825
+ const image = await createImage(imageSrc);
5826
+ const canvas = document.createElement("canvas");
5827
+ const ctx = canvas.getContext("2d");
5828
+ if (!ctx) {
5829
+ return null;
5830
+ }
5831
+ canvas.width = outputWidth;
5832
+ canvas.height = outputHeight;
5833
+ ctx.drawImage(
5834
+ image,
5835
+ pixelCrop.x,
5836
+ pixelCrop.y,
5837
+ pixelCrop.width,
5838
+ pixelCrop.height,
5839
+ 0,
5840
+ 0,
5841
+ outputWidth,
5842
+ // Draw onto the output size
5843
+ outputHeight
5844
+ );
5845
+ return new Promise((resolve) => {
5846
+ canvas.toBlob((blob) => {
5847
+ resolve(blob);
5848
+ }, "image/jpeg");
5849
+ });
5850
+ } catch (error2) {
5851
+ return null;
5852
+ }
5853
+ }
5854
+ function Cropper2({
5855
+ onOpenChange,
5856
+ open,
5857
+ previewUrl,
5858
+ onApply,
5859
+ onCancelCrop,
5860
+ rounded = false,
5861
+ portal = false
5862
+ }) {
5863
+ const [finalImageUrl, setFinalImageUrl] = useState(null);
5864
+ const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
5865
+ const [zoom, setZoom] = useState(1);
5866
+ const handleCropChange = useCallback((pixels) => {
5867
+ setCroppedAreaPixels(pixels);
5868
+ }, []);
5869
+ const handleApply = async () => {
5870
+ if (!previewUrl || !croppedAreaPixels) {
5871
+ if (previewUrl) {
5872
+ setCroppedAreaPixels(null);
5873
+ }
5874
+ return;
5875
+ }
5876
+ try {
5877
+ const croppedBlob = await getCroppedImg(previewUrl, croppedAreaPixels);
5878
+ if (!croppedBlob) {
5879
+ throw new Error("Failed to generate cropped image blob.");
5880
+ }
5881
+ const newFinalUrl = URL.createObjectURL(croppedBlob);
5882
+ if (finalImageUrl) {
5883
+ URL.revokeObjectURL(finalImageUrl);
5884
+ }
5885
+ setFinalImageUrl(newFinalUrl);
5886
+ onOpenChange(false);
5887
+ onApply(newFinalUrl, croppedBlob);
5888
+ } catch (error2) {
5889
+ onOpenChange(false);
5890
+ }
5891
+ };
5892
+ const handleCancelCrop = () => {
5893
+ setFinalImageUrl(null);
5894
+ setCroppedAreaPixels(null);
5895
+ setZoom(1);
5896
+ onOpenChange?.(false);
5897
+ onCancelCrop?.();
5898
+ };
5899
+ useEffect(() => {
5900
+ const currentFinalUrl = finalImageUrl;
5901
+ return () => {
5902
+ if (currentFinalUrl && currentFinalUrl.startsWith("blob:")) {
5903
+ URL.revokeObjectURL(currentFinalUrl);
5904
+ }
5905
+ };
5906
+ }, [finalImageUrl]);
5907
+ const { cropper } = useNebulaI18n().messages;
5908
+ return /* @__PURE__ */ jsx(
5909
+ Dialog,
5910
+ {
5911
+ open,
5912
+ onOpenChange: (isOpen) => {
5913
+ if (!isOpen && !finalImageUrl) {
5914
+ handleCancelCrop();
5915
+ }
5916
+ onOpenChange?.(isOpen);
5917
+ },
5918
+ children: /* @__PURE__ */ jsxs(
5919
+ DialogContent,
5920
+ {
5921
+ className: "nebula-ds gap-0 p-0 sm:max-w-140 *:[button]:hidden border border-cropper-dialogBorderColor",
5922
+ portal,
5923
+ showClose: false,
5924
+ onOpenAutoFocus: (e) => e.preventDefault(),
5925
+ children: [
5926
+ /* @__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: [
5927
+ /* @__PURE__ */ jsxs("div", { className: "nebula-ds flex items-center gap-2", children: [
5928
+ /* @__PURE__ */ jsx(
5929
+ Button,
5006
5930
  {
5007
- src: file.preview,
5008
- alt: file.file.name,
5009
- className: "nebula-ds rounded-[inherit] object-cover h-full w-full"
5931
+ icon: true,
5932
+ type: "button",
5933
+ variant: "ghost",
5934
+ onClick: handleCancelCrop,
5935
+ "aria-label": "close-cropper-dialog",
5936
+ "data-testid": "close-cropper-dialog",
5937
+ size: "sm",
5938
+ children: /* @__PURE__ */ jsx(XIcon, { "aria-hidden": "true" })
5010
5939
  }
5011
5940
  ),
5012
- !file && /* @__PURE__ */ jsx(UserIcon, { className: "nebula-ds size-4 opacity-60 text-fileUpload-icon" })
5013
- ]
5014
- }
5941
+ cropper.dialogTitle
5942
+ ] }),
5943
+ /* @__PURE__ */ jsx(
5944
+ Button,
5945
+ {
5946
+ autoFocus: false,
5947
+ onClick: handleApply,
5948
+ disabled: !previewUrl,
5949
+ variant: "primary",
5950
+ size: "sm",
5951
+ type: "button",
5952
+ "aria-label": "apply-cropper-image",
5953
+ "data-testid": "apply-cropper-image",
5954
+ children: cropper.applyButtonLabel
5955
+ }
5956
+ )
5957
+ ] }) }),
5958
+ previewUrl && /* @__PURE__ */ jsxs(
5959
+ CropperRoot2,
5960
+ {
5961
+ className: "nebula-ds h-96 sm:h-120",
5962
+ image: previewUrl,
5963
+ zoom,
5964
+ onCropChange: handleCropChange,
5965
+ onZoomChange: setZoom,
5966
+ children: [
5967
+ /* @__PURE__ */ jsx(CropperDescription2, {}),
5968
+ /* @__PURE__ */ jsx(CropperImage2, {}),
5969
+ /* @__PURE__ */ jsx(CropperCropArea2, { rounded })
5970
+ ]
5971
+ }
5972
+ ),
5973
+ /* @__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: [
5974
+ /* @__PURE__ */ jsx(
5975
+ MinusIcon,
5976
+ {
5977
+ className: "nebula-ds shrink-0 size-5 text-cropper-sliderIconColor",
5978
+ size: 16,
5979
+ "aria-hidden": "true"
5980
+ }
5981
+ ),
5982
+ /* @__PURE__ */ jsx(
5983
+ Slider,
5984
+ {
5985
+ defaultValue: [1],
5986
+ value: [zoom],
5987
+ min: 1,
5988
+ max: 3,
5989
+ step: 0.1,
5990
+ onValueChange: (value) => setZoom(value[0]),
5991
+ "aria-label": "Zoom slider"
5992
+ }
5993
+ ),
5994
+ /* @__PURE__ */ jsx(
5995
+ PlusIcon,
5996
+ {
5997
+ className: "nebula-ds shrink-0 size-5 text-cropper-sliderIconColor",
5998
+ size: 16,
5999
+ "aria-hidden": "true"
6000
+ }
6001
+ )
6002
+ ] }) })
6003
+ ]
6004
+ }
6005
+ )
6006
+ }
6007
+ );
6008
+ }
6009
+ var ProfileImage = ({
6010
+ maxSizeMB = 5,
6011
+ subtitle,
6012
+ onError,
6013
+ maxFiles = Infinity,
6014
+ onRemove,
6015
+ image,
6016
+ cropperProps,
6017
+ onChange
6018
+ }) => {
6019
+ const maxSize = maxSizeMB * 1024 * 1024;
6020
+ const id = useId();
6021
+ const withCropper = useMemo(() => !!cropperProps, [cropperProps]);
6022
+ const handleFileChange = (file2, blob) => {
6023
+ if (!file2) {
6024
+ return;
6025
+ }
6026
+ let fileLike = file2;
6027
+ if (withCropper && !blob) {
6028
+ return;
6029
+ }
6030
+ if (withCropper && blob) {
6031
+ fileLike = new File([blob], file2.name, { type: file2.type });
6032
+ }
6033
+ onChange?.(fileLike);
6034
+ };
6035
+ const [
6036
+ { files, isDragging, errors },
6037
+ {
6038
+ handleDragEnter,
6039
+ handleDragLeave,
6040
+ handleDragOver,
6041
+ handleDrop,
6042
+ openFileDialog,
6043
+ removeFile,
6044
+ getInputProps
6045
+ }
6046
+ ] = useFileUpload({
6047
+ maxFiles,
6048
+ initialFiles: image ? [
6049
+ {
6050
+ id,
6051
+ url: image,
6052
+ name: image,
6053
+ size: 0,
6054
+ type: "image"
6055
+ }
6056
+ ] : [],
6057
+ multiple: false,
6058
+ maxSize: maxSize > 0 ? maxSize : void 0,
6059
+ accept: "image/*",
6060
+ onFilesChange([file2]) {
6061
+ handleFileChange(file2?.file);
6062
+ }
6063
+ });
6064
+ const [finalImageUrl, setFinalImageUrl] = useState(null);
6065
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
6066
+ useEffect(() => {
6067
+ onError?.(errors);
6068
+ }, [errors, onError]);
6069
+ const [file] = files;
6070
+ const fileId = file?.id;
6071
+ const previousFileIdRef = useRef(null);
6072
+ const previewUrl = file?.preview || null;
6073
+ const profileImagePreview = useMemo(() => {
6074
+ if (withCropper) return finalImageUrl;
6075
+ return previewUrl;
6076
+ }, [previewUrl, withCropper, finalImageUrl]);
6077
+ useEffect(() => {
6078
+ if (!withCropper) return;
6079
+ if (fileId && fileId !== previousFileIdRef.current) {
6080
+ setIsDialogOpen(true);
6081
+ }
6082
+ previousFileIdRef.current = fileId;
6083
+ }, [fileId, withCropper]);
6084
+ const handleApply = async (croppedUrl, croppedBlob) => {
6085
+ try {
6086
+ const newFinalUrl = URL.createObjectURL(croppedBlob);
6087
+ if (finalImageUrl) {
6088
+ URL.revokeObjectURL(finalImageUrl);
6089
+ }
6090
+ setFinalImageUrl(newFinalUrl);
6091
+ handleFileChange(file?.file, croppedBlob);
6092
+ setIsDialogOpen(false);
6093
+ } catch (error2) {
6094
+ setIsDialogOpen(false);
6095
+ }
6096
+ };
6097
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
6098
+ /* @__PURE__ */ jsxs("div", { className: "nebula-ds flex flex-col gap-3", children: [
6099
+ /* @__PURE__ */ jsx("div", { className: "nebula-ds flex justify-center", children: /* @__PURE__ */ jsxs(
6100
+ "div",
6101
+ {
6102
+ role: "button",
6103
+ onClick: openFileDialog,
6104
+ onDragEnter: handleDragEnter,
6105
+ onDragLeave: handleDragLeave,
6106
+ onDragOver: handleDragOver,
6107
+ onDrop: handleDrop,
6108
+ "data-dragging": isDragging || void 0,
6109
+ className: cn(
6110
+ "relative border border-transparent rounded-full size-fit",
6111
+ "bg-fileUpload-background hover:bg-fileUpload-backgroundHover transition-colors",
6112
+ !file && "border-dashed border-fileUpload-border"
5015
6113
  ),
5016
- file && /* @__PURE__ */ jsx(
5017
- "button",
5018
- {
5019
- className: "nebula-ds box-border absolute flex items-center justify-center -top-1 -right-1 rounded-full size-5 text-profileImage-removeImageButton-icon border-2 border-profileImage-removeImageButton-border bg-profileImage-removeImageButton-background hover:bg-profileImage-removeImageButton-hover",
5020
- "data-testid": "remove-profile-image",
5021
- onClick: (e) => {
5022
- e.stopPropagation();
5023
- removeFile(file.id);
5024
- onRemove?.();
5025
- },
5026
- type: "button",
5027
- children: /* @__PURE__ */ jsx(XIcon, { className: "nebula-ds size-2" })
5028
- }
5029
- )
5030
- ]
6114
+ "data-testid": "select-image-profile",
6115
+ children: [
6116
+ /* @__PURE__ */ jsx(
6117
+ "input",
6118
+ {
6119
+ ...getInputProps(),
6120
+ className: "nebula-ds sr-only",
6121
+ "aria-label": "Upload file"
6122
+ }
6123
+ ),
6124
+ /* @__PURE__ */ jsxs(
6125
+ "div",
6126
+ {
6127
+ className: "nebula-ds flex size-12 shrink-0 items-center justify-center rounded-full",
6128
+ "aria-hidden": "true",
6129
+ children: [
6130
+ profileImagePreview && /* @__PURE__ */ jsx(
6131
+ "img",
6132
+ {
6133
+ src: profileImagePreview,
6134
+ alt: fileId,
6135
+ className: "nebula-ds rounded-[inherit] object-cover h-full w-full"
6136
+ }
6137
+ ),
6138
+ !file && /* @__PURE__ */ jsx(UserIcon, { className: "nebula-ds size-4 opacity-60 text-fileUpload-icon" })
6139
+ ]
6140
+ }
6141
+ ),
6142
+ profileImagePreview && file && /* @__PURE__ */ jsx(
6143
+ "button",
6144
+ {
6145
+ className: "nebula-ds box-border absolute flex items-center justify-center -top-1 -right-1 rounded-full size-5 text-profileImage-removeImageButton-icon border-2 border-profileImage-removeImageButton-border bg-profileImage-removeImageButton-background hover:bg-profileImage-removeImageButton-hover",
6146
+ "data-testid": "remove-profile-image",
6147
+ onClick: (e) => {
6148
+ e.stopPropagation();
6149
+ removeFile(file.id);
6150
+ setFinalImageUrl(null);
6151
+ onChange?.(void 0);
6152
+ onRemove?.();
6153
+ },
6154
+ type: "button",
6155
+ children: /* @__PURE__ */ jsx(XIcon, { className: "nebula-ds size-2" })
6156
+ }
6157
+ )
6158
+ ]
6159
+ }
6160
+ ) }),
6161
+ !!subtitle && /* @__PURE__ */ jsx(Paragraph, { className: "nebula-ds text-center", size: "sm", children: subtitle })
6162
+ ] }),
6163
+ withCropper && /* @__PURE__ */ jsx(
6164
+ Cropper2,
6165
+ {
6166
+ ...cropperProps,
6167
+ open: isDialogOpen,
6168
+ previewUrl,
6169
+ onOpenChange: setIsDialogOpen,
6170
+ onApply: handleApply,
6171
+ onCancelCrop: () => {
6172
+ fileId && removeFile(fileId);
6173
+ setFinalImageUrl(null);
6174
+ onChange?.(void 0);
6175
+ }
5031
6176
  }
5032
- ) }),
5033
- !!subtitle && /* @__PURE__ */ jsx(Paragraph, { className: "nebula-ds text-center", size: "sm", children: subtitle })
6177
+ )
5034
6178
  ] });
5035
6179
  };
5036
6180
  ProfileImage.displayName = "ProfileImage";
6181
+ function RadioGroup2({
6182
+ className,
6183
+ ...props
6184
+ }) {
6185
+ return /* @__PURE__ */ jsx(
6186
+ RadioGroupPrimitive.Root,
6187
+ {
6188
+ "data-slot": "radio-group",
6189
+ className: cn("grid gap-3", className),
6190
+ ...props
6191
+ }
6192
+ );
6193
+ }
6194
+ function RadioGroupItem({
6195
+ className,
6196
+ ...props
6197
+ }) {
6198
+ return /* @__PURE__ */ jsx(
6199
+ RadioGroupPrimitive.Item,
6200
+ {
6201
+ "data-slot": "radio-group-item",
6202
+ className: cn(
6203
+ `
6204
+ border-2
6205
+ data-[state=checked]:border-4
6206
+
6207
+ border-radio-border-unselected-default
6208
+ data-[state=checked]:border-radio-background-selected-default
6209
+ hover:border-radio-border-unselected-hover
6210
+ data-[state=checked]:hover:border-radio-background-selected-hover
6211
+
6212
+ bg-radio-background-unselected-default
6213
+ data-[state=checked]:bg-radio-background-unselected-default
6214
+ hover:bg-radio-background-unselected-hover
6215
+ data-[state=checked]:hover:bg-radio-background-unselected-hover
6216
+
6217
+ disabled:border-radio-border-unselected-disabled
6218
+ disabled:bg-radio-background-unselected-disabled
6219
+ data-[state=checked]:disabled:bg-radio-background-unselected-default
6220
+ data-[state=checked]:disabled:border-radio-background-selected-disabled
6221
+
6222
+ focus-visible:ring-[2px]
6223
+ focus-visible:border-radio-border-selected-focus
6224
+
6225
+ aria-invalid:ring-destructive/20
6226
+ dark:aria-invalid:ring-destructive/40
6227
+ aria-invalid:border-destructive
6228
+
6229
+ aspect-square
6230
+ size-4
6231
+ shrink-0
6232
+ rounded-full
6233
+ shadow-xs
6234
+ transition-all
6235
+ outline-none
6236
+ disabled:cursor-not-allowed
6237
+ `,
6238
+ className
6239
+ ),
6240
+ ...props,
6241
+ children: /* @__PURE__ */ jsx(RadioGroupPrimitive.Indicator, { className: "nebula-ds flex items-center justify-center text-current", children: /* @__PURE__ */ jsx("div", { className: "nebula-ds rounded-full w-2 h-2" }) })
6242
+ }
6243
+ );
6244
+ }
6245
+ var StepperContext = createContext(void 0);
6246
+ var StepItemContext = createContext(
6247
+ void 0
6248
+ );
6249
+ var useStepper = () => {
6250
+ const context = useContext(StepperContext);
6251
+ if (!context) {
6252
+ throw new Error("useStepper must be used within a Stepper");
6253
+ }
6254
+ return context;
6255
+ };
6256
+ var useStepItem = () => {
6257
+ const context = useContext(StepItemContext);
6258
+ if (!context) {
6259
+ throw new Error("useStepItem must be used within a StepperItem");
6260
+ }
6261
+ return context;
6262
+ };
6263
+ function Stepper({
6264
+ defaultValue = 0,
6265
+ value,
6266
+ onValueChange,
6267
+ orientation = "horizontal",
6268
+ className,
6269
+ ...props
6270
+ }) {
6271
+ const [activeStep, setInternalStep] = React8.useState(defaultValue);
6272
+ const setActiveStep = React8.useCallback(
6273
+ (step) => {
6274
+ if (value === void 0) {
6275
+ setInternalStep(step);
6276
+ }
6277
+ onValueChange?.(step);
6278
+ },
6279
+ [value, onValueChange]
6280
+ );
6281
+ const currentStep = value ?? activeStep;
6282
+ return /* @__PURE__ */ jsx(
6283
+ StepperContext.Provider,
6284
+ {
6285
+ value: {
6286
+ activeStep: currentStep,
6287
+ setActiveStep,
6288
+ orientation
6289
+ },
6290
+ children: /* @__PURE__ */ jsx(
6291
+ "div",
6292
+ {
6293
+ "data-slot": "stepper",
6294
+ className: cn(
6295
+ `
6296
+ group/stepper
6297
+ gap-2
6298
+ inline-flex
6299
+ data-[orientation=horizontal]:w-full
6300
+ data-[orientation=horizontal]:flex-row
6301
+ data-[orientation=vertical]:flex-col
6302
+ `,
6303
+ className
6304
+ ),
6305
+ "data-orientation": orientation,
6306
+ ...props
6307
+ }
6308
+ )
6309
+ }
6310
+ );
6311
+ }
6312
+ function StepperItem({
6313
+ step,
6314
+ completed = false,
6315
+ disabled = false,
6316
+ loading = false,
6317
+ className,
6318
+ children,
6319
+ ...props
6320
+ }) {
6321
+ const { activeStep } = useStepper();
6322
+ const state = completed || step < activeStep ? "completed" : activeStep === step ? "active" : "inactive";
6323
+ const isLoading = loading && step === activeStep;
6324
+ return /* @__PURE__ */ jsx(
6325
+ StepItemContext.Provider,
6326
+ {
6327
+ value: { step, state, isDisabled: disabled, isLoading },
6328
+ children: /* @__PURE__ */ jsx(
6329
+ "div",
6330
+ {
6331
+ "data-slot": "stepper-item",
6332
+ className: cn(
6333
+ `
6334
+ group/step
6335
+ gap-2
6336
+ flex
6337
+ items-center
6338
+ group-data-[orientation=horizontal]/stepper:flex-row
6339
+ group-data-[orientation=vertical]/stepper:flex-col
6340
+ group-data-[orientation=vertical]/stepper:items-start
6341
+ `,
6342
+ className
6343
+ ),
6344
+ "data-state": state,
6345
+ ...isLoading ? { "data-loading": true } : {},
6346
+ ...props,
6347
+ children
6348
+ }
6349
+ )
6350
+ }
6351
+ );
6352
+ }
6353
+ function StepperTrigger({
6354
+ asChild = false,
6355
+ className,
6356
+ children,
6357
+ isClickable = true,
6358
+ ...props
6359
+ }) {
6360
+ const { setActiveStep } = useStepper();
6361
+ const { step, isDisabled } = useStepItem();
6362
+ if (asChild) {
6363
+ const Comp = asChild ? Slot : "span";
6364
+ return /* @__PURE__ */ jsx(Comp, { "data-slot": "stepper-trigger", className, children });
6365
+ }
6366
+ return /* @__PURE__ */ jsx(
6367
+ "button",
6368
+ {
6369
+ "data-slot": "stepper-trigger",
6370
+ className: cn(
6371
+ `
6372
+ gap-2
6373
+ inline-flex
6374
+ items-center
6375
+ rounded-full
6376
+ outline-none
6377
+ focus-visible:z-10
6378
+ focus-visible:border-ring
6379
+ focus-visible:ring-ring/50
6380
+ focus-visible:ring-[3px]
6381
+ disabled:pointer-events-none
6382
+ disabled:opacity-50
6383
+ `,
6384
+ className
6385
+ ),
6386
+ onClick: () => isClickable ? setActiveStep(step) : null,
6387
+ disabled: isDisabled,
6388
+ ...props,
6389
+ children
6390
+ }
6391
+ );
6392
+ }
6393
+ function StepperIndicator({
6394
+ asChild = false,
6395
+ className,
6396
+ children,
6397
+ ...props
6398
+ }) {
6399
+ const { state, step, isLoading } = useStepItem();
6400
+ return /* @__PURE__ */ jsx(
6401
+ "span",
6402
+ {
6403
+ "data-slot": "stepper-indicator",
6404
+ className: cn(
6405
+ `
6406
+ flex
6407
+ size-5
6408
+ shrink-0
6409
+ items-center
6410
+ justify-center
6411
+ rounded-full
6412
+ font-semibold
6413
+ text-xs
6414
+ transition-colors
6415
+ bg-stepper-background-default
6416
+ text-stepper-count-default
6417
+ data-[state=active]:bg-stepper-background-active
6418
+ data-[state=active]:text-stepper-count-active
6419
+ data-[state=completed]:bg-stepper-background-active
6420
+ data-[state=completed]:text-stepper-count-active
6421
+ `,
6422
+ className
6423
+ ),
6424
+ "data-state": state,
6425
+ ...props,
6426
+ children: asChild ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
6427
+ isLoading ? /* @__PURE__ */ jsx(
6428
+ LoaderCircleIcon,
6429
+ {
6430
+ className: "nebula-ds animate-spin",
6431
+ size: 14,
6432
+ "aria-hidden": "true"
6433
+ }
6434
+ ) : /* @__PURE__ */ jsx(
6435
+ "span",
6436
+ {
6437
+ className: cn(
6438
+ `
6439
+ transition-all
6440
+ group-data-[state=completed]/step:scale-0
6441
+ group-data-[state=completed]/step:opacity-0
6442
+ `
6443
+ ),
6444
+ children: step
6445
+ }
6446
+ ),
6447
+ /* @__PURE__ */ jsx(
6448
+ CheckIcon,
6449
+ {
6450
+ className: cn(
6451
+ `
6452
+ absolute
6453
+ scale-0
6454
+ opacity-0
6455
+ transition-all
6456
+ group-data-[state=completed]/step:scale-100
6457
+ group-data-[state=completed]/step:opacity-100
6458
+ `
6459
+ ),
6460
+ size: 16,
6461
+ "aria-hidden": "true"
6462
+ }
6463
+ )
6464
+ ] })
6465
+ }
6466
+ );
6467
+ }
6468
+ function StepperTitle({
6469
+ className,
6470
+ ...props
6471
+ }) {
6472
+ const { state } = useStepItem();
6473
+ return /* @__PURE__ */ jsx(
6474
+ "h3",
6475
+ {
6476
+ "data-slot": "stepper-title",
6477
+ className: cn(
6478
+ `
6479
+ text-sm
6480
+ font-medium
6481
+ text-stepper-title-default
6482
+ data-[state=active]:text-stepper-title-active
6483
+ data-[state=completed]:text-stepper-title-active
6484
+ `,
6485
+ className
6486
+ ),
6487
+ "data-state": state,
6488
+ ...props
6489
+ }
6490
+ );
6491
+ }
6492
+ function StepperDescription({
6493
+ className,
6494
+ ...props
6495
+ }) {
6496
+ return /* @__PURE__ */ jsx(
6497
+ "p",
6498
+ {
6499
+ "data-slot": "stepper-description",
6500
+ className: cn(
6501
+ `
6502
+ text-sm
6503
+ text-muted-foreground
6504
+ `,
6505
+ className
6506
+ ),
6507
+ ...props
6508
+ }
6509
+ );
6510
+ }
6511
+ function StepperSeparator({
6512
+ className,
6513
+ ...props
6514
+ }) {
6515
+ const { step } = useStepItem();
6516
+ const { activeStep, orientation } = useStepper();
6517
+ const separatorState = step < activeStep ? "completed" : "inactive";
6518
+ const line = /* @__PURE__ */ jsx(
6519
+ "div",
6520
+ {
6521
+ "data-slot": "stepper-separator",
6522
+ "data-orientation": orientation,
6523
+ className: cn(
6524
+ `
6525
+ rounded-full
6526
+ bg-stepper-separator-default
6527
+ `,
6528
+ orientation === "horizontal" && `
6529
+ w-5
6530
+ h-[2px]
6531
+ flex-1
6532
+ `,
6533
+ orientation === "vertical" && `
6534
+ w-[2px]
6535
+ h-5
6536
+ flex-none
6537
+ `,
6538
+ separatorState === "completed" && `
6539
+ bg-stepper-separator-active
6540
+ `,
6541
+ className
6542
+ ),
6543
+ "data-state": separatorState,
6544
+ ...props
6545
+ }
6546
+ );
6547
+ if (orientation === "vertical") {
6548
+ return /* @__PURE__ */ jsx("div", { className: "nebula-ds flex w-5 justify-center", children: line });
6549
+ }
6550
+ return line;
6551
+ }
5037
6552
 
5038
6553
  // src/tailwind.ts
5039
6554
  function content({ base = "./" } = {}) {
@@ -5045,4 +6560,4 @@ var tailwind = {
5045
6560
  // plugin: () => require("tailwindcss")("node_modules/@nebulareact/dist/tailwind.config.js"),
5046
6561
  };
5047
6562
 
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 };
6563
+ 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, RadioGroup2 as RadioGroup, RadioGroupItem, StyledSelect as Select, Separator2 as Separator, Skeleton, Slider, Space, SpaceDirectionEnum, SpaceSizeEnum, Stepper, StepperDescription, StepperIndicator, StepperItem, StepperSeparator, StepperTitle, StepperTrigger, 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 };