@skalfa/skalfa-component 1.0.52 → 1.0.53

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.
@@ -17,6 +17,8 @@ export declare const InputImageComponent: React.FC<InputImageProps>;
17
17
  interface CropperProps {
18
18
  src: string;
19
19
  aspect: number;
20
+ fileName?: string;
21
+ fileType?: string;
20
22
  onDone?: (file: File) => void;
21
23
  }
22
24
  export declare const CanvasCropper: React.FC<CropperProps>;
@@ -16,6 +16,7 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
16
16
  const [preview, setPreview] = (0, react_1.useState)("");
17
17
  const [drag, setDrag] = (0, react_1.useState)(false);
18
18
  const [cropSrc, setCropSrc] = (0, react_1.useState)(null);
19
+ const [cropFile, setCropFile] = (0, react_1.useState)(null);
19
20
  const [openCrop, setOpenCrop] = (0, react_1.useState)(false);
20
21
  const inputHandler = (0, _utils_1.useInputHandler)(name, value, validations, register, true, unregister);
21
22
  const [invalidMessage, setInvalidMessage] = (0, _utils_1.useValidation)(inputHandler.value, validations, invalid, inputHandler.idle);
@@ -35,6 +36,7 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
35
36
  }
36
37
  }, [value]);
37
38
  const openCropper = (file) => {
39
+ setCropFile(file);
38
40
  const reader = new FileReader();
39
41
  reader.onload = () => {
40
42
  setCropSrc(reader.result);
@@ -68,10 +70,14 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
68
70
  };
69
71
  const onCropCancel = () => {
70
72
  setOpenCrop(false);
73
+ setCropFile(null);
74
+ setCropSrc(null);
71
75
  inputRef.current.value = "";
72
76
  };
73
77
  const remove = () => {
74
78
  setPreview("");
79
+ setCropFile(null);
80
+ setCropSrc(null);
75
81
  onChange?.(null);
76
82
  inputRef.current && (inputRef.current.value = "");
77
83
  };
@@ -99,12 +105,11 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
99
105
  }, onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => {
100
106
  e.preventDefault();
101
107
  setDrag(true);
102
- }, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [preview && !containerAspect ? ((0, jsx_runtime_1.jsxs)("div", { className: "relative group w-full h-full flex items-center justify-center", children: [(0, jsx_runtime_1.jsx)("img", { src: preview, alt: "Preview", className: "w-full h-auto max-h-[400px] object-contain rounded-lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/images", className: "text-3xl mb-2" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-center", children: "Klik atau seret untuk ganti Gambar" })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] })), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-fit max-w-[90vw]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) }))] }));
108
+ }, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [preview && !containerAspect ? ((0, jsx_runtime_1.jsxs)("div", { className: "relative group w-full h-full flex items-center justify-center", children: [(0, jsx_runtime_1.jsx)("img", { src: preview, alt: "Preview", className: "w-full h-auto max-h-[400px] object-contain rounded-lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/images", className: "text-3xl mb-2" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-center", children: "Klik atau seret untuk ganti Gambar" })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] })), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-fit max-w-[90vw]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), fileName: cropFile?.name, fileType: cropFile?.type, onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), fileName: cropFile?.name, fileType: cropFile?.type, onDone: onCropDone }) })) }))] }));
103
109
  };
104
110
  exports.InputImageComponent = InputImageComponent;
105
- const CanvasCropper = ({ src, aspect, onDone, }) => {
111
+ const CanvasCropper = ({ src, aspect, fileName, fileType, onDone, }) => {
106
112
  const canvasRef = (0, react_1.useRef)(null);
107
- const previewRef = (0, react_1.useRef)(null);
108
113
  const pinchDistRef = (0, react_1.useRef)(0);
109
114
  const pinchStartZoomRef = (0, react_1.useRef)(1);
110
115
  const [img, setImg] = (0, react_1.useState)(null);
@@ -130,8 +135,8 @@ const CanvasCropper = ({ src, aspect, onDone, }) => {
130
135
  canvas.width = CROP_W;
131
136
  canvas.height = CROP_SIZE;
132
137
  ctx.clearRect(0, 0, canvas.width, canvas.height);
133
- const scaledW = img.width * zoom;
134
- const scaledH = img.height * zoom;
138
+ const scaledW = img.naturalWidth * zoom;
139
+ const scaledH = img.naturalHeight * zoom;
135
140
  const x = pos.x - scaledW / 2 + CROP_W / 2;
136
141
  const y = pos.y - scaledH / 2 + CROP_SIZE / 2;
137
142
  ctx.drawImage(img, x, y, scaledW, scaledH);
@@ -178,19 +183,50 @@ const CanvasCropper = ({ src, aspect, onDone, }) => {
178
183
  const performCrop = () => {
179
184
  if (!img)
180
185
  return;
181
- const preview = previewRef.current;
182
- const pctx = preview.getContext("2d");
183
- preview.width = CROP_W;
184
- preview.height = CROP_SIZE;
185
- const scaledW = img.width * zoom;
186
- const scaledH = img.height * zoom;
187
- const x = pos.x - scaledW / 2 + CROP_W / 2;
188
- const y = pos.y - scaledH / 2 + CROP_SIZE / 2;
189
- pctx.drawImage(img, x, y, scaledW, scaledH);
190
- preview.toBlob((blob) => {
191
- const file = new File([blob], "cropped.png", { type: "image/png" });
186
+ const scaleFactor = 1 / zoom;
187
+ const scaledW = img.naturalWidth * zoom;
188
+ const scaledH = img.naturalHeight * zoom;
189
+ const cropDisplayX = (scaledW - CROP_W) / 2 - pos.x;
190
+ const cropDisplayY = (scaledH - CROP_SIZE) / 2 - pos.y;
191
+ const sourceX = cropDisplayX * scaleFactor;
192
+ const sourceY = cropDisplayY * scaleFactor;
193
+ const sourceW = CROP_W * scaleFactor;
194
+ const sourceH = CROP_SIZE * scaleFactor;
195
+ const sx = Math.max(0, Math.min(img.naturalWidth - 1, sourceX));
196
+ const sy = Math.max(0, Math.min(img.naturalHeight - 1, sourceY));
197
+ const sw = Math.min(sourceW, img.naturalWidth - sx);
198
+ const sh = Math.min(sourceH, img.naturalHeight - sy);
199
+ let targetW = Math.round(sw);
200
+ let targetH = Math.round(sh);
201
+ const MAX_DIMENSION = 2560;
202
+ if (targetW > MAX_DIMENSION || targetH > MAX_DIMENSION) {
203
+ if (targetW >= targetH) {
204
+ targetH = Math.round((targetH * MAX_DIMENSION) / targetW);
205
+ targetW = MAX_DIMENSION;
206
+ }
207
+ else {
208
+ targetW = Math.round((targetW * MAX_DIMENSION) / targetH);
209
+ targetH = MAX_DIMENSION;
210
+ }
211
+ }
212
+ const outputCanvas = document.createElement("canvas");
213
+ outputCanvas.width = targetW;
214
+ outputCanvas.height = targetH;
215
+ const octx = outputCanvas.getContext("2d");
216
+ if (!octx)
217
+ return;
218
+ octx.imageSmoothingEnabled = true;
219
+ octx.imageSmoothingQuality = "high";
220
+ octx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
221
+ const type = fileType || "image/jpeg";
222
+ const name = fileName || (type === "image/png" ? "cropped.png" : "cropped.jpg");
223
+ const quality = type === "image/png" ? undefined : 0.92;
224
+ outputCanvas.toBlob((blob) => {
225
+ if (!blob)
226
+ return;
227
+ const file = new File([blob], name, { type, lastModified: Date.now() });
192
228
  onDone?.(file);
193
- });
229
+ }, type, quality);
194
230
  };
195
231
  const handleWheel = (e) => {
196
232
  e.preventDefault();
@@ -276,6 +312,6 @@ const CanvasCropper = ({ src, aspect, onDone, }) => {
276
312
  setDragging(false);
277
313
  pinchDistRef.current = 0;
278
314
  };
279
- return ((0, jsx_runtime_1.jsxs)("div", { style: { touchAction: "none" }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-wrapper", style: { width: CROP_W, height: CROP_SIZE }, onMouseMove: handleMove, onMouseUp: handleUp, onMouseLeave: handleUp, onWheel: handleWheel, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [(0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, width: CROP_W, height: CROP_SIZE, onMouseDown: handleDown, className: "cursor-move" }), (0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-grid-container", style: { width: CROP_W, height: CROP_SIZE }, children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-border" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-2" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-2" })] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-4 pb-2", children: (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { label: "Selesai", variant: "outline", onClick: performCrop, block: true }) }), (0, jsx_runtime_1.jsx)("canvas", { ref: previewRef, className: "hidden" })] }));
315
+ return ((0, jsx_runtime_1.jsxs)("div", { style: { touchAction: "none" }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-wrapper", style: { width: CROP_W, height: CROP_SIZE }, onMouseMove: handleMove, onMouseUp: handleUp, onMouseLeave: handleUp, onWheel: handleWheel, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [(0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, width: CROP_W, height: CROP_SIZE, onMouseDown: handleDown, className: "cursor-move" }), (0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-grid-container", style: { width: CROP_W, height: CROP_SIZE }, children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-border" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-2" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-2" })] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-4 pb-2", children: (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { label: "Selesai", variant: "outline", onClick: performCrop, block: true }) })] }));
280
316
  };
281
317
  exports.CanvasCropper = CanvasCropper;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skalfa/skalfa-component",
3
- "version": "1.0.52",
3
+ "version": "1.0.53",
4
4
  "description": "Reusable UI components for Skalfa App.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -54,6 +54,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
54
54
  const [preview, setPreview] = useState("");
55
55
  const [drag, setDrag] = useState(false);
56
56
  const [cropSrc, setCropSrc] = useState<string | null>(null);
57
+ const [cropFile, setCropFile] = useState<File | null>(null);
57
58
  const [openCrop, setOpenCrop] = useState(false);
58
59
 
59
60
  const inputHandler = useInputHandler(name, value, validations, register, true, unregister);
@@ -77,6 +78,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
77
78
  }, [value]);
78
79
 
79
80
  const openCropper = (file: File) => {
81
+ setCropFile(file);
80
82
  const reader = new FileReader();
81
83
  reader.onload = () => {
82
84
  setCropSrc(reader.result as string);
@@ -113,11 +115,15 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
113
115
 
114
116
  const onCropCancel = () => {
115
117
  setOpenCrop(false);
118
+ setCropFile(null);
119
+ setCropSrc(null);
116
120
  inputRef.current!.value = "";
117
121
  };
118
122
 
119
123
  const remove = () => {
120
124
  setPreview("");
125
+ setCropFile(null);
126
+ setCropSrc(null);
121
127
  onChange?.(null);
122
128
  inputRef.current && (inputRef.current.value = "");
123
129
  };
@@ -243,6 +249,8 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
243
249
  <CanvasCropper
244
250
  src={cropSrc}
245
251
  aspect={eval(currentAspect.replace(":", "/"))}
252
+ fileName={cropFile?.name}
253
+ fileType={cropFile?.type}
246
254
  onDone={onCropDone}
247
255
  />
248
256
  </div>
@@ -255,6 +263,8 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
255
263
  <CanvasCropper
256
264
  src={cropSrc}
257
265
  aspect={eval(currentAspect.replace(":", "/"))}
266
+ fileName={cropFile?.name}
267
+ fileType={cropFile?.type}
258
268
  onDone={onCropDone}
259
269
  />
260
270
  </div>
@@ -269,18 +279,21 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
269
279
 
270
280
 
271
281
  interface CropperProps {
272
- src : string;
273
- aspect : number;
274
- onDone ?: (file: File) => void;
282
+ src : string;
283
+ aspect : number;
284
+ fileName ?: string;
285
+ fileType ?: string;
286
+ onDone ?: (file: File) => void;
275
287
  }
276
288
 
277
289
  export const CanvasCropper: React.FC<CropperProps> = ({
278
290
  src,
279
291
  aspect,
292
+ fileName,
293
+ fileType,
280
294
  onDone,
281
295
  }) => {
282
296
  const canvasRef = useRef<HTMLCanvasElement>(null);
283
- const previewRef = useRef<HTMLCanvasElement>(null);
284
297
  const pinchDistRef = useRef(0);
285
298
  const pinchStartZoomRef = useRef(1);
286
299
 
@@ -314,8 +327,8 @@ export const CanvasCropper: React.FC<CropperProps> = ({
314
327
 
315
328
  ctx.clearRect(0, 0, canvas.width, canvas.height);
316
329
 
317
- const scaledW = img.width * zoom;
318
- const scaledH = img.height * zoom;
330
+ const scaledW = img.naturalWidth * zoom;
331
+ const scaledH = img.naturalHeight * zoom;
319
332
 
320
333
  const x = pos.x - scaledW / 2 + CROP_W / 2;
321
334
  const y = pos.y - scaledH / 2 + CROP_SIZE / 2;
@@ -376,24 +389,63 @@ export const CanvasCropper: React.FC<CropperProps> = ({
376
389
  const performCrop = () => {
377
390
  if (!img) return;
378
391
 
379
- const preview = previewRef.current!;
380
- const pctx = preview.getContext("2d")!;
392
+ const scaleFactor = 1 / zoom;
381
393
 
382
- preview.width = CROP_W;
383
- preview.height = CROP_SIZE;
394
+ const scaledW = img.naturalWidth * zoom;
395
+ const scaledH = img.naturalHeight * zoom;
384
396
 
385
- const scaledW = img.width * zoom;
386
- const scaledH = img.height * zoom;
397
+ const cropDisplayX = (scaledW - CROP_W) / 2 - pos.x;
398
+ const cropDisplayY = (scaledH - CROP_SIZE) / 2 - pos.y;
387
399
 
388
- const x = pos.x - scaledW / 2 + CROP_W / 2;
389
- const y = pos.y - scaledH / 2 + CROP_SIZE / 2;
400
+ const sourceX = cropDisplayX * scaleFactor;
401
+ const sourceY = cropDisplayY * scaleFactor;
402
+ const sourceW = CROP_W * scaleFactor;
403
+ const sourceH = CROP_SIZE * scaleFactor;
390
404
 
391
- pctx.drawImage(img, x, y, scaledW, scaledH);
405
+ const sx = Math.max(0, Math.min(img.naturalWidth - 1, sourceX));
406
+ const sy = Math.max(0, Math.min(img.naturalHeight - 1, sourceY));
407
+ const sw = Math.min(sourceW, img.naturalWidth - sx);
408
+ const sh = Math.min(sourceH, img.naturalHeight - sy);
392
409
 
393
- preview.toBlob((blob) => {
394
- const file = new File([blob!], "cropped.png", { type: "image/png" });
395
- onDone?.(file);
396
- });
410
+ let targetW = Math.round(sw);
411
+ let targetH = Math.round(sh);
412
+
413
+ const MAX_DIMENSION = 2560;
414
+ if (targetW > MAX_DIMENSION || targetH > MAX_DIMENSION) {
415
+ if (targetW >= targetH) {
416
+ targetH = Math.round((targetH * MAX_DIMENSION) / targetW);
417
+ targetW = MAX_DIMENSION;
418
+ } else {
419
+ targetW = Math.round((targetW * MAX_DIMENSION) / targetH);
420
+ targetH = MAX_DIMENSION;
421
+ }
422
+ }
423
+
424
+ const outputCanvas = document.createElement("canvas");
425
+ outputCanvas.width = targetW;
426
+ outputCanvas.height = targetH;
427
+
428
+ const octx = outputCanvas.getContext("2d");
429
+ if (!octx) return;
430
+
431
+ octx.imageSmoothingEnabled = true;
432
+ octx.imageSmoothingQuality = "high";
433
+
434
+ octx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
435
+
436
+ const type = fileType || "image/jpeg";
437
+ const name = fileName || (type === "image/png" ? "cropped.png" : "cropped.jpg");
438
+ const quality = type === "image/png" ? undefined : 0.92;
439
+
440
+ outputCanvas.toBlob(
441
+ (blob) => {
442
+ if (!blob) return;
443
+ const file = new File([blob], name, { type, lastModified: Date.now() });
444
+ onDone?.(file);
445
+ },
446
+ type,
447
+ quality
448
+ );
397
449
  };
398
450
 
399
451
 
@@ -557,8 +609,6 @@ export const CanvasCropper: React.FC<CropperProps> = ({
557
609
  block
558
610
  />
559
611
  </div>
560
-
561
- <canvas ref={previewRef} className="hidden" />
562
612
  </div>
563
613
  );
564
614
  };