@greatapps/common 1.1.528 → 1.1.530

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.
@@ -96,35 +96,35 @@ const baseCropStyles = `
96
96
  .custom-crop-area .ReactCrop__drag-handle.ord-se::after {
97
97
  content: '';
98
98
  position: absolute;
99
- width: 16px;
100
- height: 16px;
101
- border-color: rgba(0, 0, 0, 0.7);
99
+ width: 22px;
100
+ height: 22px;
101
+ border-color: white;
102
102
  border-style: solid;
103
103
  border-width: 0;
104
104
  }
105
105
 
106
106
  .custom-crop-area .ReactCrop__drag-handle.ord-nw::after {
107
- top: 0; left: 0;
108
- border-top-width: 3px;
109
- border-left-width: 3px;
107
+ top: -2px; left: -2px;
108
+ border-top-width: 4px;
109
+ border-left-width: 4px;
110
110
  }
111
111
 
112
112
  .custom-crop-area .ReactCrop__drag-handle.ord-ne::after {
113
- top: 0; right: 0;
114
- border-top-width: 3px;
115
- border-right-width: 3px;
113
+ top: -2px; right: -2px;
114
+ border-top-width: 4px;
115
+ border-right-width: 4px;
116
116
  }
117
117
 
118
118
  .custom-crop-area .ReactCrop__drag-handle.ord-sw::after {
119
- bottom: 0; left: 0;
120
- border-bottom-width: 3px;
121
- border-left-width: 3px;
119
+ bottom: -2px; left: -2px;
120
+ border-bottom-width: 4px;
121
+ border-left-width: 4px;
122
122
  }
123
123
 
124
124
  .custom-crop-area .ReactCrop__drag-handle.ord-se::after {
125
- bottom: 0; right: 0;
126
- border-bottom-width: 3px;
127
- border-right-width: 3px;
125
+ bottom: -2px; right: -2px;
126
+ border-bottom-width: 4px;
127
+ border-right-width: 4px;
128
128
  }
129
129
 
130
130
  /* Desabilitar drag da imagem de fundo */
@@ -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 /* Ocultar handles das laterais (manter só os 4 cantos) */\n .custom-crop-area .ReactCrop__drag-handle.ord-n,\n .custom-crop-area .ReactCrop__drag-handle.ord-s,\n .custom-crop-area .ReactCrop__drag-handle.ord-e,\n .custom-crop-area .ReactCrop__drag-handle.ord-w {\n display: none !important;\n }\n\n /* Estilizar handles dos cantos — indicador preto em L nos cantos */\n .custom-crop-area .ReactCrop__drag-handle.ord-nw,\n .custom-crop-area .ReactCrop__drag-handle.ord-ne,\n .custom-crop-area .ReactCrop__drag-handle.ord-sw,\n .custom-crop-area .ReactCrop__drag-handle.ord-se {\n width: 20px;\n height: 20px;\n background: transparent !important;\n border: none !important;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-nw::after,\n .custom-crop-area .ReactCrop__drag-handle.ord-ne::after,\n .custom-crop-area .ReactCrop__drag-handle.ord-sw::after,\n .custom-crop-area .ReactCrop__drag-handle.ord-se::after {\n content: '';\n position: absolute;\n width: 16px;\n height: 16px;\n border-color: rgba(0, 0, 0, 0.7);\n border-style: solid;\n border-width: 0;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-nw::after {\n top: 0; left: 0;\n border-top-width: 3px;\n border-left-width: 3px;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-ne::after {\n top: 0; right: 0;\n border-top-width: 3px;\n border-right-width: 3px;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-sw::after {\n bottom: 0; left: 0;\n border-bottom-width: 3px;\n border-left-width: 3px;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-se::after {\n bottom: 0; right: 0;\n border-bottom-width: 3px;\n border-right-width: 3px;\n }\n\n /* Desabilitar drag da imagem de fundo */\n .custom-crop-area img {\n pointer-events: none;\n user-select: none;\n -webkit-user-drag: none;\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 </>\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 draggable={false}\n className=\"max-h-[50vh] object-contain select-none\"\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":[]}
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 /* Ocultar handles das laterais (manter só os 4 cantos) */\n .custom-crop-area .ReactCrop__drag-handle.ord-n,\n .custom-crop-area .ReactCrop__drag-handle.ord-s,\n .custom-crop-area .ReactCrop__drag-handle.ord-e,\n .custom-crop-area .ReactCrop__drag-handle.ord-w {\n display: none !important;\n }\n\n /* Estilizar handles dos cantos — indicador preto em L nos cantos */\n .custom-crop-area .ReactCrop__drag-handle.ord-nw,\n .custom-crop-area .ReactCrop__drag-handle.ord-ne,\n .custom-crop-area .ReactCrop__drag-handle.ord-sw,\n .custom-crop-area .ReactCrop__drag-handle.ord-se {\n width: 20px;\n height: 20px;\n background: transparent !important;\n border: none !important;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-nw::after,\n .custom-crop-area .ReactCrop__drag-handle.ord-ne::after,\n .custom-crop-area .ReactCrop__drag-handle.ord-sw::after,\n .custom-crop-area .ReactCrop__drag-handle.ord-se::after {\n content: '';\n position: absolute;\n width: 22px;\n height: 22px;\n border-color: white;\n border-style: solid;\n border-width: 0;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-nw::after {\n top: -2px; left: -2px;\n border-top-width: 4px;\n border-left-width: 4px;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-ne::after {\n top: -2px; right: -2px;\n border-top-width: 4px;\n border-right-width: 4px;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-sw::after {\n bottom: -2px; left: -2px;\n border-bottom-width: 4px;\n border-left-width: 4px;\n }\n\n .custom-crop-area .ReactCrop__drag-handle.ord-se::after {\n bottom: -2px; right: -2px;\n border-bottom-width: 4px;\n border-right-width: 4px;\n }\n\n /* Desabilitar drag da imagem de fundo */\n .custom-crop-area img {\n pointer-events: none;\n user-select: none;\n -webkit-user-drag: none;\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 </>\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 draggable={false}\n className=\"max-h-[50vh] object-contain select-none\"\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":[]}
@@ -42,27 +42,28 @@ function useImageUpload(options) {
42
42
  return;
43
43
  }
44
44
  currentFileName.current = file.name;
45
- const reader = new FileReader();
46
- reader.onload = (e) => {
47
- const base64 = e.target?.result;
48
- setOriginalImage(base64);
49
- const img = new Image();
50
- img.onload = () => {
51
- const { naturalWidth, naturalHeight } = img;
52
- if (checkMinimumDimensions(naturalWidth, naturalHeight)) {
53
- setShowCropModal(true);
54
- } else {
55
- setTooSmallError({
56
- fileName: currentFileName.current,
57
- minWidth: config.width,
58
- minHeight: config.height
59
- });
60
- setShowTooSmallModal(true);
61
- }
62
- };
63
- img.src = base64;
45
+ const objectUrl = URL.createObjectURL(file);
46
+ setOriginalImage(objectUrl);
47
+ const img = new Image();
48
+ img.onload = () => {
49
+ const { naturalWidth, naturalHeight } = img;
50
+ if (checkMinimumDimensions(naturalWidth, naturalHeight)) {
51
+ setShowCropModal(true);
52
+ } else {
53
+ setTooSmallError({
54
+ fileName: currentFileName.current,
55
+ minWidth: config.width,
56
+ minHeight: config.height
57
+ });
58
+ setShowTooSmallModal(true);
59
+ }
64
60
  };
65
- reader.readAsDataURL(file);
61
+ img.onerror = () => {
62
+ URL.revokeObjectURL(objectUrl);
63
+ setOriginalImage(null);
64
+ onErrorRef.current?.("Erro ao carregar a imagem. Tente uma imagem menor ou em outro formato.");
65
+ };
66
+ img.src = objectUrl;
66
67
  },
67
68
  [config, checkMinimumDimensions]
68
69
  );
@@ -80,8 +81,9 @@ function useImageUpload(options) {
80
81
  setShowTooSmallModal(false);
81
82
  setShowCropModal(false);
82
83
  setTooSmallError(null);
84
+ if (originalImage) URL.revokeObjectURL(originalImage);
83
85
  setOriginalImage(null);
84
- }, []);
86
+ }, [originalImage]);
85
87
  const handleCropConfirm = useCallback(
86
88
  async (imageElement, cropArea) => {
87
89
  setIsProcessing(true);
@@ -94,6 +96,8 @@ function useImageUpload(options) {
94
96
  );
95
97
  setCroppedPreview(croppedBase64);
96
98
  setShowCropModal(false);
99
+ if (originalImage) URL.revokeObjectURL(originalImage);
100
+ setOriginalImage(null);
97
101
  onSuccessRef.current?.(croppedBase64);
98
102
  } catch {
99
103
  onErrorRef.current?.("Erro ao aplicar crop");
@@ -104,9 +108,10 @@ function useImageUpload(options) {
104
108
  [config]
105
109
  );
106
110
  const clear = useCallback(() => {
111
+ if (originalImage) URL.revokeObjectURL(originalImage);
107
112
  setOriginalImage(null);
108
113
  setCroppedPreview(null);
109
- }, []);
114
+ }, [originalImage]);
110
115
  return {
111
116
  originalImage,
112
117
  croppedPreview,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/images/hooks/use-image-upload.hook.ts"],"sourcesContent":["'use client'\n\nimport { useState, useCallback, useRef, useEffect } 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 // Refs para manter callbacks sempre atualizados (evita stale closures)\n const onSuccessRef = useRef(onSuccess)\n const onErrorRef = useRef(onError)\n useEffect(() => {\n onSuccessRef.current = onSuccess\n onErrorRef.current = onError\n }, [onSuccess, onError])\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 onErrorRef.current?.(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 [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 onSuccessRef.current?.(croppedBase64)\n } catch {\n onErrorRef.current?.('Erro ao aplicar crop')\n } finally {\n setIsProcessing(false)\n }\n },\n [config]\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":";AAEA,SAAS,UAAU,aAAa,QAAQ,iBAAiB;AAEzD,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;AAevB,SAAS,eAAe,SAAgC;AAC7D,QAAM,EAAE,QAAQ,WAAW,QAAQ,IAAI;AAGvC,QAAM,eAAe,OAAO,SAAS;AACrC,QAAM,aAAa,OAAO,OAAO;AACjC,YAAU,MAAM;AACd,iBAAa,UAAU;AACvB,eAAW,UAAU;AAAA,EACvB,GAAG,CAAC,WAAW,OAAO,CAAC;AAEvB,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAwB,IAAI;AACtE,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAwB,IAAI;AACxE,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AACxD,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAS,KAAK;AAChE,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAoC,IAAI;AAClF,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,eAAe,OAAyB,IAAI;AAClD,QAAM,kBAAkB,OAAe,EAAE;AAGzC,QAAM,yBAAyB;AAAA,IAC7B,CAAC,cAAsB,kBAAmC;AACxD,YAAM,EAAE,OAAO,MAAM,QAAQ,MAAM,YAAY,IAAI;AACnD,YAAM,YAAY,eAAe;AAEjC,UAAI,aAAa,aAAa;AAC5B,cAAM,aAAa;AACnB,cAAM,YAAY,aAAa;AAC/B,eAAO,aAAa,QAAQ,cAAc;AAAA,MAC5C,OAAO;AACL,cAAM,YAAY;AAClB,cAAM,aAAa,YAAY;AAC/B,eAAO,aAAa,QAAQ,cAAc;AAAA,MAC5C;AAAA,IACF;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAGA,QAAM,mBAAmB;AAAA,IACvB,CAAC,SAAe;AACd,YAAM,aAAa,cAAc,IAAI;AACrC,UAAI,CAAC,WAAW,OAAO;AACrB,mBAAW,UAAU,WAAW,SAAS,qBAAkB;AAC3D;AAAA,MACF;AAEA,sBAAgB,UAAU,KAAK;AAG/B,YAAM,SAAS,IAAI,WAAW;AAC9B,aAAO,SAAS,CAAC,MAAM;AACrB,cAAM,SAAS,EAAE,QAAQ;AACzB,yBAAiB,MAAM;AAGvB,cAAM,MAAM,IAAI,MAAM;AACtB,YAAI,SAAS,MAAM;AACjB,gBAAM,EAAE,cAAc,cAAc,IAAI;AAExC,cAAI,uBAAuB,cAAc,aAAa,GAAG;AAEvD,6BAAiB,IAAI;AAAA,UACvB,OAAO;AAEL,6BAAiB;AAAA,cACf,UAAU,gBAAgB;AAAA,cAC1B,UAAU,OAAO;AAAA,cACjB,WAAW,OAAO;AAAA,YACpB,CAAC;AACD,iCAAqB,IAAI;AAAA,UAC3B;AAAA,QACF;AACA,YAAI,MAAM;AAAA,MACZ;AACA,aAAO,cAAc,IAAI;AAAA,IAC3B;AAAA,IACA,CAAC,QAAQ,sBAAsB;AAAA,EACjC;AAGA,QAAM,yBAAyB;AAAA,IAC7B,CAAC,eAAuB,mBAA2B;AAAA,IAGnD;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,0BAA0B,YAAY,MAAM;AAChD,yBAAqB,KAAK;AAC1B,qBAAiB,IAAI;AACrB,iBAAa,SAAS,MAAM;AAAA,EAC9B,GAAG,CAAC,CAAC;AAEL,QAAM,uBAAuB,YAAY,MAAM;AAC7C,yBAAqB,KAAK;AAC1B,qBAAiB,KAAK;AACtB,qBAAiB,IAAI;AACrB,qBAAiB,IAAI;AAAA,EACvB,GAAG,CAAC,CAAC;AAEL,QAAM,oBAAoB;AAAA,IACxB,OAAO,cAAgC,aAAmB;AACxD,sBAAgB,IAAI;AACpB,UAAI;AAGF,cAAM,gBAAgB,MAAM;AAAA,UAC1B;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP,OAAO;AAAA,QACT;AAEA,0BAAkB,aAAa;AAC/B,yBAAiB,KAAK;AACtB,qBAAa,UAAU,aAAa;AAAA,MACtC,QAAQ;AACN,mBAAW,UAAU,sBAAsB;AAAA,MAC7C,UAAE;AACA,wBAAgB,KAAK;AAAA,MACvB;AAAA,IACF;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,qBAAiB,IAAI;AACrB,sBAAkB,IAAI;AAAA,EACxB,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/images/hooks/use-image-upload.hook.ts"],"sourcesContent":["'use client'\n\nimport { useState, useCallback, useRef, useEffect } 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 // Refs para manter callbacks sempre atualizados (evita stale closures)\n const onSuccessRef = useRef(onSuccess)\n const onErrorRef = useRef(onError)\n useEffect(() => {\n onSuccessRef.current = onSuccess\n onErrorRef.current = onError\n }, [onSuccess, onError])\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 onErrorRef.current?.(validation.error || 'Arquivo inválido')\n return\n }\n\n currentFileName.current = file.name\n\n // Usa object URL em vez de base64 mais eficiente para imagens grandes\n const objectUrl = URL.createObjectURL(file)\n setOriginalImage(objectUrl)\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 setShowCropModal(true)\n } else {\n setTooSmallError({\n fileName: currentFileName.current,\n minWidth: config.width,\n minHeight: config.height,\n })\n setShowTooSmallModal(true)\n }\n }\n img.onerror = () => {\n URL.revokeObjectURL(objectUrl)\n setOriginalImage(null)\n onErrorRef.current?.('Erro ao carregar a imagem. Tente uma imagem menor ou em outro formato.')\n }\n img.src = objectUrl\n },\n [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 if (originalImage) URL.revokeObjectURL(originalImage)\n setOriginalImage(null)\n }, [originalImage])\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 if (originalImage) URL.revokeObjectURL(originalImage)\n setOriginalImage(null)\n onSuccessRef.current?.(croppedBase64)\n } catch {\n onErrorRef.current?.('Erro ao aplicar crop')\n } finally {\n setIsProcessing(false)\n }\n },\n [config]\n )\n\n const clear = useCallback(() => {\n if (originalImage) URL.revokeObjectURL(originalImage)\n setOriginalImage(null)\n setCroppedPreview(null)\n }, [originalImage])\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.528",
3
+ "version": "1.1.530",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -99,35 +99,35 @@ const baseCropStyles = `
99
99
  .custom-crop-area .ReactCrop__drag-handle.ord-se::after {
100
100
  content: '';
101
101
  position: absolute;
102
- width: 16px;
103
- height: 16px;
104
- border-color: rgba(0, 0, 0, 0.7);
102
+ width: 22px;
103
+ height: 22px;
104
+ border-color: white;
105
105
  border-style: solid;
106
106
  border-width: 0;
107
107
  }
