@greatapps/common 1.1.461 → 1.1.463

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.
@@ -116,8 +116,7 @@ function ImageCropModal({
116
116
  onImageLoad: onImageLoadCallback,
117
117
  showCircleGuide,
118
118
  minWidth,
119
- minHeight,
120
- hideOverlay
119
+ minHeight
121
120
  }) {
122
121
  const imgRef = useRef(null);
123
122
  const [crop, setCrop] = useState();
@@ -168,8 +167,8 @@ function ImageCropModal({
168
167
  /* @__PURE__ */ jsxs(
169
168
  DialogContent,
170
169
  {
171
- className: "w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0",
172
- hideOverlay,
170
+ className: "w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!",
171
+ overlayClassName: "z-[1002]!",
173
172
  children: [
174
173
  /* @__PURE__ */ jsx(DialogHeader, { className: "p-5", children: /* @__PURE__ */ jsx(DialogTitle, { className: "text-center text-[16px]!", children: "Ajustar imagem" }) }),
175
174
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 px-5 pb-5", children: [
@@ -198,7 +197,7 @@ function ImageCropModal({
198
197
  ) }),
199
198
  /* @__PURE__ */ jsx("p", { className: "paragraph-small-regular text-zinc-500 text-center", children: "Arraste para ajustar a \xE1rea de corte" })
200
199
  ] }),
201
- /* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200", children: [
200
+ /* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200", children: [
202
201
  /* @__PURE__ */ jsx(Button, { onClick: handleConfirm, children: "Confirmar" }),
203
202
  /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: handleCancel, children: "Cancelar" })
