@ews-admin/global-design-system 1.17.1 → 1.18.0

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 (31) hide show
  1. package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts +1 -1
  2. package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts.map +1 -1
  3. package/dist/components/ImageCropModal/ImageCropModal.d.ts +28 -0
  4. package/dist/components/ImageCropModal/ImageCropModal.d.ts.map +1 -0
  5. package/dist/components/ImageCropModal/cropImage.d.ts +3 -0
  6. package/dist/components/ImageCropModal/cropImage.d.ts.map +1 -0
  7. package/dist/components/ImageCropModal/index.d.ts +3 -0
  8. package/dist/components/ImageCropModal/index.d.ts.map +1 -0
  9. package/dist/components/Logo/Logo.d.ts +1 -1
  10. package/dist/components/Logo/Logo.d.ts.map +1 -1
  11. package/dist/components/Modal/Modal.d.ts +1 -1
  12. package/dist/components/Modal/Modal.d.ts.map +1 -1
  13. package/dist/components/MultiSearchAutocomplete/MultiSearchAutocomplete.d.ts +1 -1
  14. package/dist/components/MultiSearchAutocomplete/MultiSearchAutocomplete.d.ts.map +1 -1
  15. package/dist/components/RegionSelect/RegionSelect.d.ts +2 -1
  16. package/dist/components/RegionSelect/RegionSelect.d.ts.map +1 -1
  17. package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts +2 -1
  18. package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts.map +1 -1
  19. package/dist/index.css +1 -1
  20. package/dist/index.d.ts +69 -42
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.esm.css +1 -1
  23. package/dist/index.esm.js +1393 -12
  24. package/dist/index.esm.js.map +1 -1
  25. package/dist/index.js +1410 -10
  26. package/dist/index.js.map +1 -1
  27. package/package.json +3 -2
  28. package/src/components/ImageCropModal/ImageCropModal.tsx +110 -0
  29. package/src/components/ImageCropModal/cropImage.ts +48 -0
  30. package/src/components/ImageCropModal/index.ts +2 -0
  31. package/src/index.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ews-admin/global-design-system",
3
- "version": "1.17.1",
3
+ "version": "1.18.0",
4
4
  "description": "EWS Global Design System - Reusable components for EWS applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -62,7 +62,8 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "clsx": "^2.0.0",
65
- "lucide-react": "^0.544.0"
65
+ "lucide-react": "^0.544.0",
66
+ "react-easy-crop": "^6.0.2"
66
67
  },