108
108
 
109
109
  .custom-crop-area .ReactCrop__drag-handle.ord-nw::after {
110
- top: 0; left: 0;
111
- border-top-width: 3px;
112
- border-left-width: 3px;
110
+ top: -2px; left: -2px;
111
+ border-top-width: 4px;
112
+ border-left-width: 4px;
113
113
  }
114
114
 
115
115
  .custom-crop-area .ReactCrop__drag-handle.ord-ne::after {
116
- top: 0; right: 0;
117
- border-top-width: 3px;
118
- border-right-width: 3px;
116
+ top: -2px; right: -2px;
117
+ border-top-width: 4px;
118
+ border-right-width: 4px;
119
119
  }
120
120
 
121
121
  .custom-crop-area .ReactCrop__drag-handle.ord-sw::after {
122
- bottom: 0; left: 0;
123
- border-bottom-width: 3px;
124
- border-left-width: 3px;
122
+ bottom: -2px; left: -2px;
123
+ border-bottom-width: 4px;
124
+ border-left-width: 4px;
125
125
  }
126
126
 
127
127
  .custom-crop-area .ReactCrop__drag-handle.ord-se::after {
128
- bottom: 0; right: 0;
129
- border-bottom-width: 3px;
130
- border-right-width: 3px;
128
+ bottom: -2px; right: -2px;
129
+ border-bottom-width: 4px;
130
+ border-right-width: 4px;
131
131
  }
