@archbase/components 3.0.25 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/dist/buttons/ArchbaseSpeedDial.d.ts +59 -0
  2. package/dist/buttons/index.d.ts +2 -0
  3. package/dist/charts/ArchbaseArcGauge.d.ts +55 -0
  4. package/dist/charts/ArchbaseGantt.d.ts +70 -0
  5. package/dist/charts/ArchbaseLinearGauge.d.ts +55 -0
  6. package/dist/charts/ArchbaseResourceTimeline.d.ts +61 -0
  7. package/dist/charts/ArchbaseSparkline.d.ts +45 -0
  8. package/dist/charts/index.d.ts +10 -0
  9. package/dist/containers/ArchbaseExpansionPanel.d.ts +60 -0
  10. package/dist/containers/ArchbaseFloatingPanel.d.ts +91 -0
  11. package/dist/containers/index.d.ts +4 -0
  12. package/dist/datagrid/export/ArchbaseDataGridExcelExport.d.ts +67 -0
  13. package/dist/datagrid/export/index.d.ts +2 -0
  14. package/dist/datagrid/index.d.ts +1 -0
  15. package/dist/display/ArchbaseAnimationWrapper.d.ts +73 -0
  16. package/dist/display/ArchbaseBarcodeGenerator.d.ts +50 -0
  17. package/dist/display/ArchbaseCarousel.d.ts +100 -0
  18. package/dist/display/ArchbaseCodeViewer.d.ts +63 -0
  19. package/dist/display/ArchbaseDiffViewer.d.ts +41 -0
  20. package/dist/display/ArchbaseJsonTree.d.ts +34 -0
  21. package/dist/display/ArchbaseLightbox.d.ts +89 -0
  22. package/dist/display/ArchbaseMarquee.d.ts +46 -0
  23. package/dist/display/ArchbasePhotoAlbum.d.ts +58 -0
  24. package/dist/display/ArchbaseQRCode.d.ts +44 -0
  25. package/dist/display/ArchbaseTooltipRich.d.ts +82 -0
  26. package/dist/display/ArchbaseVirtualList.d.ts +116 -0
  27. package/dist/display/index.d.ts +24 -0
  28. package/dist/editors/ArchbaseAIPromptInput.d.ts +69 -0
  29. package/dist/editors/ArchbaseAsyncMultiSelect.d.ts +1 -1
  30. package/dist/editors/ArchbaseAsyncSelect.d.ts +1 -1
  31. package/dist/editors/ArchbaseBarcodeScanner.d.ts +63 -0
  32. package/dist/editors/ArchbaseCepInput.d.ts +26 -0
  33. package/dist/editors/ArchbaseColorGradientPicker.d.ts +80 -0
  34. package/dist/editors/ArchbaseCpfCnpjInput.d.ts +21 -0
  35. package/dist/editors/ArchbaseCurrencyInput.d.ts +25 -0
  36. package/dist/editors/ArchbaseDualListbox.d.ts +61 -0
  37. package/dist/editors/ArchbaseInPlaceEditor.d.ts +67 -0
  38. package/dist/editors/ArchbaseLightGrid.d.ts +56 -0
  39. package/dist/editors/ArchbaseMentionInput.d.ts +75 -0
  40. package/dist/editors/ArchbaseMultiEmail.d.ts +53 -0
  41. package/dist/editors/ArchbaseMultiViewCalendar.d.ts +47 -0
  42. package/dist/editors/ArchbaseNumberStepper.d.ts +66 -0
  43. package/dist/editors/ArchbaseOTPInput.d.ts +27 -0
  44. package/dist/editors/ArchbasePhoneInput.d.ts +52 -0
  45. package/dist/editors/ArchbaseRangeSlider.d.ts +22 -0
  46. package/dist/editors/ArchbaseSignaturePad.d.ts +60 -0
  47. package/dist/editors/ArchbaseSpeechToTextInput.d.ts +106 -0
  48. package/dist/editors/ArchbaseSpreadsheet.d.ts +56 -0
  49. package/dist/editors/ArchbaseTagInput.d.ts +55 -0
  50. package/dist/editors/index.d.ts +40 -0
  51. package/dist/feedback/ArchbaseChunkProgressBar.d.ts +35 -0
  52. package/dist/feedback/ArchbaseRipple.d.ts +43 -0
  53. package/dist/feedback/ArchbaseSkeleton.d.ts +115 -0
  54. package/dist/feedback/index.d.ts +6 -0
  55. package/dist/filters/ArchbaseColumnSelector.d.ts +38 -0
  56. package/dist/filters/index.d.ts +2 -0
  57. package/dist/forms/ArchbaseFieldArray.d.ts +80 -0
  58. package/dist/forms/ArchbaseFormWizard.d.ts +73 -0
  59. package/dist/forms/ArchbaseStepper.d.ts +51 -0
  60. package/dist/forms/index.d.ts +8 -0
  61. package/dist/forms/useArchbaseFormWizard.d.ts +74 -0
  62. package/dist/hooks/index.d.ts +2 -0
  63. package/dist/hooks/useArchbaseConfirm.d.ts +59 -0
  64. package/dist/image/editor/index.d.ts +43 -4
  65. package/dist/image/index.d.ts +1 -0
  66. package/dist/index.css +1 -1
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.js +22510 -13820
  69. package/dist/list/ArchbaseTreeList.d.ts +76 -0
  70. package/dist/list/index.d.ts +2 -0
  71. package/dist/navigation/ArchbaseActionSheet.d.ts +70 -0
  72. package/dist/navigation/ArchbaseAppBar.d.ts +63 -0
  73. package/dist/navigation/ArchbaseBottomNavigation.d.ts +61 -0
  74. package/dist/navigation/ArchbaseContextMenu.d.ts +76 -0
  75. package/dist/navigation/ArchbaseKeyboardShortcuts.d.ts +85 -0
  76. package/dist/navigation/ArchbaseSpotlight.d.ts +69 -0
  77. package/dist/navigation/index.d.ts +12 -0
  78. package/dist/notification/ArchbaseNotificationCenter.d.ts +56 -0
  79. package/dist/notification/index.d.ts +2 -0
  80. package/dist/onboarding/ArchbaseOnboardingTour.d.ts +27 -76
  81. package/dist/viewers/ArchbaseDocViewer.d.ts +40 -0
  82. package/dist/viewers/ArchbasePdfBuilder.d.ts +53 -0
  83. package/dist/viewers/ArchbaseXmlViewer.d.ts +36 -0
  84. package/dist/viewers/index.d.ts +6 -0
  85. package/package.json +41 -18
  86. package/src/buttons/ArchbaseSpeedDial.tsx +282 -0
  87. package/src/buttons/index.ts +3 -0
  88. package/src/charts/ArchbaseArcGauge.tsx +325 -0
  89. package/src/charts/ArchbaseGantt.tsx +494 -0
  90. package/src/charts/ArchbaseLinearGauge.tsx +288 -0
  91. package/src/charts/ArchbaseResourceTimeline.tsx +433 -0
  92. package/src/charts/ArchbaseSparkline.tsx +193 -0
  93. package/src/charts/index.ts +20 -1
  94. package/src/containers/ArchbaseExpansionPanel.tsx +208 -0
  95. package/src/containers/ArchbaseFloatingPanel.tsx +448 -0
  96. package/src/containers/index.ts +17 -0
  97. package/src/datagrid/components/pagination/grid-pagination.tsx +2 -2
  98. package/src/datagrid/export/ArchbaseDataGridExcelExport.tsx +293 -0
  99. package/src/datagrid/export/index.ts +8 -0
  100. package/src/datagrid/index.tsx +2 -0
  101. package/src/display/ArchbaseAnimationWrapper.tsx +272 -0
  102. package/src/display/ArchbaseBarcodeGenerator.tsx +303 -0
  103. package/src/display/ArchbaseCarousel.tsx +679 -0
  104. package/src/display/ArchbaseCodeViewer.tsx +271 -0
  105. package/src/display/ArchbaseDiffViewer.tsx +108 -0
  106. package/src/display/ArchbaseJsonTree.tsx +150 -0
  107. package/src/display/ArchbaseLightbox.tsx +279 -0
  108. package/src/display/ArchbaseMarquee.tsx +268 -0
  109. package/src/display/ArchbasePhotoAlbum.tsx +415 -0
  110. package/src/display/ArchbaseQRCode.tsx +137 -0
  111. package/src/display/ArchbaseTooltipRich.tsx +317 -0
  112. package/src/display/ArchbaseVirtualList.tsx +618 -0
  113. package/src/display/index.ts +94 -1
  114. package/src/editors/ArchbaseAIPromptInput.tsx +432 -0
  115. package/src/editors/ArchbaseBarcodeScanner.tsx +570 -0
  116. package/src/editors/ArchbaseCepInput.tsx +173 -0
  117. package/src/editors/ArchbaseColorGradientPicker.tsx +460 -0
  118. package/src/editors/ArchbaseCpfCnpjInput.tsx +193 -0
  119. package/src/editors/ArchbaseCurrencyInput.tsx +100 -0
  120. package/src/editors/ArchbaseDualListbox.tsx +671 -0
  121. package/src/editors/ArchbaseInPlaceEditor.tsx +406 -0
  122. package/src/editors/ArchbaseLightGrid.tsx +538 -0
  123. package/src/editors/ArchbaseMentionInput.tsx +384 -0
  124. package/src/editors/ArchbaseMultiEmail.tsx +367 -0
  125. package/src/editors/ArchbaseMultiViewCalendar.tsx +439 -0
  126. package/src/editors/ArchbaseNumberStepper.tsx +219 -0
  127. package/src/editors/ArchbaseOTPInput.tsx +133 -0
  128. package/src/editors/ArchbaseOperationHoursEditor.tsx +1 -1
  129. package/src/editors/ArchbasePhoneInput.tsx +355 -0
  130. package/src/editors/ArchbaseRangeSlider.tsx +109 -0
  131. package/src/editors/ArchbaseSignaturePad.tsx +425 -0
  132. package/src/editors/ArchbaseSpeechToTextInput.tsx +444 -0
  133. package/src/editors/ArchbaseSpreadsheet.tsx +388 -0
  134. package/src/editors/ArchbaseTagInput.tsx +326 -0
  135. package/src/editors/index.tsx +92 -0
  136. package/src/feedback/ArchbaseChunkProgressBar.tsx +156 -0
  137. package/src/feedback/ArchbaseRipple.tsx +278 -0
  138. package/src/feedback/ArchbaseSkeleton.tsx +419 -0
  139. package/src/feedback/index.ts +22 -1
  140. package/src/filters/ArchbaseColumnSelector.tsx +268 -0
  141. package/src/filters/index.ts +2 -0
  142. package/src/forms/ArchbaseFieldArray.tsx +305 -0
  143. package/src/forms/ArchbaseFormWizard.tsx +284 -0
  144. package/src/forms/ArchbaseStepper.tsx +137 -0
  145. package/src/forms/index.ts +24 -2
  146. package/src/forms/useArchbaseFormWizard.ts +283 -0
  147. package/src/hooks/index.ts +12 -0
  148. package/src/hooks/useArchbaseConfirm.ts +293 -0
  149. package/src/image/editor/functions/image-processing.ts +5 -0
  150. package/src/image/editor/index.tsx +305 -391
  151. package/src/image/index.ts +1 -0
  152. package/src/index.ts +3 -0
  153. package/src/list/ArchbaseTreeList.tsx +422 -0
  154. package/src/list/index.ts +3 -0
  155. package/src/navigation/ArchbaseActionSheet.tsx +301 -0
  156. package/src/navigation/ArchbaseAppBar.tsx +233 -0
  157. package/src/navigation/ArchbaseBottomNavigation.tsx +243 -0
  158. package/src/navigation/ArchbaseContextMenu.tsx +298 -0
  159. package/src/navigation/ArchbaseKeyboardShortcuts.tsx +472 -0
  160. package/src/navigation/ArchbaseSpotlight.tsx +483 -0
  161. package/src/navigation/index.ts +48 -1
  162. package/src/notification/ArchbaseNotificationCenter.tsx +502 -0
  163. package/src/notification/index.ts +2 -0
  164. package/src/onboarding/ArchbaseOnboardingTour.tsx +229 -102
  165. package/src/viewers/ArchbaseDocViewer.tsx +584 -0
  166. package/src/viewers/ArchbasePdfBuilder.tsx +291 -0
  167. package/src/viewers/ArchbaseXmlViewer.tsx +142 -0
  168. package/src/viewers/index.ts +12 -0