204
203
  ] })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/components/widgets/ImageUpload/ImageCropModal.tsx"],"sourcesContent":["'use client'\n\nimport { useRef, useState } from 'react'\nimport ReactCrop, {\n type Crop,\n centerCrop,\n makeAspectCrop,\n} from 'react-image-crop'\nimport 'react-image-crop/dist/ReactCrop.css'\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/overlay/Dialog'\nimport { Button } from '../../ui/buttons/Button'\n\n// CSS customizado para grid pontilhado fino e círculo\nconst baseCropStyles = `\n .custom-crop-area .ReactCrop__crop-selection {\n border: 2px solid white !important;\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5) !important;\n }\n\n /* Grid de 9 quadrados - linhas pontilhadas finas */\n .custom-crop-area .ReactCrop__crop-selection .crop-grid {\n position: absolute;\n inset: 0;\n pointer-events: none;\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line {\n position: absolute;\n background: repeating-linear-gradient(\n to right,\n rgba(255, 255, 255, 0.5) 0,\n rgba(255, 255, 255, 0.5) 4px,\n transparent 4px,\n transparent 8px\n );\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.horizontal {\n left: 0;\n right: 0;\n height: 1px;\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.vertical {\n top: 0;\n bottom: 0;\n width: 1px;\n background: repeating-linear-gradient(\n to bottom,\n rgba(255, 255, 255, 0.5) 0,\n rgba(255, 255, 255, 0.5) 4px,\n transparent 4px,\n transparent 8px\n );\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.h1 { top: 33.33%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.h2 { top: 66.66%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.v1 { left: 33.33%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.v2 { left: 66.66%; }\n\n /* Círculo inscrito */\n .custom-crop-area.with-circle .ReactCrop__crop-selection .crop-circle {\n position: absolute;\n inset: 0;\n border: 1px dashed rgba(255, 255, 255, 0.6);\n border-radius: 50%;\n pointer-events: none;\n }\n\n /* Bolinhas centrais nas bordas */\n .custom-crop-area .ReactCrop__crop-selection .crop-handle {\n position: absolute;\n width: 10px;\n height: 10px;\n background: white;\n border-radius: 50%;\n pointer-events: none;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.top {\n top: -5px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.bottom {\n bottom: -5px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.left {\n left: -5px;\n top: 50%;\n transform: translateY(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.right {\n right: -5px;\n top: 50%;\n transform: translateY(-50%);\n }\n`\n\ninterface ImageCropModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n imageSrc: string\n aspectRatio: number\n onConfirm: (image: HTMLImageElement, crop: Crop) => void\n onCancel?: () => void\n onImageLoad?: (naturalWidth: number, naturalHeight: number) => void\n showCircleGuide?: boolean\n minWidth?: number\n minHeight?: number\n hideOverlay?: boolean\n}\n\nexport function ImageCropModal({\n open,\n onOpenChange,\n imageSrc,\n aspectRatio,\n onConfirm,\n onCancel,\n onImageLoad: onImageLoadCallback,\n showCircleGuide,\n minWidth,\n minHeight,\n hideOverlay,\n}: ImageCropModalProps) {\n const imgRef = useRef<HTMLImageElement>(null)\n const [crop, setCrop] = useState<Crop>()\n const [imageSize, setImageSize] = useState({ width: 0, height: 0 })\n\n const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {\n const { naturalWidth, naturalHeight } = e.currentTarget\n setImageSize({ width: naturalWidth, height: naturalHeight })\n\n // Notifica o hook para verificar dimensões mínimas\n onImageLoadCallback?.(naturalWidth, naturalHeight)\n\n const initialCrop = centerCrop(\n makeAspectCrop(\n { unit: '%', width: 90 },\n aspectRatio,\n naturalWidth,\n naturalHeight\n ),\n naturalWidth,\n naturalHeight\n )\n\n setCrop(initialCrop)\n }\n\n // Calcula tamanho mínimo em porcentagem baseado nas dimensões mínimas em pixels\n const minCropWidth = minWidth && imageSize.width > 0\n ? Math.min((minWidth / imageSize.width) * 100, 100)\n : undefined\n const minCropHeight = minHeight && imageSize.height > 0\n ? Math.min((minHeight / imageSize.height) * 100, 100)\n : undefined\n\n const handleConfirm = () => {\n if (imgRef.current && crop) {\n onConfirm(imgRef.current, crop)\n }\n }\n\n const handleCancel = () => {\n onCancel?.()\n onOpenChange(false)\n }\n\n // Determina se deve mostrar círculo (aspect ratio 1:1 por padrão)\n const shouldShowCircle = showCircleGuide ?? aspectRatio === 1\n\n // Renderiza grid, círculo e bolinhas dentro da área de crop\n const renderSelectionAddon = () => (\n <>\n {/* Grid de 9 quadrados com linhas pontilhadas */}\n <div className=\"crop-grid\">\n <div className=\"crop-grid-line horizontal h1\" />\n <div className=\"crop-grid-line horizontal h2\" />\n <div className=\"crop-grid-line vertical v1\" />\n <div className=\"crop-grid-line vertical v2\" />\n </div>\n {/* Círculo inscrito */}\n {shouldShowCircle && <div className=\"crop-circle\" />}\n {/* Bolinhas centrais nas bordas */}\n <div className=\"crop-handle top\" />\n <div className=\"crop-handle bottom\" />\n <div className=\"crop-handle left\" />\n <div className=\"crop-handle right\" />\n </>\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <style>{baseCropStyles}</style>\n <DialogContent\n className=\"w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0\"\n hideOverlay={hideOverlay}\n >\n <DialogHeader className=\"p-5\">\n <DialogTitle className=\"text-center text-[16px]!\">\n Ajustar imagem\n </DialogTitle>\n </DialogHeader>\n\n <div className=\"flex flex-col gap-4 px-5 pb-5\">\n <div className=\"flex justify-center overflow-hidden rounded-lg\">\n <ReactCrop\n crop={crop}\n onChange={(c) => setCrop(c)}\n aspect={aspectRatio}\n className={`max-h-[50vh] custom-crop-area ${shouldShowCircle ? 'with-circle' : ''}`}\n keepSelection\n minWidth={minCropWidth}\n minHeight={minCropHeight}\n renderSelectionAddon={renderSelectionAddon}\n >\n <img\n ref={imgRef}\n src={imageSrc}\n alt=\"Crop\"\n onLoad={onImageLoad}\n className=\"max-h-[50vh] object-contain\"\n />\n </ReactCrop>\n </div>\n\n <p className=\"paragraph-small-regular text-zinc-500 text-center\">\n Arraste para ajustar a área de corte\n </p>\n </div>\n\n <DialogFooter className=\"flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200\">\n <Button onClick={handleConfirm}>Confirmar</Button>\n <Button variant=\"secondary\" onClick={handleCancel}>\n Cancelar\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":";AA4LI,mBAGI,KADF,YAFF;AA1LJ,SAAS,QAAQ,gBAAgB;AACjC,OAAO;AAAA,EAEL;AAAA,EACA;AAAA,OACK;AACP,OAAO;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AAGvB,MAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2GhB,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,SAAS,OAAyB,IAAI;AAC5C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAe;AACvC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,EAAE,OAAO,GAAG,QAAQ,EAAE,CAAC;AAElE,QAAM,cAAc,CAAC,MAA8C;AACjE,UAAM,EAAE,cAAc,cAAc,IAAI,EAAE;AAC1C,iBAAa,EAAE,OAAO,cAAc,QAAQ,cAAc,CAAC;AAG3D,0BAAsB,cAAc,aAAa;AAEjD,UAAM,cAAc;AAAA,MAClB;AAAA,QACE,EAAE,MAAM,KAAK,OAAO,GAAG;AAAA,QACvB;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,YAAQ,WAAW;AAAA,EACrB;AAGA,QAAM,eAAe,YAAY,UAAU,QAAQ,IAC/C,KAAK,IAAK,WAAW,UAAU,QAAS,KAAK,GAAG,IAChD;AACJ,QAAM,gBAAgB,aAAa,UAAU,SAAS,IAClD,KAAK,IAAK,YAAY,UAAU,SAAU,KAAK,GAAG,IAClD;AAEJ,QAAM,gBAAgB,MAAM;AAC1B,QAAI,OAAO,WAAW,MAAM;AAC1B,gBAAU,OAAO,SAAS,IAAI;AAAA,IAChC;AAAA,EACF;AAEA,QAAM,eAAe,MAAM;AACzB,eAAW;AACX,iBAAa,KAAK;AAAA,EACpB;AAGA,QAAM,mBAAmB,mBAAmB,gBAAgB;AAG5D,QAAM,uBAAuB,MAC3B,iCAEE;AAAA,yBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,SAAI,WAAU,gCAA+B;AAAA,MAC9C,oBAAC,SAAI,WAAU,gCAA+B;AAAA,MAC9C,oBAAC,SAAI,WAAU,8BAA6B;AAAA,MAC5C,oBAAC,SAAI,WAAU,8BAA6B;AAAA,OAC9C;AAAA,IAEC,oBAAoB,oBAAC,SAAI,WAAU,eAAc;AAAA,IAElD,oBAAC,SAAI,WAAU,mBAAkB;AAAA,IACjC,oBAAC,SAAI,WAAU,sBAAqB;AAAA,IACpC,oBAAC,SAAI,WAAU,oBAAmB;AAAA,IAClC,oBAAC,SAAI,WAAU,qBAAoB;AAAA,KACrC;AAGF,SACE,qBAAC,UAAO,MAAY,cAClB;AAAA,wBAAC,WAAO,0BAAe;AAAA,IACvB;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV;AAAA,QAEA;AAAA,8BAAC,gBAAa,WAAU,OACtB,8BAAC,eAAY,WAAU,4BAA2B,4BAElD,GACF;AAAA,UAEA,qBAAC,SAAI,WAAU,iCACb;AAAA,gCAAC,SAAI,WAAU,kDACb;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA,UAAU,CAAC,MAAM,QAAQ,CAAC;AAAA,gBAC1B,QAAQ;AAAA,gBACR,WAAW,iCAAiC,mBAAmB,gBAAgB,EAAE;AAAA,gBACjF,eAAa;AAAA,gBACb,UAAU;AAAA,gBACV,WAAW;AAAA,gBACX;AAAA,gBAEA;AAAA,kBAAC;AAAA;AAAA,oBACC,KAAK;AAAA,oBACL,KAAK;AAAA,oBACL,KAAI;AAAA,oBACJ,QAAQ;AAAA,oBACR,WAAU;AAAA;AAAA,gBACZ;AAAA;AAAA,YACF,GACF;AAAA,YAEA,oBAAC,OAAE,WAAU,qDAAoD,qDAEjE;AAAA,aACF;AAAA,UAEA,qBAAC,gBAAa,WAAU,0EACtB;AAAA,gCAAC,UAAO,SAAS,eAAe,uBAAS;AAAA,YACzC,oBAAC,UAAO,SAAQ,aAAY,SAAS,cAAc,sBAEnD;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../../src/components/widgets/ImageUpload/ImageCropModal.tsx"],"sourcesContent":["'use client'\n\nimport { useRef, useState } from 'react'\nimport ReactCrop, {\n type Crop,\n centerCrop,\n makeAspectCrop,\n} from 'react-image-crop'\nimport 'react-image-crop/dist/ReactCrop.css'\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/overlay/Dialog'\nimport { Button } from '../../ui/buttons/Button'\n\n// CSS customizado para grid pontilhado fino e círculo\nconst baseCropStyles = `\n .custom-crop-area .ReactCrop__crop-selection {\n border: 2px solid white !important;\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5) !important;\n }\n\n /* Grid de 9 quadrados - linhas pontilhadas finas */\n .custom-crop-area .ReactCrop__crop-selection .crop-grid {\n position: absolute;\n inset: 0;\n pointer-events: none;\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line {\n position: absolute;\n background: repeating-linear-gradient(\n to right,\n rgba(255, 255, 255, 0.5) 0,\n rgba(255, 255, 255, 0.5) 4px,\n transparent 4px,\n transparent 8px\n );\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.horizontal {\n left: 0;\n right: 0;\n height: 1px;\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.vertical {\n top: 0;\n bottom: 0;\n width: 1px;\n background: repeating-linear-gradient(\n to bottom,\n rgba(255, 255, 255, 0.5) 0,\n rgba(255, 255, 255, 0.5) 4px,\n transparent 4px,\n transparent 8px\n );\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.h1 { top: 33.33%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.h2 { top: 66.66%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.v1 { left: 33.33%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.v2 { left: 66.66%; }\n\n /* Círculo inscrito */\n .custom-crop-area.with-circle .ReactCrop__crop-selection .crop-circle {\n position: absolute;\n inset: 0;\n border: 1px dashed rgba(255, 255, 255, 0.6);\n border-radius: 50%;\n pointer-events: none;\n }\n\n /* Bolinhas centrais nas bordas */\n .custom-crop-area .ReactCrop__crop-selection .crop-handle {\n position: absolute;\n width: 10px;\n height: 10px;\n background: white;\n border-radius: 50%;\n pointer-events: none;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.top {\n top: -5px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.bottom {\n bottom: -5px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.left {\n left: -5px;\n top: 50%;\n transform: translateY(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.right {\n right: -5px;\n top: 50%;\n transform: translateY(-50%);\n }\n`\n\ninterface ImageCropModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n imageSrc: string\n aspectRatio: number\n onConfirm: (image: HTMLImageElement, crop: Crop) => void\n onCancel?: () => void\n onImageLoad?: (naturalWidth: number, naturalHeight: number) => void\n showCircleGuide?: boolean\n minWidth?: number\n minHeight?: number\n}\n\nexport function ImageCropModal({\n open,\n onOpenChange,\n imageSrc,\n aspectRatio,\n onConfirm,\n onCancel,\n onImageLoad: onImageLoadCallback,\n showCircleGuide,\n minWidth,\n minHeight,\n}: ImageCropModalProps) {\n const imgRef = useRef<HTMLImageElement>(null)\n const [crop, setCrop] = useState<Crop>()\n const [imageSize, setImageSize] = useState({ width: 0, height: 0 })\n\n const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {\n const { naturalWidth, naturalHeight } = e.currentTarget\n setImageSize({ width: naturalWidth, height: naturalHeight })\n\n // Notifica o hook para verificar dimensões mínimas\n onImageLoadCallback?.(naturalWidth, naturalHeight)\n\n const initialCrop = centerCrop(\n makeAspectCrop(\n { unit: '%', width: 90 },\n aspectRatio,\n naturalWidth,\n naturalHeight\n ),\n naturalWidth,\n naturalHeight\n )\n\n setCrop(initialCrop)\n }\n\n // Calcula tamanho mínimo em porcentagem baseado nas dimensões mínimas em pixels\n const minCropWidth = minWidth && imageSize.width > 0\n ? Math.min((minWidth / imageSize.width) * 100, 100)\n : undefined\n const minCropHeight = minHeight && imageSize.height > 0\n ? Math.min((minHeight / imageSize.height) * 100, 100)\n : undefined\n\n const handleConfirm = () => {\n if (imgRef.current && crop) {\n onConfirm(imgRef.current, crop)\n }\n }\n\n const handleCancel = () => {\n onCancel?.()\n onOpenChange(false)\n }\n\n // Determina se deve mostrar círculo (aspect ratio 1:1 por padrão)\n const shouldShowCircle = showCircleGuide ?? aspectRatio === 1\n\n // Renderiza grid, círculo e bolinhas dentro da área de crop\n const renderSelectionAddon = () => (\n <>\n {/* Grid de 9 quadrados com linhas pontilhadas */}\n <div className=\"crop-grid\">\n <div className=\"crop-grid-line horizontal h1\" />\n <div className=\"crop-grid-line horizontal h2\" />\n <div className=\"crop-grid-line vertical v1\" />\n <div className=\"crop-grid-line vertical v2\" />\n </div>\n {/* Círculo inscrito */}\n {shouldShowCircle && <div className=\"crop-circle\" />}\n {/* Bolinhas centrais nas bordas */}\n <div className=\"crop-handle top\" />\n <div className=\"crop-handle bottom\" />\n <div className=\"crop-handle left\" />\n <div className=\"crop-handle right\" />\n </>\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <style>{baseCropStyles}</style>\n <DialogContent\n className=\"w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!\"\n overlayClassName=\"z-[1002]!\"\n >\n <DialogHeader className=\"p-5\">\n <DialogTitle className=\"text-center text-[16px]!\">\n Ajustar imagem\n </DialogTitle>\n </DialogHeader>\n\n <div className=\"flex flex-col gap-4 px-5 pb-5\">\n <div className=\"flex justify-center overflow-hidden rounded-lg\">\n <ReactCrop\n crop={crop}\n onChange={(c) => setCrop(c)}\n aspect={aspectRatio}\n className={`max-h-[50vh] custom-crop-area ${shouldShowCircle ? 'with-circle' : ''}`}\n keepSelection\n minWidth={minCropWidth}\n minHeight={minCropHeight}\n renderSelectionAddon={renderSelectionAddon}\n >\n <img\n ref={imgRef}\n src={imageSrc}\n alt=\"Crop\"\n onLoad={onImageLoad}\n className=\"max-h-[50vh] object-contain\"\n />\n </ReactCrop>\n </div>\n\n <p className=\"paragraph-small-regular text-zinc-500 text-center\">\n Arraste para ajustar a área de corte\n </p>\n </div>\n\n <DialogFooter className=\"flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200\">\n <Button onClick={handleConfirm}>Confirmar</Button>\n <Button variant=\"secondary\" onClick={handleCancel}>\n Cancelar\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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","names":[]}