132
132
 
133
133
  /* Desabilitar drag da imagem de fundo */
@@ -68,33 +68,32 @@ export function useImageUpload(options: UseImageUploadOptions) {
68
68
 
69
69
  currentFileName.current = file.name
70
70
 
71
- // Converte para base64
72
- const reader = new FileReader()
73
- reader.onload = (e) => {
74
- const base64 = e.target?.result as string
75
- setOriginalImage(base64)
76
-
77
- // Carrega imagem em memória para verificar dimensões ANTES de abrir modal
78
- const img = new Image()
79
- img.onload = () => {
80
- const { naturalWidth, naturalHeight } = img
81
-
82
- if (checkMinimumDimensions(naturalWidth, naturalHeight)) {
83
- // Dimensões OK - abre modal de crop
84
- setShowCropModal(true)
85
- } else {
86
- // Imagem muito pequena - abre modal de erro diretamente
87
- setTooSmallError({
88
- fileName: currentFileName.current,
89
- minWidth: config.width,
90
- minHeight: config.height,
91
- })
92
- setShowTooSmallModal(true)
93
- }
71
+ // Usa object URL em vez de base64 — mais eficiente para imagens grandes
72
+ const objectUrl = URL.createObjectURL(file)
73
+ setOriginalImage(objectUrl)
74
+
75
+ // Carrega imagem em memória para verificar dimensões ANTES de abrir modal
76
+ const img = new Image()
77
+ img.onload = () => {
78
+ const { naturalWidth, naturalHeight } = img
79
+
80
+ if (checkMinimumDimensions(naturalWidth, naturalHeight)) {
81
+ setShowCropModal(true)
82
+ } else {
83
+ setTooSmallError({
84
+ fileName: currentFileName.current,
85
+ minWidth: config.width,
86
+ minHeight: config.height,
87
+ })
88
+ setShowTooSmallModal(true)
94
89
  }
95
- img.src = base64
96
90
  }
97
- reader.readAsDataURL(file)
91
+ img.onerror = () => {
92
+ URL.revokeObjectURL(objectUrl)
93
+ setOriginalImage(null)
94
+ onErrorRef.current?.('Erro ao carregar a imagem. Tente uma imagem menor ou em outro formato.')
95
+ }
96
+ img.src = objectUrl
98
97
  },
99
98
  [config, checkMinimumDimensions]
100
99
  )
@@ -118,8 +117,9 @@ export function useImageUpload(options: UseImageUploadOptions) {
118
117
  setShowTooSmallModal(false)
119
118
  setShowCropModal(false)
120
119
  setTooSmallError(null)
120
+ if (originalImage) URL.revokeObjectURL(originalImage)
121
121
  setOriginalImage(null)
122
- }, [])
122
+ }, [originalImage])
123
123
 
124
124
  const handleCropConfirm = useCallback(
125
125
  async (imageElement: HTMLImageElement, cropArea: Crop) => {
@@ -136,6 +136,8 @@ export function useImageUpload(options: UseImageUploadOptions) {
136
136
 
137
137
  setCroppedPreview(croppedBase64)
138
138
  setShowCropModal(false)
139
+ if (originalImage) URL.revokeObjectURL(originalImage)
140
+ setOriginalImage(null)
139
141
  onSuccessRef.current?.(croppedBase64)
140
142
  } catch {
141
143
  onErrorRef.current?.('Erro ao aplicar crop')
@@ -147,9 +149,10 @@ export function useImageUpload(options: UseImageUploadOptions) {
147
149
  )
148
150
 
149
151
  const clear = useCallback(() => {
152
+ if (originalImage) URL.revokeObjectURL(originalImage)
150
153
  setOriginalImage(null)
151
154
  setCroppedPreview(null)
152
- }, [])
155
+ }, [originalImage])
153
156
 
154
157
  return {
155
158
  originalImage,