@archbase/components 4.0.17 → 4.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archbase/components",
3
- "version": "4.0.17",
3
+ "version": "4.0.19",
4
4
  "description": "UI Components for Archbase React v3 - Form editors, data visualization, and business components",
5
5
  "author": "Edson Martins <edsonmartins2005@gmail.com>",
6
6
  "license": "MIT",
@@ -87,7 +87,6 @@
87
87
  "react-frame-component": "^5.2.7",
88
88
  "react-hook-form": "^7.54.2",
89
89
  "react-i18next": "^13.5.0",
90
- "react-image-picker-editor": "^1.1.1",
91
90
  "react-imask": "^7.6.1",
92
91
  "react-inspector": "^9.0.0",
93
92
  "react-intersection-observer": "^9.10.3",
@@ -121,9 +120,9 @@
121
120
  "vis-timeline": "^7.7.3",
122
121
  "xlsx": "^0.18.5",
123
122
  "yet-another-react-lightbox": "^3.21.0",
124
- "@archbase/core": "4.0.17",
125
- "@archbase/data": "4.0.17",
126
- "@archbase/layout": "4.0.17"
123
+ "@archbase/core": "4.0.19",
124
+ "@archbase/data": "4.0.19",
125
+ "@archbase/layout": "4.0.19"
127
126
  },
128
127
  "devDependencies": {
129
128
  "@types/d3": "^7.4.3",
@@ -143,7 +142,7 @@
143
142
  "vitest": "^2.1.8"
144
143
  },
145
144
  "publishConfig": {
146
- "registry": "http://192.168.100.5:4873"
145
+ "registry": "http://192.168.1.110:4873"
147
146
  },