@@ -1,437 +1,351 @@
1
- /* eslint-disable react-hooks/exhaustive-deps */
2
- import { ActionIcon, ActionIconVariant, rem, Text, Tooltip, useMantineColorScheme } from '@mantine/core';
3
- import {
4
- IconCloudDownload,
5
- IconCloudUpload,
6
- IconPhotoDown,
7
- IconPhotoEdit,
8
- IconPhotoUp,
9
- IconPhotoX,
10
- IconTrash,
11
- } from '@tabler/icons-react';
1
+ /**
2
+ * ArchbaseImagePickerEditor - Wrapper para react-image-picker-editor
3
+ * Componente para seleção, edição e compressão de imagens em png, jpeg e webp
4
+ *
5
+ * Funcionalidades:
6
+ * - Suporta diferentes tipos de entrada: URL, data URI (base64), blob URL
7
+ * - Debounce para evitar múltiplas chamadas do callback
8
+ * - Mantém compatibilidade total com a API anterior
9
+ */
10
+ import { ActionIconVariant, Text, useMantineColorScheme } from '@mantine/core';
11
+ import { useDebouncedCallback } from '@mantine/hooks';
12
12
  import { useArchbaseTranslation } from '@archbase/core';
13
- import React, { Fragment, memo, useEffect, useMemo, useRef, useState } from 'react';
13
+ import React, { memo, useEffect, useMemo, useRef, useState, useCallback } from 'react';
14
14
  import { useArchbaseTheme } from '@archbase/core';