67
68
  "scripts": {
68
69
  "build": "rollup -c",
@@ -0,0 +1,110 @@
1
+ import React, { useCallback, useState } from "react";
2
+ // react-easy-crop's .d.ts uses `export type *` which Node moduleResolution
3
+ // can't resolve as a value. Cast to any here — we own all props passed below.
4
+ import EasyCrop from "react-easy-crop";
5
+ import type { Area, Point } from "react-easy-crop";
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ const Cropper = EasyCrop as unknown as React.ComponentType<any>;
9
+
10
+ import { Modal } from "../Modal/Modal";
11
+ import { getCroppedFile } from "./cropImage";
12
+
13
+ export interface ImageCropModalProps {
14
+ /** Whether the modal is open */
15
+ isOpen: boolean;
16
+ /** Object URL of the image to crop (created with URL.createObjectURL) */
17
+ imageSrc: string;
18
+ /** Original file name — used to derive the extension of the cropped file */
19
+ originalFileName: string;
20
+ /**
21
+ * Locked aspect ratio for the crop area.
22
+ * Pass 1 for 1:1 (profile photos), 3 for 3:1 (banners), 4/3, etc.
23
+ * Defaults to 1 (square).
24
+ */
25
+ aspectRatio?: number;
26
+ /** Called when the user cancels or closes the modal */
27
+ onClose: () => void;
28
+ /**
29
+ * Called with the cropped File after the user confirms.
30
+ * The File is a JPEG produced from the selected crop region.
31
+ */
32
+ onCropComplete: (croppedFile: File) => void;
33
+ // ── Label props (pass translated strings from the consuming app) ──
34
+ title?: string;
35
+ confirmLabel?: string;
36
+ cancelLabel?: string;
37
+ zoomLabel?: string;
38
+ }
39
+
40
+ export const ImageCropModal: React.FC<ImageCropModalProps> = ({
41
+ isOpen,
42
+ imageSrc,
43
+ originalFileName,
44
+ aspectRatio = 1,
45
+ onClose,
46
+ onCropComplete,
47
+ title = "Crop image",
48
+ confirmLabel = "Crop & Upload",
49
+ cancelLabel = "Cancel",
50
+ zoomLabel = "Zoom",
51
+ }) => {
52
+ const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
53
+ const [zoom, setZoom] = useState(1);
54
+ const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
55
+ const [isCropping, setIsCropping] = useState(false);
56
+
57
+ const handleCropComplete = useCallback((_: Area, pixels: Area) => {
58
+ setCroppedAreaPixels(pixels);
59
+ }, []);
60
+
61
+ const handleConfirm = async () => {
62
+ if (!croppedAreaPixels) return;
63
+ setIsCropping(true);
64
+ try {
65
+ const file = await getCroppedFile(imageSrc, croppedAreaPixels, originalFileName);
66
+ onCropComplete(file);
67
+ } finally {
68
+ setIsCropping(false);
69
+ }
70
+ };
71
+
72
+ return (
73
+ <Modal
74
+ isOpen={isOpen}
75
+ onClose={onClose}
76
+ title={title}
77
+ size="lg"
78
+ primaryAction={confirmLabel}
79
+ secondaryAction={cancelLabel}
80
+ onPrimaryAction={handleConfirm}
81
+ onSecondaryAction={onClose}
82
+ isLoading={isCropping}
83
+ closeOnOverlayClick={false}
84
+ >
85
+ <div className="relative h-80 w-full bg-gray-900">
86
+ <Cropper
87
+ image={imageSrc}
88
+ crop={crop}
89
+ zoom={zoom}
90
+ aspect={aspectRatio}
91
+ onCropChange={setCrop}
92
+ onZoomChange={setZoom}
93
+ onCropComplete={handleCropComplete}
94
+ />
95
+ </div>
96
+ <div className="mt-4 px-1">
97
+ <label className="mb-1 block text-sm text-gray-500">{zoomLabel}</label>
98
+ <input
99
+ type="range"
100
+ min={1}
101
+ max={3}
102
+ step={0.05}
103
+ value={zoom}
104
+ onChange={(e) => setZoom(Number(e.target.value))}
105
+ className="w-full accent-ews-primary"
106
+ />
107
+ </div>
108
+ </Modal>
109
+ );
110
+ };
@@ -0,0 +1,48 @@
1
+ import type { Area } from "react-easy-crop";
2
+
3
+ export async function getCroppedFile(
4
+ imageSrc: string,
5
+ pixelCrop: Area,
6
+ originalFileName: string,
7
+ ): Promise<File> {
8
+ const image = await loadImage(imageSrc);
9
+ const canvas = document.createElement("canvas");
10
+ canvas.width = pixelCrop.width;
11
+ canvas.height = pixelCrop.height;
12
+
13
+ const ctx = canvas.getContext("2d");
14
+ if (!ctx) throw new Error("Could not get 2d canvas context");
15
+
16
+ ctx.drawImage(
17
+ image,
18
+ pixelCrop.x,
19
+ pixelCrop.y,
20
+ pixelCrop.width,
21
+ pixelCrop.height,
22
+ 0,
23
+ 0,
24
+ pixelCrop.width,
25
+ pixelCrop.height,
26
+ );
27
+
28
+ return new Promise((resolve, reject) => {
29
+ canvas.toBlob((blob) => {
30
+ if (!blob) {
31
+ reject(new Error("canvas.toBlob produced no output"));
32
+ return;
33
+ }
34
+ const ext =
35
+ originalFileName.substring(originalFileName.lastIndexOf(".")) || ".jpg";
36
+ resolve(new File([blob], `cropped${ext}`, { type: "image/jpeg" }));
37
+ }, "image/jpeg");
38
+ });
39
+ }
40
+
41
+ function loadImage(src: string): Promise<HTMLImageElement> {
42
+ return new Promise((resolve, reject) => {
43
+ const img = new Image();
44
+ img.addEventListener("load", () => resolve(img));
45
+ img.addEventListener("error", reject);
46
+ img.src = src;
47
+ });
48
+ }
@@ -0,0 +1,2 @@
1
+ export { ImageCropModal } from "./ImageCropModal";
2
+ export type { ImageCropModalProps } from "./ImageCropModal";
package/src/index.ts CHANGED
@@ -22,6 +22,9 @@ export type {
22
22
  ProfileImageUploadProps,
23
23
  } from "./components/ProfileImageUpload";
24
24
 
25
+ export { ImageCropModal } from "./components/ImageCropModal";
26
+ export type { ImageCropModalProps } from "./components/ImageCropModal";
27
+
25
28
  export { DropdownMultiSelect } from "./components/DropdownMultiSelect";
26
29
  export type { DropdownMultiSelectProps } from "./components/DropdownMultiSelect";
27
30