148
147
  "scripts": {
149
148
  "dev": "NODE_OPTIONS=\"--max-old-space-size=16384\" vite build --watch",
@@ -60,6 +60,26 @@ export interface ArchbaseImageEditProps<T, ID> extends ImageProps {
60
60
  maxHeight?: number;
61
61
  /** Tamanho máximo da imagem em KB (recomprime se exceder) */
62
62
  maxSizeKb?: number;
63
+ /**
64
+ * Preserva a transparência da imagem (canal alfa).
65
+ *
66
+ * Quando true:
67
+ * - desabilita a compressão automática (`compressInitial` é forçado para `null`),
68
+ * impedindo que a biblioteca subjacente reencodifique PNG/WebP como JPEG.
69
+ * - força a saída em PNG (ou WebP, quando a origem já é WebP) ao redimensionar
70
+ * via `maxWidth`/`maxHeight`/`maxSizeKb`.
71
+ *
72
+ * Útil quando o usuário precisa enviar logos com fundo transparente.
73
+ * Default: `false`.
74
+ */
75
+ preserveTransparency?: boolean;
76
+ /**
77
+ * Habilita logs detalhados do fluxo de processamento da imagem no console
78
+ * (formato detectado, decisões de compressão, mime de saída, tamanhos, etc).
79
+ * Útil para diagnosticar problemas como perda de transparência.
80
+ * Default: `false`.
81
+ */
82
+ debug?: boolean;
63
83
  }
64
84
 
65
85
  export function ArchbaseImageEdit<T, ID>({
@@ -77,7 +97,7 @@ export function ArchbaseImageEdit<T, ID>({
77
97
  radius = '4px',
78
98
  aspectRatio,
79
99
  objectFit = 'contain',
80
- compressInitial = 80,
100
+ compressInitial = null,
81
101
  onChangeImage,
82
102
  disabledBase64Convertion,
83
103
  innerRef,
@@ -87,8 +107,26 @@ export function ArchbaseImageEdit<T, ID>({
87
107
  maxWidth,
88
108
  maxHeight,
89
109
  maxSizeKb,
110
+ preserveTransparency = false,
111
+ debug = false,
90
112
  ...otherProps
91
113
  }: ArchbaseImageEditProps<T, ID>) {
114
+ // Log inicial das props relevantes — só quando elas realmente mudarem
115
+ // (evita "loop" de logs causado por re-renders do Mantine/dataSource).
116
+ useEffect(() => {
117
+ if (!debug) return;
118
+ // eslint-disable-next-line no-console
119
+ console.log('[ArchbaseImageEdit] props', {
120
+ dataField,
121
+ preserveTransparency,
122
+ compressInitial,
123
+ maxWidth,
124
+ maxHeight,
125
+ maxSizeKb,
126
+ disabledBase64Convertion,
127
+ effectiveCompressInitial: preserveTransparency ? null : compressInitial,
128
+ });
129
+ }, [debug, dataField, preserveTransparency, compressInitial, maxWidth, maxHeight, maxSizeKb, disabledBase64Convertion]);
92
130
  // 🔄 MIGRAÇÃO V1/V2: Hook de compatibilidade
93
131
  const v1v2Compatibility = useArchbaseV1V2Compatibility<string | undefined>(
94
132
  'ArchbaseImageEdit',
@@ -202,6 +240,19 @@ useEffect(() => {
202
240
  }, []);
203
241
 
204
242
  const handleChangeImage = (image: string | undefined) => {
243
+ if (debug) {
244
+ const head = typeof image === 'string' ? image.slice(0, 64) : image;
245
+ const mimeMatch = typeof image === 'string' ? image.match(/^data:(image\/[a-zA-Z+\-.]+);/) : null;
246
+ // eslint-disable-next-line no-console
247
+ console.log('[ArchbaseImageEdit] handleChangeImage', {
248
+ dataField,
249
+ incomingMime: mimeMatch?.[1] ?? '(no data uri)',
250
+ incomingHead: head,
251
+ lengthChars: image?.length ?? 0,
252
+ approxSizeKb: image ? Math.ceil(((3 / 4) * image.length) / 1024) : 0,
253
+ });
254
+ }
255
+
205
256
  // ✅ Limpa erro quando usuário edita o campo (tanto do estado local quanto do contexto)
206
257
  const hasError = internalError || contextError;
207
258
  if (hasError) {
@@ -265,7 +316,9 @@ useEffect(() => {
265
316
  width,
266
317
  height,
267
318
  objectFit,
268
- compressInitial,
319
+ // preserveTransparency desabilita a compressão automática
320
+ // (que internamente força conversão para JPEG)
321
+ compressInitial: preserveTransparency ? null : compressInitial,
269
322
  showImageSize: !isReadOnly(),
270
323
  hideDeleteBtn: isReadOnly(),
271
324
  hideDownloadBtn: isReadOnly(),
@@ -276,6 +329,8 @@ useEffect(() => {
276
329
  maxWidth,
277
330
  maxHeight,
278
331
  maxSizeKb,
332
+ preserveTransparency,
333
+ debug,
279
334
  }}
280
335
  />
281
336
  </Input.Wrapper>
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Utilidades de crop usadas pelo modal de edição do ArchbaseImagePickerEditor.
3
+ * Trabalham diretamente sobre data URIs, preservando o mime original (PNG/WebP
4
+ * mantêm alpha; JPEG continua JPEG). Não há "fallback silencioso" para JPEG.
5
+ *
6
+ * Algoritmo de crop com rotação adaptado do exemplo oficial do react-easy-crop:
7
+ * https://github.com/ValentinH/react-easy-crop/blob/master/example/utils.ts
8
+ */
9
+
10
+ export interface PixelCrop {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ }
16
+
17
+ /**
18
+ * Detecta o mime de saída do crop respeitando alpha:
19
+ * - se a origem é PNG ou WebP, ou se `preserveTransparency` está ligado,
20
+ * retornamos um mime com suporte a alpha (PNG ou WebP);
21
+ * - caso contrário, JPEG.
22
+ */
23
+ function pickOutputMime(sourceDataUri: string, preserveTransparency: boolean): string {
24
+ const match = sourceDataUri.match(/^data:(image\/[a-zA-Z+\-.]+);/);
25
+ const sourceMime = match?.[1] ?? '';
26
+ if (preserveTransparency || sourceMime === 'image/png' || sourceMime === 'image/webp') {
27
+ return sourceMime === 'image/webp' ? 'image/webp' : 'image/png';
28
+ }
29
+ return 'image/jpeg';
30
+ }
31
+
32
+ function loadImage(src: string): Promise<HTMLImageElement> {
33
+ return new Promise((resolve, reject) => {
34
+ const img = new Image();
35
+ img.crossOrigin = 'anonymous';
36
+ img.onload = () => resolve(img);
37
+ img.onerror = (e) => reject(e);
38
+ img.src = src;
39
+ });
40
+ }
41
+
42
+ function toRadians(deg: number): number {
43
+ return (deg * Math.PI) / 180;
44
+ }
45
+
46
+ /**
47
+ * Bounding box que comporta a imagem após rotação. Necessário para o canvas
48
+ * intermediário não cortar a imagem rotacionada.
49
+ */
50
+ function rotateSize(width: number, height: number, rotation: number): { width: number; height: number } {
51
+ const rotRad = toRadians(rotation);
52
+ return {
53
+ width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
54
+ height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Aplica rotação (em graus) + crop sobre `imageSrc` e retorna o data URI
60
+ * resultante.
61
+ *
62
+ * Estratégia:
63
+ * 1. Desenha a imagem rotacionada centralizada num canvas auxiliar do
64
+ * tamanho do bounding box rotacionado.
65
+ * 2. Faz drawImage do recorte (pixelCrop) desse canvas auxiliar para o
66
+ * canvas final do tamanho do recorte.
67
+ *
68
+ * Output mime respeita `preserveTransparency` e a origem (ver pickOutputMime).
69
+ */
70
+ export async function getCroppedImage(
71
+ imageSrc: string,
72
+ pixelCrop: PixelCrop,
73
+ rotation: number = 0,
74
+ preserveTransparency: boolean = false,
75
+ quality: number = 92,
76
+ ): Promise<string> {
77
+ const image = await loadImage(imageSrc);
78
+ const mime = pickOutputMime(imageSrc, preserveTransparency);
79
+
80
+ // 1) Canvas intermediário: imagem rotacionada centralizada.
81
+ const { width: bBoxW, height: bBoxH } = rotateSize(image.width, image.height, rotation);
82
+ const work = document.createElement('canvas');
83
+ work.width = Math.round(bBoxW);
84
+ work.height = Math.round(bBoxH);
85
+ const workCtx = work.getContext('2d');
86
+ if (!workCtx) throw new Error('Canvas 2D context not available');
87
+
88
+ workCtx.translate(bBoxW / 2, bBoxH / 2);
89
+ workCtx.rotate(toRadians(rotation));
90
+ workCtx.translate(-image.width / 2, -image.height / 2);
91
+ workCtx.drawImage(image, 0, 0);
92
+
93
+ // 2) Canvas final do tamanho do recorte (em pixels reais).
94
+ const out = document.createElement('canvas');
95
+ out.width = Math.round(pixelCrop.width);
96
+ out.height = Math.round(pixelCrop.height);
97
+ const outCtx = out.getContext('2d');
98
+ if (!outCtx) throw new Error('Canvas 2D context not available');
99
+
100
+ outCtx.drawImage(
101
+ work,
102
+ Math.round(pixelCrop.x),
103
+ Math.round(pixelCrop.y),
104
+ Math.round(pixelCrop.width),
105
+ Math.round(pixelCrop.height),
106
+ 0,
107
+ 0,
108
+ Math.round(pixelCrop.width),
109
+ Math.round(pixelCrop.height),
110
+ );
111
+
112
+ // PNG ignora o parâmetro de qualidade; passamos mesmo assim para JPEG/WebP.
113
+ return out.toDataURL(mime, quality / 100);
114
+ }
115
+
116
+ /**
117
+ * Redimensiona um data URI para respeitar maxWidth/maxHeight, preservando
118
+ * proporção e mime apropriado. Usado fora do fluxo de crop, ao subir um arquivo.
119
+ */
120
+ export async function resizeDataUri(
121
+ dataUri: string,
122
+ maxWidth?: number,
123
+ maxHeight?: number,
124
+ maxSizeKb?: number,
125
+ quality: number = 92,
126
+ preserveTransparency: boolean = false,
127
+ ): Promise<string> {
128
+ if (!maxWidth && !maxHeight && !maxSizeKb) return dataUri;
129
+
130
+ const img = await loadImage(dataUri);
131
+ let targetWidth = img.width;
132
+ let targetHeight = img.height;
133
+
134
+ if (maxWidth && img.width > maxWidth) {
135
+ const ratio = maxWidth / img.width;
136
+ targetWidth = maxWidth;
137
+ targetHeight = Math.round(img.height * ratio);
138
+ }
139
+ if (maxHeight && targetHeight > maxHeight) {
140
+ const ratio = maxHeight / targetHeight;
141
+ targetHeight = maxHeight;
142
+ targetWidth = Math.round(targetWidth * ratio);
143
+ }
144
+
145
+ if (targetWidth === img.width && targetHeight === img.height && !maxSizeKb) {
146
+ return dataUri;
147
+ }
148
+
149
+ const canvas = document.createElement('canvas');
150
+ canvas.width = targetWidth;
151
+ canvas.height = targetHeight;
152
+ const ctx = canvas.getContext('2d');
153
+ if (!ctx) return dataUri;
154
+ ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
155
+
156
+ const mime = pickOutputMime(dataUri, preserveTransparency);
157
+
158
+ const compressToSize = (currentQuality: number): string => {
159
+ const result = canvas.toDataURL(mime, currentQuality / 100);
160
+ if (maxSizeKb) {
161
+ const base64 = result.split(',')[1] ?? '';
162
+ const sizeKb = Math.ceil(((3 / 4) * base64.length) / 1024);
163
+ if (sizeKb > maxSizeKb && currentQuality > 20 && mime !== 'image/png') {
164
+ // PNG é lossless; reduzir quality não diminui o tamanho.
165
+ return compressToSize(currentQuality - 10);
166
+ }
167
+ }
168
+ return result;
169
+ };
170
+
171
+ return compressToSize(quality);
172
+ }