@@ -15,15 +15,14 @@ function ImageTooSmallModal({
15
15
  minWidth,
16
16
  minHeight,
17
17
  onSelectAnother,
18
- onCancel,
19
- hideOverlay
18
+ onCancel
20
19
  }) {
21
20
  const dimensionText = `${minWidth}x${minHeight}px`;
22
21
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
23
22
  DialogContent,
24
23
  {
25
- className: "w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0",
26
- hideOverlay,
24
+ className: "w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!",
25
+ overlayClassName: "z-[1002]!",
27
26
  children: [
28
27
  /* @__PURE__ */ jsx(DialogHeader, { className: "p-5", children: /* @__PURE__ */ jsxs(DialogTitle, { className: "text-center text-[16px]!", children: [
29
28
  "Foto menor que ",
@@ -38,7 +37,7 @@ function ImageTooSmallModal({
38
37
  dimensionText,
39
38
  " para continuar."
40
39
  ] }) }),
41
- /* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200", children: [
40
+ /* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200", children: [
42
41
  /* @__PURE__ */ jsx(Button, { onClick: onSelectAnother, children: "Enviar outra" }),
43
42
  /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onCancel, children: "Cancelar" })
44
43
  ] })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/components/widgets/ImageUpload/ImageTooSmallModal.tsx"],"sourcesContent":["'use client'\n\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/overlay/Dialog'\nimport { Button } from '../../ui/buttons/Button'\n\ninterface ImageTooSmallModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n fileName: string\n minWidth: number\n minHeight: number\n onSelectAnother: () => void\n onCancel: () => void\n hideOverlay?: boolean\n}\n\nexport function ImageTooSmallModal({\n open,\n onOpenChange,\n fileName,\n minWidth,\n minHeight,\n onSelectAnother,\n onCancel,\n hideOverlay,\n}: ImageTooSmallModalProps) {\n const dimensionText = `${minWidth}x${minHeight}px`\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className=\"w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0\"\n hideOverlay={hideOverlay}\n >\n <DialogHeader className=\"p-5\">\n <DialogTitle className=\"text-center text-[16px]!\">\n Foto menor que {dimensionText}\n </DialogTitle>\n </DialogHeader>\n\n <div className=\"px-5 pb-5\">\n <p className=\"paragraph-small-regular text-zinc-600 text-center\">\n O arquivo \"{fileName}\" é menor que {dimensionText}. Faça envio de\n uma imagem com pelo menos {dimensionText} para continuar.\n </p>\n </div>\n\n <DialogFooter className=\"flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200\">\n <Button onClick={onSelectAnother}>Enviar outra</Button>\n <Button variant=\"secondary\" onClick={onCancel}>\n Cancelar\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":";AAwCQ,cACE,YADF;AAtCR;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AAahB,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,gBAAgB,GAAG,QAAQ,IAAI,SAAS;AAE9C,SACE,oBAAC,UAAO,MAAY,cAClB;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV;AAAA,MAEA;AAAA,4BAAC,gBAAa,WAAU,OACtB,+BAAC,eAAY,WAAU,4BAA2B;AAAA;AAAA,UAChC;AAAA,WAClB,GACF;AAAA,QAEA,oBAAC,SAAI,WAAU,aACb,+BAAC,OAAE,WAAU,qDAAoD;AAAA;AAAA,UACnD;AAAA,UAAS;AAAA,UAAe;AAAA,UAAc;AAAA,UACvB;AAAA,UAAc;AAAA,WAC3C,GACF;AAAA,QAEA,qBAAC,gBAAa,WAAU,0EACtB;AAAA,8BAAC,UAAO,SAAS,iBAAiB,0BAAY;AAAA,UAC9C,oBAAC,UAAO,SAAQ,aAAY,SAAS,UAAU,sBAE/C;AAAA,WACF;AAAA;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../../src/components/widgets/ImageUpload/ImageTooSmallModal.tsx"],"sourcesContent":["'use client'\n\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/overlay/Dialog'\nimport { Button } from '../../ui/buttons/Button'\n\ninterface ImageTooSmallModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n fileName: string\n minWidth: number\n minHeight: number\n onSelectAnother: () => void\n onCancel: () => void\n}\n\nexport function ImageTooSmallModal({\n open,\n onOpenChange,\n fileName,\n minWidth,\n minHeight,\n onSelectAnother,\n onCancel,\n}: ImageTooSmallModalProps) {\n const dimensionText = `${minWidth}x${minHeight}px`\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className=\"w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!\"\n overlayClassName=\"z-[1002]!\"\n >\n <DialogHeader className=\"p-5\">\n <DialogTitle className=\"text-center text-[16px]!\">\n Foto menor que {dimensionText}\n </DialogTitle>\n </DialogHeader>\n\n <div className=\"px-5 pb-5\">\n <p className=\"paragraph-small-regular text-zinc-600 text-center\">\n O arquivo \"{fileName}\" é menor que {dimensionText}. Faça envio de\n uma imagem com pelo menos {dimensionText} para continuar.\n </p>\n </div>\n\n <DialogFooter className=\"flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200\">\n <Button onClick={onSelectAnother}>Enviar outra</Button>\n <Button variant=\"secondary\" onClick={onCancel}>\n Cancelar\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":";AAsCQ,cACE,YADF;AApCR;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AAYhB,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,gBAAgB,GAAG,QAAQ,IAAI,SAAS;AAE9C,SACE,oBAAC,UAAO,MAAY,cAClB;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,kBAAiB;AAAA,MAEjB;AAAA,4BAAC,gBAAa,WAAU,OACtB,+BAAC,eAAY,WAAU,4BAA2B;AAAA;AAAA,UAChC;AAAA,WAClB,GACF;AAAA,QAEA,oBAAC,SAAI,WAAU,aACb,+BAAC,OAAE,WAAU,qDAAoD;AAAA;AAAA,UACnD;AAAA,UAAS;AAAA,UAAe;AAAA,UAAc;AAAA,UACvB;AAAA,UAAc;AAAA,WAC3C,GACF;AAAA,QAEA,qBAAC,gBAAa,WAAU,4EACtB;AAAA,8BAAC,UAAO,SAAS,iBAAiB,0BAAY;AAAA,UAC9C,oBAAC,UAAO,SAAQ,aAAY,SAAS,UAAU,sBAE/C;AAAA,WACF;AAAA;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
@@ -12,6 +12,22 @@ function useImageUpload(options) {
12
12
  const [isProcessing, setIsProcessing] = useState(false);
13
13
  const fileInputRef = useRef(null);
14
14
  const currentFileName = useRef("");
15
+ const checkMinimumDimensions = useCallback(
16
+ (naturalWidth, naturalHeight) => {
17
+ const { width: minW, height: minH, aspectRatio } = config;
18
+ const imgAspect = naturalWidth / naturalHeight;
19
+ if (imgAspect >= aspectRatio) {
20
+ const cropHeight = naturalHeight;
21
+ const cropWidth = cropHeight * aspectRatio;
22
+ return cropWidth >= minW && cropHeight >= minH;
23
+ } else {
24
+ const cropWidth = naturalWidth;
25
+ const cropHeight = cropWidth / aspectRatio;
26
+ return cropWidth >= minW && cropHeight >= minH;
27
+ }
28
+ },
29
+ [config]
30
+ );
15
31
  const handleFileSelect = useCallback(
16
32
  (file) => {
17
33
  const validation = validateImage(file);
@@ -22,38 +38,32 @@ function useImageUpload(options) {
22
38
  currentFileName.current = file.name;
23
39
  const reader = new FileReader();
24
40
  reader.onload = (e) => {
25
- setOriginalImage(e.target?.result);
26
- setShowCropModal(true);
41
+ const base64 = e.target?.result;
42
+ setOriginalImage(base64);
43
+ const img = new Image();
44
+ img.onload = () => {
45
+ const { naturalWidth, naturalHeight } = img;
46
+ if (checkMinimumDimensions(naturalWidth, naturalHeight)) {
47
+ setShowCropModal(true);
48
+ } else {
49
+ setTooSmallError({
50
+ fileName: currentFileName.current,
51
+ minWidth: config.width,
52
+ minHeight: config.height
53
+ });
54
+ setShowTooSmallModal(true);
55
+ }
56
+ };
57
+ img.src = base64;
27
58
  };
28
59
  reader.readAsDataURL(file);
29
60
  },
30
- [onError]
61
+ [onError, config, checkMinimumDimensions]
31
62
  );
32
63
  const handleImageLoadInModal = useCallback(
33
- (naturalWidth, naturalHeight) => {
34
- const { width: minW, height: minH, aspectRatio } = config;
35
- const imgAspect = naturalWidth / naturalHeight;
36
- let canFitMinCrop = false;
37
- if (imgAspect >= aspectRatio) {
38
- const cropHeight = naturalHeight;
39
- const cropWidth = cropHeight * aspectRatio;
40
- canFitMinCrop = cropWidth >= minW && cropHeight >= minH;
41
- } else {
42
- const cropWidth = naturalWidth;
43
- const cropHeight = cropWidth / aspectRatio;
44
- canFitMinCrop = cropWidth >= minW && cropHeight >= minH;
45
- }
46
- if (!canFitMinCrop) {
47
- setShowCropModal(false);
48
- setTooSmallError({
49
- fileName: currentFileName.current,
50
- minWidth: minW,
51
- minHeight: minH
52
- });
53
- setShowTooSmallModal(true);
54
- }
64
+ (_naturalWidth, _naturalHeight) => {
55
65
  },
56
- [config]
66
+ []
57
67
  );
58
68
  const handleSelectAnotherFile = useCallback(() => {
59
69
  setShowTooSmallModal(false);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/images/hooks/use-image-upload.hook.ts"],"sourcesContent":["'use client'\n\nimport { useState, useCallback, useRef } from 'react'\nimport type { Crop } from 'react-image-crop'\nimport { cropImageToCanvas } from '../utils/crop-image'\nimport { validateImage } from '../utils/validate-image'\nimport type { ImageConfig } from '../types/image.type'\n\nexport interface ImageTooSmallError {\n fileName: string\n minWidth: number\n minHeight: number\n}\n\nexport interface UseImageUploadOptions {\n config: ImageConfig\n onSuccess?: (base64Image: string) => void\n onError?: (error: string) => void\n}\n\nexport function useImageUpload(options: UseImageUploadOptions) {\n const { config, onSuccess, onError } = options\n\n const [originalImage, setOriginalImage] = useState<string | null>(null)\n const [croppedPreview, setCroppedPreview] = useState<string | null>(null)\n const [showCropModal, setShowCropModal] = useState(false)\n const [showTooSmallModal, setShowTooSmallModal] = useState(false)\n const [tooSmallError, setTooSmallError] = useState<ImageTooSmallError | null>(null)\n const [isProcessing, setIsProcessing] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n const currentFileName = useRef<string>('')\n\n // Abre modal IMEDIATAMENTE sem processar - muito mais rápido\n const handleFileSelect = useCallback(\n (file: File) => {\n const validation = validateImage(file)\n if (!validation.valid) {\n onError?.(validation.error || 'Arquivo inválido')\n return\n }\n\n currentFileName.current = file.name\n\n // Converte para base64 e abre modal instantaneamente\n const reader = new FileReader()\n reader.onload = (e) => {\n setOriginalImage(e.target?.result as string)\n setShowCropModal(true)\n }\n reader.readAsDataURL(file)\n },\n [onError]\n )\n\n // Chamado pelo ImageCropModal quando a imagem carrega - verifica dimensões\n const handleImageLoadInModal = useCallback(\n (naturalWidth: number, naturalHeight: number) => {\n const { width: minW, height: minH, aspectRatio } = config\n const imgAspect = naturalWidth / naturalHeight\n\n let canFitMinCrop = false\n if (imgAspect >= aspectRatio) {\n const cropHeight = naturalHeight\n const cropWidth = cropHeight * aspectRatio\n canFitMinCrop = cropWidth >= minW && cropHeight >= minH\n } else {\n const cropWidth = naturalWidth\n const cropHeight = cropWidth / aspectRatio\n canFitMinCrop = cropWidth >= minW && cropHeight >= minH\n }\n\n if (!canFitMinCrop) {\n // Fecha modal de crop e abre modal de erro\n setShowCropModal(false)\n setTooSmallError({\n fileName: currentFileName.current,\n minWidth: minW,\n minHeight: minH,\n })\n setShowTooSmallModal(true)\n }\n },\n [config]\n )\n\n const handleSelectAnotherFile = useCallback(() => {\n setShowTooSmallModal(false)\n setTooSmallError(null)\n fileInputRef.current?.click()\n }, [])\n\n const handleCancelTooSmall = useCallback(() => {\n setShowTooSmallModal(false)\n setShowCropModal(false)\n setTooSmallError(null)\n setOriginalImage(null)\n }, [])\n\n const handleCropConfirm = useCallback(\n async (imageElement: HTMLImageElement, cropArea: Crop) => {\n setIsProcessing(true)\n try {\n // Crop é feito inteiramente no cliente via Canvas\n // Já produz imagem no tamanho final (config.width x config.height)\n const croppedBase64 = await cropImageToCanvas(\n imageElement,\n cropArea,\n config.width,\n config.height\n )\n\n setCroppedPreview(croppedBase64)\n setShowCropModal(false)\n onSuccess?.(croppedBase64)\n } catch {\n onError?.('Erro ao aplicar crop')\n } finally {\n setIsProcessing(false)\n }\n },\n [config, onSuccess, onError]\n )\n\n const clear = useCallback(() => {\n setOriginalImage(null)\n setCroppedPreview(null)\n }, [])\n\n return {\n originalImage,\n croppedPreview,\n showCropModal,\n showTooSmallModal,\n tooSmallError,\n config,\n isProcessing,\n fileInputRef,\n handleFileSelect,\n handleCropConfirm,\n handleImageLoadInModal,\n handleSelectAnotherFile,\n handleCancelTooSmall,\n setShowCropModal,\n setShowTooSmallModal,\n clear,\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/images/hooks/use-image-upload.hook.ts"],"sourcesContent":["'use client'\n\nimport { useState, useCallback, useRef } from 'react'\nimport type { Crop } from 'react-image-crop'\nimport { cropImageToCanvas } from '../utils/crop-image'\nimport { validateImage } from '../utils/validate-image'\nimport type { ImageConfig } from '../types/image.type'\n\nexport interface ImageTooSmallError {\n fileName: string\n minWidth: number\n minHeight: number\n}\n\nexport interface UseImageUploadOptions {\n config: ImageConfig\n onSuccess?: (base64Image: string) => void\n onError?: (error: string) => void\n}\n\nexport function useImageUpload(options: UseImageUploadOptions) {\n const { config, onSuccess, onError } = options\n\n const [originalImage, setOriginalImage] = useState<string | null>(null)\n const [croppedPreview, setCroppedPreview] = useState<string | null>(null)\n const [showCropModal, setShowCropModal] = useState(false)\n const [showTooSmallModal, setShowTooSmallModal] = useState(false)\n const [tooSmallError, setTooSmallError] = useState<ImageTooSmallError | null>(null)\n const [isProcessing, setIsProcessing] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n const currentFileName = useRef<string>('')\n\n // Verifica se a imagem cabe o crop mínimo\n const checkMinimumDimensions = useCallback(\n (naturalWidth: number, naturalHeight: number): boolean => {\n const { width: minW, height: minH, aspectRatio } = config\n const imgAspect = naturalWidth / naturalHeight\n\n if (imgAspect >= aspectRatio) {\n const cropHeight = naturalHeight\n const cropWidth = cropHeight * aspectRatio\n return cropWidth >= minW && cropHeight >= minH\n } else {\n const cropWidth = naturalWidth\n const cropHeight = cropWidth / aspectRatio\n return cropWidth >= minW && cropHeight >= minH\n }\n },\n [config]\n )\n\n // Valida dimensões ANTES de abrir qualquer modal\n const handleFileSelect = useCallback(\n (file: File) => {\n const validation = validateImage(file)\n if (!validation.valid) {\n onError?.(validation.error || 'Arquivo inválido')\n return\n }\n\n currentFileName.current = file.name\n\n // Converte para base64\n const reader = new FileReader()\n reader.onload = (e) => {\n const base64 = e.target?.result as string\n setOriginalImage(base64)\n\n // Carrega imagem em memória para verificar dimensões ANTES de abrir modal\n const img = new Image()\n img.onload = () => {\n const { naturalWidth, naturalHeight } = img\n\n if (checkMinimumDimensions(naturalWidth, naturalHeight)) {\n // Dimensões OK - abre modal de crop\n setShowCropModal(true)\n } else {\n // Imagem muito pequena - abre modal de erro diretamente\n setTooSmallError({\n fileName: currentFileName.current,\n minWidth: config.width,\n minHeight: config.height,\n })\n setShowTooSmallModal(true)\n }\n }\n img.src = base64\n }\n reader.readAsDataURL(file)\n },\n [onError, config, checkMinimumDimensions]\n )\n\n // Mantido para compatibilidade, mas não mais necessário para validação\n const handleImageLoadInModal = useCallback(\n (_naturalWidth: number, _naturalHeight: number) => {\n // Validação agora é feita antes de abrir a modal\n // Este callback pode ser usado para outros propósitos se necessário\n },\n []\n )\n\n const handleSelectAnotherFile = useCallback(() => {\n setShowTooSmallModal(false)\n setTooSmallError(null)\n fileInputRef.current?.click()\n }, [])\n\n const handleCancelTooSmall = useCallback(() => {\n setShowTooSmallModal(false)\n setShowCropModal(false)\n setTooSmallError(null)\n setOriginalImage(null)\n }, [])\n\n const handleCropConfirm = useCallback(\n async (imageElement: HTMLImageElement, cropArea: Crop) => {\n setIsProcessing(true)\n try {\n // Crop é feito inteiramente no cliente via Canvas\n // Já produz imagem no tamanho final (config.width x config.height)\n const croppedBase64 = await cropImageToCanvas(\n imageElement,\n cropArea,\n config.width,\n config.height\n )\n\n setCroppedPreview(croppedBase64)\n setShowCropModal(false)\n onSuccess?.(croppedBase64)\n } catch {\n onError?.('Erro ao aplicar crop')\n } finally {\n setIsProcessing(false)\n }\n },\n [config, onSuccess, onError]\n )\n\n const clear = useCallback(() => {\n setOriginalImage(null)\n setCroppedPreview(null)\n }, [])\n\n return {\n originalImage,\n croppedPreview,\n showCropModal,\n showTooSmallModal,\n tooSmallError,\n config,\n isProcessing,\n fileInputRef,\n handleFileSelect,\n handleCropConfirm,\n handleImageLoadInModal,\n handleSelectAnotherFile,\n handleCancelTooSmall,\n setShowCropModal,\n setShowTooSmallModal,\n clear,\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.461",
3
+ "version": "1.1.463",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -121,7 +121,6 @@ interface ImageCropModalProps {
121
121
  showCircleGuide?: boolean
122
122
  minWidth?: number
123
123
  minHeight?: number
124
- hideOverlay?: boolean
125
124
  }
126
125
 
127
126
  export function ImageCropModal({
@@ -135,7 +134,6 @@ export function ImageCropModal({
135
134
  showCircleGuide,
136
135
  minWidth,
137
136
  minHeight,
138
- hideOverlay,
139
137
  }: ImageCropModalProps) {
140
138
  const imgRef = useRef<HTMLImageElement>(null)
141
139
  const [crop, setCrop] = useState<Crop>()
@@ -208,8 +206,8 @@ export function ImageCropModal({
208
206
  <Dialog open={open} onOpenChange={onOpenChange}>
209
207
  <style>{baseCropStyles}</style>
210
208
  <DialogContent
211
- className="w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0"
212
- hideOverlay={hideOverlay}
209
+ className="w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!"
210
+ overlayClassName="z-[1002]!"
213
211
  >
214
212
  <DialogHeader className="p-5">
215
213
  <DialogTitle className="text-center text-[16px]!">
@@ -244,7 +242,7 @@ export function ImageCropModal({
244
242
  </p>
245
243
  </div>
246
244
 
247
- <DialogFooter className="flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200">
245
+ <DialogFooter className="flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200">
248
246
  <Button onClick={handleConfirm}>Confirmar</Button>
249
247
  <Button variant="secondary" onClick={handleCancel}>
250
248
  Cancelar
@@ -17,7 +17,6 @@ interface ImageTooSmallModalProps {
17
17
  minHeight: number
18
18
  onSelectAnother: () => void
19
19
  onCancel: () => void
20
- hideOverlay?: boolean
21
20
  }
22
21
 
23
22
  export function ImageTooSmallModal({
@@ -28,15 +27,14 @@ export function ImageTooSmallModal({
28
27
  minHeight,
29
28
  onSelectAnother,
30
29
  onCancel,
31
- hideOverlay,
32
30
  }: ImageTooSmallModalProps) {
33
31
  const dimensionText = `${minWidth}x${minHeight}px`
34
32
 
35
33
  return (
36
34
  <Dialog open={open} onOpenChange={onOpenChange}>
37
35
  <DialogContent
38
- className="w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0"
39
- hideOverlay={hideOverlay}
36
+ className="w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!"
37
+ overlayClassName="z-[1002]!"
40
38
  >
41
39
  <DialogHeader className="p-5">
42
40
  <DialogTitle className="text-center text-[16px]!">
@@ -51,7 +49,7 @@ export function ImageTooSmallModal({
51
49
  </p>
52
50
  </div>
53
51
 
54
- <DialogFooter className="flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200">
52
+ <DialogFooter className="flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200">
55
53
  <Button onClick={onSelectAnother}>Enviar outra</Button>
56
54
  <Button variant="secondary" onClick={onCancel}>
57
55
  Cancelar
@@ -30,7 +30,26 @@ export function useImageUpload(options: UseImageUploadOptions) {
30
30
  const fileInputRef = useRef<HTMLInputElement>(null)
31
31
  const currentFileName = useRef<string>('')
32
32
 
33
- // Abre modal IMEDIATAMENTE sem processar - muito mais rápido
33
+ // Verifica se a imagem cabe o crop mínimo
34
+ const checkMinimumDimensions = useCallback(
35
+ (naturalWidth: number, naturalHeight: number): boolean => {
36
+ const { width: minW, height: minH, aspectRatio } = config
37
+ const imgAspect = naturalWidth / naturalHeight
38
+
39
+ if (imgAspect >= aspectRatio) {
40
+ const cropHeight = naturalHeight
41
+ const cropWidth = cropHeight * aspectRatio
42
+ return cropWidth >= minW && cropHeight >= minH
43
+ } else {
44
+ const cropWidth = naturalWidth
45
+ const cropHeight = cropWidth / aspectRatio
46
+ return cropWidth >= minW && cropHeight >= minH
47
+ }
48
+ },
49
+ [config]
50
+ )
51
+
52
+ // Valida dimensões ANTES de abrir qualquer modal
34
53
  const handleFileSelect = useCallback(
35
54
  (file: File) => {
36
55
  const validation = validateImage(file)
@@ -41,46 +60,44 @@ export function useImageUpload(options: UseImageUploadOptions) {
41
60
 
42
61
  currentFileName.current = file.name
43
62
 
44
- // Converte para base64 e abre modal instantaneamente
63
+ // Converte para base64
45
64
  const reader = new FileReader()
46
65
  reader.onload = (e) => {
47
- setOriginalImage(e.target?.result as string)
48
- setShowCropModal(true)
66
+ const base64 = e.target?.result as string
67
+ setOriginalImage(base64)
68
+
69
+ // Carrega imagem em memória para verificar dimensões ANTES de abrir modal
70
+ const img = new Image()
71
+ img.onload = () => {
72
+ const { naturalWidth, naturalHeight } = img
73
+
74
+ if (checkMinimumDimensions(naturalWidth, naturalHeight)) {
75
+ // Dimensões OK - abre modal de crop
76
+ setShowCropModal(true)
77
+ } else {
78
+ // Imagem muito pequena - abre modal de erro diretamente
79
+ setTooSmallError({
80
+ fileName: currentFileName.current,
81
+ minWidth: config.width,
82
+ minHeight: config.height,
83
+ })
84
+ setShowTooSmallModal(true)
85
+ }
86
+ }
87
+ img.src = base64
49
88
  }
50
89
  reader.readAsDataURL(file)
51
90
  },
52
- [onError]
91
+ [onError, config, checkMinimumDimensions]
53
92
  )
54
93
 
55
- // Chamado pelo ImageCropModal quando a imagem carrega - verifica dimensões
94
+ // Mantido para compatibilidade, mas não mais necessário para validação
56
95
  const handleImageLoadInModal = useCallback(
57
- (naturalWidth: number, naturalHeight: number) => {
58
- const { width: minW, height: minH, aspectRatio } = config
59
- const imgAspect = naturalWidth / naturalHeight
60
-
61
- let canFitMinCrop = false
62
- if (imgAspect >= aspectRatio) {
63
- const cropHeight = naturalHeight
64
- const cropWidth = cropHeight * aspectRatio
65
- canFitMinCrop = cropWidth >= minW && cropHeight >= minH
66
- } else {
67
- const cropWidth = naturalWidth
68
- const cropHeight = cropWidth / aspectRatio
69
- canFitMinCrop = cropWidth >= minW && cropHeight >= minH
70
- }
71
-
72
- if (!canFitMinCrop) {
73
- // Fecha modal de crop e abre modal de erro
74
- setShowCropModal(false)
75
- setTooSmallError({
76
- fileName: currentFileName.current,
77
- minWidth: minW,
78
- minHeight: minH,
79
- })
80
- setShowTooSmallModal(true)
81
- }
96
+ (_naturalWidth: number, _naturalHeight: number) => {
97
+ // Validação agora é feita antes de abrir a modal
98
+ // Este callback pode ser usado para outros propósitos se necessário
82
99
  },
83
- [config]
100
+ []
84
101
  )
85
102
 
86
103
  const handleSelectAnotherFile = useCallback(() => {