15
- import ArchbaseEditImage from './components/EditImage/ArchbaseEditImage';
16
- import { convertImageUsingCanvas } from './functions/image-processing';
15
+ import ReactImagePickerEditor, { ImagePickerConf } from 'react-image-picker-editor';
16
+ import 'react-image-picker-editor/dist/index.css';
17
17
  import './image_editor_styles.scss';
18
- import { ArchbaseImagePickerConf, IState } from './models/index.models';
18
+ import { ArchbaseImagePickerConf } from './models/index.models';
19
19
 
20
20
  export * from './models/index.models';
21
21
 
22
- const initialConfig: ArchbaseImagePickerConf = {
23
- objectFit: 'cover',
24
- hideDeleteBtn: false,
25
- hideDownloadBtn: false,
26
- hideEditBtn: false,
27
- hideAddBtn: false,
28
- compressInitial: null,
22
+ // Mapeamento de idiomas para o react-image-picker-editor
23
+ const LANGUAGE_MAP: Record<string, string> = {
24
+ 'pt-BR': 'pt',
25
+ 'pt': 'pt',
26
+ 'en': 'en',
27
+ 'en-US': 'en',
28
+ 'es': 'es',
29
+ 'es-ES': 'es',
30
+ 'fr': 'fr',
31
+ 'fr-FR': 'fr',
32
+ 'de': 'de',
33
+ 'de-DE': 'de',
34
+ 'fa': 'fa',
29
35
  };
30
36
 
31
- const initialState: IState = {
32
- maxHeight: 3000,
33
- maxWidth: 3000,
34
- cropHeight: 150,
35
- cropWidth: 150,
36
- maintainAspectRatio: true,
37
- format: 'png',
38
- arrayCopiedImages: [],
39
- originImageSrc: '',
40
- basicFilters: undefined,
41
- quality: 100,
42
- };
37
+ /**
38
+ * Verifica se uma string é um data URI válido (com prefixo data:image/...)
39
+ */
40
+ function isDataUri(str: string | null | undefined): boolean {
41
+ if (!str) return false;
42
+ return str.startsWith('data:');
43
+ }
44
+
45
+ /**
46
+ * Verifica se uma string é um blob URL
47
+ */
48
+ function isBlobUrl(str: string | null | undefined): boolean {
49
+ if (!str) return false;
50
+ return str.startsWith('blob:');
51
+ }
52
+
53
+ /**
54
+ * Verifica se uma string é uma URL externa (http/https)
55
+ */
56
+ function isExternalUrl(str: string | null | undefined): boolean {
57
+ if (!str) return false;
58
+ return str.startsWith('http://') || str.startsWith('https://');
59
+ }
60
+
61
+ /**
62
+ * Verifica se uma string parece ser base64 puro (sem prefixo data:)
63
+ * Base64 válido contém apenas A-Z, a-z, 0-9, +, /, = e tem comprimento múltiplo de 4
64
+ */
65
+ function isRawBase64(str: string | null | undefined): boolean {
66
+ if (!str || str.length < 100) return false; // Imagens base64 são geralmente grandes
67
+ // Verificar se não começa com prefixos conhecidos
68
+ if (isDataUri(str) || isBlobUrl(str) || isExternalUrl(str)) return false;
69
+ // Verificar padrão de base64
70
+ const base64Regex = /^[A-Za-z0-9+/]+=*$/;
71
+ // Verificar apenas os primeiros 100 caracteres para performance
72
+ return base64Regex.test(str.substring(0, 100));
73
+ }
74
+
75
+ /**
76
+ * Normaliza a imagem de entrada para um formato que o componente pode exibir
77
+ * Converte base64 puro para data URI se necessário
78
+ */
79
+ function normalizeImageSrc(src: string | null | undefined): string | null {
80
+ if (!src) return null;
81
+
82
+ // Se já é um formato válido, retornar como está
83
+ if (isDataUri(src) || isBlobUrl(src) || isExternalUrl(src)) {
84
+ return src;
85
+ }
86
+
87
+ // Se parece ser base64 puro, converter para data URI
88
+ if (isRawBase64(src)) {
89
+ // Tentar detectar o tipo da imagem pelo header base64
90
+ // JPEG começa com /9j/
91
+ // PNG começa com iVBOR
92
+ // GIF começa com R0lGOD
93
+ // WebP começa com UklGR
94
+ let mimeType = 'image/jpeg'; // Padrão
95
+ if (src.startsWith('/9j/')) {
96
+ mimeType = 'image/jpeg';
97
+ } else if (src.startsWith('iVBOR')) {
98
+ mimeType = 'image/png';
99
+ } else if (src.startsWith('R0lGOD')) {
100
+ mimeType = 'image/gif';
101
+ } else if (src.startsWith('UklGR')) {
102
+ mimeType = 'image/webp';
103
+ }
104
+ return `data:${mimeType};base64,${src}`;
105
+ }
106
+
107
+ // Se não reconhecemos o formato, retornar como está (pode ser uma URL relativa)
108
+ return src;
109
+ }
110
+
111
+ /**
112
+ * Converte a configuração do Archbase para o formato do react-image-picker-editor
113
+ */
114
+ function toImagePickerConf(
115
+ config: ArchbaseImagePickerConf,
116
+ colorScheme: 'light' | 'dark',
117
+ language: string
118
+ ): ImagePickerConf {
119
+ return {
120
+ width: typeof config.width === 'number' ? `${config.width}px` : (config.width as string),
121
+ height: typeof config.height === 'number' ? `${config.height}px` : (config.height as string),
122
+ borderRadius: typeof config.borderRadius === 'number' ? `${config.borderRadius}px` : (config.borderRadius as string),
123
+ aspectRatio: config.aspectRatio ?? undefined,
124
+ objectFit: config.objectFit,
125
+ compressInitial: config.compressInitial ?? undefined,
126
+ hideDeleteBtn: config.hideDeleteBtn,
127
+ hideDownloadBtn: config.hideDownloadBtn,
128
+ hideEditBtn: config.hideEditBtn,
129
+ hideAddBtn: config.hideAddBtn,
130
+ darkMode: colorScheme === 'dark',
131
+ language: LANGUAGE_MAP[language] || 'en',
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Calcula o tamanho da imagem em KB a partir de uma string base64
137
+ */
138
+ function calculateImageSize(dataUri: string | null | undefined): number | null {
139
+ if (!dataUri || !dataUri.length || !isDataUri(dataUri)) return null;
140
+ // Remove o header do data URI para calcular apenas o conteúdo base64
141
+ const base64 = dataUri.split(',')[1];
142
+ if (!base64) return null;
143
+ // Fórmula: (3/4) * length / 1024 para obter KB aproximado
144
+ return Math.ceil(((3 / 4) * base64.length) / 1024);
145
+ }
146
+
147
+ /**
148
+ * Extrai o formato da imagem do data URI
149
+ */
150
+ function extractFormat(dataUri: string | null | undefined): string {
151
+ if (!dataUri || !isDataUri(dataUri)) return '';
152
+ const match = dataUri.match(/data:image\/([a-zA-Z]+);base64,/);
153
+ if (match && match[1]) {
154
+ return match[1] === 'jpeg' ? 'jpeg' : match[1];
155
+ }
156
+ return 'png';
157
+ }
158
+
159
+ /**
160
+ * Props do ArchbaseImagePickerEditor
161
+ * Mantém compatibilidade total com a versão anterior
162
+ */
163
+ export interface ArchbaseImagePickerEditorProps {
164
+ /** Configuração do editor (interface ArchbaseImagePickerConf) */
165
+ config?: ArchbaseImagePickerConf;
166
+ /** Imagem inicial (data URI, URL externa ou blob URL) */
167
+ imageSrcProp?: string;
168
+ /** Cor de destaque */
169
+ color?: string;
170
+ /**
171
+ * Callback quando a imagem é alterada
172
+ * @deprecated Use config.onChangeImage ao invés
173
+ */
174
+ imageChanged?: (newDataUri: string | undefined) => void;
175
+ /** Variante dos botões de ação */
176
+ variant?: ActionIconVariant;
177
+ }
43
178
 
179
+ /**
180
+ * ArchbaseImagePickerEditor - Componente de seleção e edição de imagens
181
+ *
182
+ * Funcionalidades:
183
+ * - Seleção de imagens (upload)
184
+ * - Edição com crop, redimensionamento
185
+ * - Compressão de qualidade (JPEG/WebP)
186
+ * - Conversão de formatos (PNG, JPEG, WebP)
187
+ * - Filtros (contraste, brilho, saturação, sépia, blur)
188
+ * - Suporte a dark mode
189
+ * - Suporte a URL externa, data URI e blob URL
190
+ *
191
+ * @example
192
+ * ```tsx
193
+ * <ArchbaseImagePickerEditor
194
+ * imageSrcProp={imageValue}
195
+ * config={{
196
+ * width: 300,
197
+ * height: 200,
198
+ * compressInitial: 80,
199
+ * objectFit: 'contain',
200
+ * onChangeImage: (newImage) => setImageValue(newImage),
201
+ * }}
202
+ * />
203
+ * ```
204
+ */
44
205
  export const ArchbaseImagePickerEditor = memo(
45
206
  ({
46
207
  config = {},
47
208
  imageSrcProp = '',
48
209
  color = '#1e88e5',
49
- imageChanged = () => {},
210
+ imageChanged,
50
211
  variant = 'transparent',
51
- }: {
52
- config: ArchbaseImagePickerConf;
53
- imageSrcProp?: string;
54
- color?: string;
55
- imageChanged?: Function;
56
- variant?: ActionIconVariant
57
- }) => {
212
+ }: ArchbaseImagePickerEditorProps) => {
58
213
  const theme = useArchbaseTheme();
59
214
  const { colorScheme } = useMantineColorScheme();
60
- const { t } = useArchbaseTranslation();
61
- const [state, setState] = useState<IState>({
62
- ...initialState,
63
- });
64
- const [imageSrc, setImageSrc] = useState<string | null>('');
65
- const [loadImage, setLoadImage] = useState<boolean>(false);
66
- const [showEditPanel, setShowEditPanel] = useState<boolean>(false);
67
- const [configuration, setConfiguration] = useState<ArchbaseImagePickerConf>(initialConfig);
68
- const imagePicker = useRef<any>(null);
69
- const fileType = useRef('');
70
- const urlImage = useRef('');
71
- const uuidFilePicker = Date.now().toString(20);
215
+ const { t, i18n } = useArchbaseTranslation();
216
+
217
+ // Normalizar a imagem de entrada para formato suportado
218
+ const normalizedInitialSrc = useMemo(() => normalizeImageSrc(imageSrcProp), [imageSrcProp]);
219
+
220
+ const [imageSrc, setImageSrc] = useState<string | null>(normalizedInitialSrc);
221
+ const [loadImage, setLoadImage] = useState<boolean>(!!normalizedInitialSrc);
72
222
  const imageName = useRef('download');
73
- const mounted = useRef(false);
74
223
 
75
- useEffect(() => {
76
- processConfig();
77
- }, [config]);
224
+ // Ref para rastrear a última imagem reportada (evitar callbacks duplicados)
225
+ const lastReportedImageRef = useRef<string | null | undefined>(undefined);
78
226
 
79
- useEffect(() => {
80
- loadImageFromProps();
81
- }, [imageSrcProp]);
227
+ // Ref para a imagem original (antes de normalização) para comparação
228
+ const originalImagePropRef = useRef<string | undefined>(imageSrcProp);
82
229
 
83
- async function loadImageFromProps() {
84
- if (imageSrcProp) {
85
- const result = await parseToBase64(imageSrcProp);
86
- const newState: IState = result.state;
87
- newState.originImageSrc = imageSrcProp;
88
- newState.arrayCopiedImages = [
89
- {
90
- lastImage: result.imageUri,
91
- width: newState.maxWidth,
92
- height: newState.maxHeight,
93
- quality: newState.quality,
94
- format: newState.format,
95
- originImageSrc: imageSrcProp,
96
- },
97
- ];
98
- setImageSrc(result.imageUri);
99
- setState(newState);
100
- setLoadImage(true);
101
- } else {
102
- const newState = { ...state };
103
- newState.originImageSrc = null;
104
- newState.arrayCopiedImages = [];
105
- setLoadImage(false);
106
- setImageSrc(null);
107
- setState(newState);
108
- }
109
- }
230
+ // Ref para saber se é a primeira renderização
231
+ const isFirstRender = useRef(true);
110
232
 
111
- useEffect(() => {
112
- imageChanged(imageSrc);
113
- }, [imageSrc]);
233
+ // Configuração padrão
234
+ const defaultConfig: ArchbaseImagePickerConf = {
235
+ objectFit: 'cover',
236
+ hideDeleteBtn: false,
237
+ hideDownloadBtn: false,
238
+ hideEditBtn: false,
239
+ hideAddBtn: false,
240
+ compressInitial: null,
241
+ showImageSize: true,
242
+ width: 330,
243
+ height: 250,
244
+ borderRadius: '8px',
245
+ };
114
246
 
115
- function processConfig() {
116
- const dataConf = { ...configuration, ...config };
117
- setConfiguration(dataConf);
118
- }
247
+ const mergedConfig = useMemo(() => ({ ...defaultConfig, ...config }), [config]);
119
248
 
120
- function onUpload(event: any) {
121
- event.preventDefault();
122
- imagePicker?.current?.click();
123
- }
249
+ // Converter para configuração do react-image-picker-editor
250
+ const pickerConfig = useMemo(
251
+ () => toImagePickerConf(mergedConfig, colorScheme as 'light' | 'dark', i18n.language),
252
+ [mergedConfig, colorScheme, i18n.language]
253
+ );
124
254
 
125
- function handleFileSelect(this: typeof handleFileSelect, event: any) {
126
- const files = event.target?.files;
127
- if (files) {
128
- const file = files[0];
129
- imageName.current = file.name.split('.')[0];
130
- fileType.current = file.type;
131
- if (!fileType.current.includes('image')) return;
132
- urlImage.current = `data:${file.type};base64,`;
133
- if (file) {
134
- const reader = new FileReader();
135
- reader.onload = handleReaderLoaded.bind(this);
136
- reader.readAsBinaryString(file);
137
- }
255
+ // Callback com debounce para evitar múltiplas chamadas rápidas
256
+ const debouncedOnChange = useDebouncedCallback((newDataUri: string | undefined) => {
257
+ // Verificar se a imagem realmente mudou
258
+ if (lastReportedImageRef.current === newDataUri) {
259
+ return;
138
260
  }
139
- }
140
261
 
141
- async function handleReaderLoaded(readerEvt: any) {
142
- const binaryString = readerEvt.target.result;
143
- const base64textString = btoa(binaryString);
144
- let newState = { ...state };
145
- const newImageSrc = urlImage.current + base64textString;
146
- newState.originImageSrc = urlImage.current + base64textString;
147
- newState.format = fileType.current.split('image/')[1];
148
- if (configuration.compressInitial) {
149
- newState = {
150
- ...newState,
151
- quality: Math.min(configuration.compressInitial || 92, 100),
152
- maintainAspectRatio: true,
153
- format: newState.format ? newState.format : 'png',
154
- };
155
- const result = await convertImageUsingCanvas(newState.originImageSrc as string, false, newState, {
156
- getDimFromImage: true,
157
- });
158
- setState(result.state);
159
- setImageSrc(result.imageUri);
160
- setLoadImage(true);
161
- if (configuration.onChangeImage) {
162
- configuration.onChangeImage(result.imageUri);
163
- }
164
- } else {
165
- const img = document.createElement('img');
166
- img.src = newImageSrc;
167
- img.onload = () => {
168
- newState.arrayCopiedImages = [];
169
- newState.maxHeight = img.height;
170
- newState.maxWidth = img.width;
171
- newState.format = fileType.current.split('image/')[1];
172
- newState.arrayCopiedImages.push({
173
- lastImage: newImageSrc,
174
- width: img.width,
175
- height: img.height,
176
- quality: newState.quality,
177
- format: fileType.current.split('image/')[1],
178
- originImageSrc: newState.originImageSrc as string,
179
- });
180
- setState(newState);
181
- setImageSrc(newImageSrc);
182
- setLoadImage(true);
183
- if (configuration.onChangeImage) {
184
- configuration.onChangeImage(newImageSrc);
185
- }
186
- };
187
- }
188
- }
262
+ lastReportedImageRef.current = newDataUri;
189
263
 
190
- const sizeImage = useMemo(() => {
191
- if (imageSrc && imageSrc.length) {
192
- return Math.ceil(((3 / 4) * imageSrc.length) / 1024);
193
- } else {
194
- return '';
195
- }
196
- }, [imageSrc]);
197
-
198
- function parseToBase64(imageUrl: string): Promise<{ imageUri: string; state: IState }> {
199
- let newState = { ...state };
200
- let type: string;
201
-
202
- // Verifica se a URL é base64
203
- if (imageUrl.startsWith('data:image/')) {
204
- // Extrai o tipo do formato 'data:image/png;base64,'
205
- const matches = imageUrl.match(/data:image\/([a-zA-Z]+);base64,/);
206
- if (matches && matches[1]) {
207
- type = matches[1];
208
- } else {
209
- // Se não houver correspondência, define um tipo padrão
210
- type = 'png'; // ou qualquer tipo padrão que você preferir
211
- }
212
- } else {
213
- // Trata a URL como um caminho de arquivo
214
- const types = imageUrl.split('.');
215
- type = types[types.length - 1];
216
- if (!['png', 'jpeg', 'webp'].includes(type)) {
217
- type = 'png'; // ou qualquer tipo padrão que você preferir
218
- }
264
+ // Chamar callback do config (API principal)
265
+ if (mergedConfig.onChangeImage) {
266
+ mergedConfig.onChangeImage(newDataUri);
219
267
  }
220
268
 
221
- newState.format = type;
222
- if (config.compressInitial != null) {
223
- let quality = 1;
224
- if (config.compressInitial >= 0 && config.compressInitial <= 100) {
225
- quality = config.compressInitial;
226
- }
227
- newState.quality = quality;
269
+ // Chamar callback legado (para compatibilidade)
270
+ if (imageChanged) {
271
+ imageChanged(newDataUri);
228
272
  }
273
+ }, 300); // 300ms de debounce
229
274
 
230
- return new Promise((resolve, reject) => {
231
- const img = document.createElement('img');
232
- img.crossOrigin = 'Anonymous';
233
- newState.maxHeight = img.height;
234
- newState.maxWidth = img.width;
235
-
236
- img.onload = () => {
237
- const canvas = document.createElement('canvas');
238
- const ctx: any = canvas.getContext('2d');
239
- const ratio = 1.0;
240
- canvas.width = img.width * ratio;
241
- canvas.height = img.height * ratio;
242
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
243
- const dataURI = canvas.toDataURL(`image/${type}`, newState.quality / 100);
244
- return resolve({
245
- dataUri: dataURI,
246
- width: canvas.width,
247
- height: canvas.height,
248
- });
249
- };
250
- img.onerror = (e: any) => {
251
- return reject(e.message || `Error loading the src = ${imageUrl}`);
252
- };
253
- img.src = imageUrl;
254
- }).then((data: any) => {
255
- newState = {
256
- ...newState,
257
- maxHeight: data.height,
258
- maxWidth: data.width,
259
- };
260
- return { imageUri: data.dataUri, state: newState };
261
- });
262
- }
275
+ // Sincronizar com prop externa
276
+ useEffect(() => {
277
+ // atualizar se a prop original realmente mudou
278
+ if (imageSrcProp !== originalImagePropRef.current) {
279
+ originalImagePropRef.current = imageSrcProp;
263
280
 
264
- function onOpenEditPanel() {
265
- setShowEditPanel(true);
266
- }
281
+ const normalizedSrc = normalizeImageSrc(imageSrcProp);
282
+ setImageSrc(normalizedSrc);
283
+ setLoadImage(!!normalizedSrc);
267
284
 
268
- function onCloseEditPanel(data: any) {
269
- setShowEditPanel(false);
270
- if (data) {
271
- setState(data.state);
272
- setImageSrc(data.imageSrc);
273
- if (configuration.onChangeImage) {
274
- configuration.onChangeImage(data.imageSrc);
285
+ // Atualizar a referência sem disparar callback na sincronização inicial
286
+ if (isFirstRender.current) {
287
+ lastReportedImageRef.current = normalizedSrc;
288
+ isFirstRender.current = false;
275
289
  }
276
290
  }
277
- }
291
+ }, [imageSrcProp]);
278
292
 
279
- function onRemove() {
280
- setImageSrc(null);
281
- setLoadImage(false);
282
- const newState: IState = {
283
- ...state,
284
- ...initialState,
285
- };
286
- setState(newState);
287
- setShowEditPanel(false);
288
- if (configuration.onChangeImage) {
289
- configuration.onChangeImage(undefined);
293
+ // Handler quando a imagem muda no editor
294
+ const handleImageChanged = useCallback((newDataUri: string) => {
295
+ const newValue = newDataUri || undefined;
296
+
297
+ // Só processa se for diferente do valor atual
298
+ if (imageSrc === newDataUri) {
299
+ return;
290
300
  }
291
- }
301
+
302
+ setImageSrc(newDataUri || null);
303
+ setLoadImage(!!newDataUri);
304
+
305
+ // Usar callback com debounce
306
+ debouncedOnChange(newValue);
307
+ }, [imageSrc, debouncedOnChange]);
308
+
309
+ // Calcular tamanho e formato da imagem (apenas para data URIs)
310
+ const sizeImage = useMemo(() => calculateImageSize(imageSrc), [imageSrc]);
311
+ const format = useMemo(() => extractFormat(imageSrc), [imageSrc]);
312
+
313
+ // Verificar se deve mostrar informações de tamanho
314
+ const showSizeInfo = loadImage && sizeImage !== null && mergedConfig.showImageSize && isDataUri(imageSrc);
315
+
316
+ // Estilo do container
317
+ const containerStyle = useMemo(() => ({
318
+ width: typeof mergedConfig.width === 'number' ? `${mergedConfig.width}px` : mergedConfig.width,
319
+ backgroundColor: mergedConfig.imageBackgroundColor ?? (colorScheme === 'dark' ? theme.colors.dark[7] : theme.white),
320
+ }), [mergedConfig.width, mergedConfig.imageBackgroundColor, colorScheme, theme]);
292
321
 
293
322
  return (
294
- <div className="ArchbaseImagePickerEditor">
295
- {!loadImage && (
296
- <div className="place-image">
297
- <div
298
- className="image-holder"
299
- style={{
300
- width: configuration.width,
301
- height: configuration.height,
302
- borderRadius: configuration.borderRadius,
303
- aspectRatio: configuration.aspectRatio + '',
304
- backgroundColor: config.imageBackgroundColor ?? (colorScheme === 'dark' ? theme.colors.dark[7] : theme.white),
305
- border: `${rem(1)} solid ${colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[3]}`,
306
- }}
307
- >
308
- <Tooltip withinPortal withArrow label={`${t('archbase:Upload a image')}`}>
309
- <ActionIcon component="button" color="blue" variant={variant} className="icon-btn image-upload-btn" onClick={onUpload}>
310
- <IconCloudUpload color="purple" />
311
- </ActionIcon>
312
- </Tooltip>
313
- <input
314
- ref={imagePicker}
315
- type="file"
316
- style={{ display: 'none' }}
317
- id={'filePicker-' + uuidFilePicker}
318
- onChange={handleFileSelect}
319
- />
320
- </div>
321
- </div>
322
- )}
323
- {loadImage && (
324
- <div className="place-image">
325
- <div
326
- className="image-holder-loaded"
327
- style={{
328
- width: configuration.width,
329
- height: configuration.height,
330
- borderRadius: configuration.borderRadius,
331
- aspectRatio: configuration.aspectRatio + '',
332
- background: colorScheme === 'dark' ? theme.colors.dark[7] : theme.white,
333
- border: `${rem(1)} solid ${colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[3]}`,
334
- }}
335
- >
336
- <img
337
- src={imageSrc as string}
338
- alt="image-loaded"
339
- style={{
340
- height: configuration.height,
341
- borderRadius: configuration.borderRadius,
342
- objectFit: configuration.objectFit,
343
- backgroundColor: config.imageBackgroundColor ?? (colorScheme === 'dark' ? theme.colors.dark[7] : theme.white),
344
- }}
345
- />
346
- {!configuration.hideEditBtn && (
347
- <Fragment>
348
- <div className="curtain" onClick={onUpload}>
349
- <Tooltip withinPortal withArrow label={`${t('archbase:Upload a image')}`}>
350
- <ActionIcon component="button" color="blue" variant={variant}>
351
- <IconCloudUpload color="white" />
352
- </ActionIcon>
353
- </Tooltip>
354
- </div>
355
- <input
356
- ref={imagePicker}
357
- type="file"
358
- style={{ display: 'none' }}
359
- id={'filePicker-' + uuidFilePicker}
360
- onChange={handleFileSelect}
361
- />
362
- </Fragment>
363
- )}
364
- </div>
365
- {sizeImage && configuration.showImageSize && (
366
- <div
367
- className="caption image-caption"
368
- style={{
369
- color: sizeImage > 120 ? '#f44336' : 'unset',
370
- fontWeight: sizeImage > 120 ? '500' : 'unset',
371
- }}
372
- >
373
- <Text>{`${t('archbase:size')}: ${sizeImage}Kb ${state.format}`}</Text>
374
- </div>
375
- )}
376
-
377
- <div
378
- style={{
379
- flexDirection: 'row',
380
- boxSizing: 'border-box',
381
- display: 'flex',
382
- placeContent: 'flex-start',
383
- alignItems: 'flex-start',
384
- }}
385
- className="editing-bar-btn"
386
- >
387
- {!configuration.hideAddBtn && (
388
- <Tooltip withinPortal withArrow label={`${t('archbase:Upload a image')}`}>
389
- <ActionIcon id="upload-img" color="blue" variant={variant} onClick={onUpload}>
390
- <IconCloudUpload color="purple" />
391
- </ActionIcon>
392
- </Tooltip>
393
- )}
394
-
395
- {!configuration.hideEditBtn && (
396
- <Tooltip withinPortal withArrow label={`${t('archbase:Open the editor panel')}`}>
397
- <ActionIcon id="edit-img" color="blue" variant={variant} onClick={onOpenEditPanel}>
398
- <IconPhotoEdit color="teal" />
399
- </ActionIcon>
400
- </Tooltip>
401
- )}
402
- {!configuration.hideDownloadBtn && (
403
- <Tooltip withinPortal withArrow label={`${t('archbase:Download the image')}`}>
404
- <ActionIcon
405
- component="a"
406
- id="download-img"
407
- href={imageSrc as string}
408
- color="green"
409
- download={imageName.current}
410
- variant={variant}
411
- >
412
- <IconCloudDownload color="green" />
413
- </ActionIcon>
414
- </Tooltip>
415
- )}
416
- {!configuration.hideDeleteBtn && (
417
- <Tooltip withinPortal withArrow label={`${t('archbase:Remove')}`}>
418
- <ActionIcon id="delete-img" color="red" variant={variant} onClick={() => onRemove()}>
419
- <IconTrash color="#C91A25" />
420
- </ActionIcon>
421
- </Tooltip>
422
- )}
423
- </div>
323
+ <div className="ArchbaseImagePickerEditor" style={containerStyle}>
324
+ {/* Componente react-image-picker-editor */}
325
+ <ReactImagePickerEditor
326
+ config={pickerConfig}
327
+ imageSrcProp={imageSrc || ''}
328
+ imageChanged={handleImageChanged}
329
+ color={color}
330
+ />
331
+
332
+ {/* Informação de tamanho (quando habilitado, imagem carregada e é data URI) */}
333
+ {showSizeInfo && (
334
+ <div
335
+ className="caption image-caption"
336
+ style={{
337
+ color: sizeImage! > 120 ? '#f44336' : 'unset',
338
+ fontWeight: sizeImage! > 120 ? '500' : 'unset',
339
+ marginTop: '4px',
340
+ textAlign: 'center',
341
+ }}
342
+ >
343
+ <Text size="sm">{`${t('archbase:size')}: ${sizeImage}Kb ${format}`}</Text>
424
344
  </div>
425
345
  )}
426
- {showEditPanel && (
427
- <ArchbaseEditImage
428
- saveUpdates={onCloseEditPanel}
429
- color={color}
430
- image={imageSrc}
431
- initialState={state}
432
- ></ArchbaseEditImage>
433
- )}
434
346
  </div>
435
347
  );
436
348
  },
437
349
  );
350
+
351
+ ArchbaseImagePickerEditor.displayName = 'ArchbaseImagePickerEditor';