hazo_auth 10.2.2 → 10.2.3
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/dist/components/layouts/my_settings/components/profile_picture_upload_tab.d.ts +7 -1
- package/dist/components/layouts/my_settings/components/profile_picture_upload_tab.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_upload_tab.js +46 -15
- package/package.json +2 -2
|
@@ -10,14 +10,20 @@ export type ProfilePictureUploadTabProps = {
|
|
|
10
10
|
imageCompressionMaxDimension?: number;
|
|
11
11
|
uploadFileHardLimitBytes?: number;
|
|
12
12
|
allowedImageMimeTypes?: string[];
|
|
13
|
+
cropTitle?: string;
|
|
14
|
+
cropConfirmLabel?: string;
|
|
15
|
+
cropCancelLabel?: string;
|
|
16
|
+
cropZoomLabel?: string;
|
|
13
17
|
};
|
|
14
18
|
/**
|
|
15
19
|
* Upload tab component for profile picture dialog
|
|
16
20
|
* Two columns: left = dropzone, right = preview
|
|
17
21
|
* Uses browser-image-compression for client-side compression
|
|
22
|
+
* Routes decodable images through HazoUiImageCropperDialog (round crop, 512² WebP)
|
|
23
|
+
* Falls back to direct upload for HEIC and other browser-undecodable formats
|
|
18
24
|
* @param props - Component props including upload state, file handler, and configuration
|
|
19
25
|
* @returns Upload tab component
|
|
20
26
|
*/
|
|
21
27
|
export declare function ProfilePictureUploadTab({ useUpload, onUseUploadChange, onFileSelect, maxSize, uploadEnabled, disabled, currentPreview, photoUploadDisabledMessage, imageCompressionMaxDimension, uploadFileHardLimitBytes, // 10MB default
|
|
22
|
-
allowedImageMimeTypes, }: ProfilePictureUploadTabProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
allowedImageMimeTypes, cropTitle, cropConfirmLabel, cropCancelLabel, cropZoomLabel, }: ProfilePictureUploadTabProps): import("react/jsx-runtime").JSX.Element;
|
|
23
29
|
//# sourceMappingURL=profile_picture_upload_tab.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile_picture_upload_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/my_settings/components/profile_picture_upload_tab.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"profile_picture_upload_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/my_settings/components/profile_picture_upload_tab.tsx"],"names":[],"mappings":"AA2BA,MAAM,MAAM,4BAA4B,GAAG;IACzC,SAAS,EAAE,OAAO,CAAC;IACnB,iBAAiB,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1C,YAAY,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,OAAO,EACP,aAAa,EACb,QAAgB,EAChB,cAAc,EACd,0BAA0B,EAC1B,4BAAkC,EAClC,wBAAmC,EAAE,eAAe;AACpD,qBAAgE,EAChE,SAAwB,EACxB,gBAA+B,EAC/B,eAA0B,EAC1B,aAAsB,GACvB,EAAE,4BAA4B,2CAgU9B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// file_description: Upload tab component for profile picture dialog with dropzone and preview
|
|
1
|
+
// file_description: Upload tab component for profile picture dialog with dropzone, crop step, and preview
|
|
2
2
|
// section: client_directive
|
|
3
3
|
"use client";
|
|
4
4
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -10,22 +10,37 @@ import { Avatar, AvatarImage, AvatarFallback } from "../../../ui/avatar.js";
|
|
|
10
10
|
import { Upload, X, Loader2, Info } from "lucide-react";
|
|
11
11
|
import { Button } from "../../../ui/button.js";
|
|
12
12
|
import imageCompression from "browser-image-compression";
|
|
13
|
+
import { HazoUiImageCropperDialog } from "hazo_ui";
|
|
14
|
+
// section: helpers
|
|
15
|
+
/** Resolves true if the browser can decode this image file, false otherwise (e.g. desktop HEIC). */
|
|
16
|
+
function can_decode_image(file) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const url = URL.createObjectURL(file);
|
|
19
|
+
const img = new Image();
|
|
20
|
+
img.onload = () => { URL.revokeObjectURL(url); resolve(true); };
|
|
21
|
+
img.onerror = () => { URL.revokeObjectURL(url); resolve(false); };
|
|
22
|
+
img.src = url;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
13
25
|
// section: component
|
|
14
26
|
/**
|
|
15
27
|
* Upload tab component for profile picture dialog
|
|
16
28
|
* Two columns: left = dropzone, right = preview
|
|
17
29
|
* Uses browser-image-compression for client-side compression
|
|
30
|
+
* Routes decodable images through HazoUiImageCropperDialog (round crop, 512² WebP)
|
|
31
|
+
* Falls back to direct upload for HEIC and other browser-undecodable formats
|
|
18
32
|
* @param props - Component props including upload state, file handler, and configuration
|
|
19
33
|
* @returns Upload tab component
|
|
20
34
|
*/
|
|
21
35
|
export function ProfilePictureUploadTab({ useUpload, onUseUploadChange, onFileSelect, maxSize, uploadEnabled, disabled = false, currentPreview, photoUploadDisabledMessage, imageCompressionMaxDimension = 200, uploadFileHardLimitBytes = 10485760, // 10MB default
|
|
22
|
-
allowedImageMimeTypes = ["image/jpeg", "image/jpg", "image/png"], }) {
|
|
36
|
+
allowedImageMimeTypes = ["image/jpeg", "image/jpg", "image/png"], cropTitle = "Crop photo", cropConfirmLabel = "Save photo", cropCancelLabel = "Cancel", cropZoomLabel = "Zoom", }) {
|
|
23
37
|
const [dragActive, setDragActive] = useState(false);
|
|
24
38
|
const [preview, setPreview] = useState(currentPreview || null);
|
|
25
39
|
const [isNewImage, setIsNewImage] = useState(false); // Track if preview is showing a newly uploaded image
|
|
26
40
|
const [uploading, setUploading] = useState(false);
|
|
27
41
|
const [compressing, setCompressing] = useState(false);
|
|
28
42
|
const [error, setError] = useState(null);
|
|
43
|
+
const [cropFile, setCropFile] = useState(null);
|
|
29
44
|
// Update preview when currentPreview changes (e.g., when dialog opens)
|
|
30
45
|
useEffect(() => {
|
|
31
46
|
if (currentPreview) {
|
|
@@ -40,17 +55,7 @@ allowedImageMimeTypes = ["image/jpeg", "image/jpg", "image/png"], }) {
|
|
|
40
55
|
}
|
|
41
56
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
57
|
}, [currentPreview]); // Only depend on currentPreview to avoid loops, isNewImage check is intentional
|
|
43
|
-
const
|
|
44
|
-
// Validate file type
|
|
45
|
-
if (!allowedImageMimeTypes.includes(file.type)) {
|
|
46
|
-
setError(`Invalid file type. Only ${allowedImageMimeTypes.map(t => t.split("/")[1].toUpperCase()).join(", ")} files are allowed.`);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
// Hard limit: reject files larger than configured limit (too large to process efficiently)
|
|
50
|
-
if (file.size > uploadFileHardLimitBytes) {
|
|
51
|
-
setError(`File is too large. Maximum size is ${Math.round(maxSize / 1024)}KB.`);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
58
|
+
const process_and_upload = useCallback(async (file) => {
|
|
54
59
|
setError(null);
|
|
55
60
|
setCompressing(false);
|
|
56
61
|
setUploading(false);
|
|
@@ -112,7 +117,32 @@ allowedImageMimeTypes = ["image/jpeg", "image/jpg", "image/png"], }) {
|
|
|
112
117
|
setUploading(false);
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
|
-
}, [maxSize, onFileSelect]);
|
|
120
|
+
}, [maxSize, onFileSelect, imageCompressionMaxDimension]);
|
|
121
|
+
const handleFile = useCallback(async (file) => {
|
|
122
|
+
// Validate file type
|
|
123
|
+
if (!allowedImageMimeTypes.includes(file.type)) {
|
|
124
|
+
setError(`Invalid file type. Only ${allowedImageMimeTypes.map(t => t.split("/")[1].toUpperCase()).join(", ")} files are allowed.`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Hard limit: reject files larger than configured limit (too large to process efficiently)
|
|
128
|
+
if (file.size > uploadFileHardLimitBytes) {
|
|
129
|
+
setError(`File is too large. Maximum size is ${Math.round(maxSize / 1024)}KB.`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
setError(null);
|
|
133
|
+
const decodable = await can_decode_image(file);
|
|
134
|
+
if (decodable) {
|
|
135
|
+
setCropFile(file); // opens the crop dialog
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
await process_and_upload(file); // HEIC/undecodable fallback: direct path
|
|
139
|
+
}
|
|
140
|
+
}, [allowedImageMimeTypes, uploadFileHardLimitBytes, maxSize, process_and_upload]);
|
|
141
|
+
const handle_crop_confirm = useCallback(async (blob) => {
|
|
142
|
+
const cropped = new File([blob], "avatar.webp", { type: "image/webp" });
|
|
143
|
+
await process_and_upload(cropped);
|
|
144
|
+
setCropFile(null);
|
|
145
|
+
}, [process_and_upload]);
|
|
116
146
|
const handleDrag = useCallback((e) => {
|
|
117
147
|
e.preventDefault();
|
|
118
148
|
e.stopPropagation();
|
|
@@ -165,5 +195,6 @@ allowedImageMimeTypes = ["image/jpeg", "image/jpg", "image/png"], }) {
|
|
|
165
195
|
if (!disabled && uploadEnabled) {
|
|
166
196
|
(_a = document.getElementById("file-upload-input")) === null || _a === void 0 ? void 0 : _a.click();
|
|
167
197
|
}
|
|
168
|
-
}, children: [_jsx("input", { id: "file-upload-input", type: "file", accept: allowedImageMimeTypes.join(","), onChange: handleChange, disabled: disabled || !uploadEnabled, className: "hidden", "aria-label": "Upload profile picture" }), _jsx(Upload, { className: "h-8 w-8 text-[var(--hazo-text-subtle)] mb-2", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_dropzone_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Drag and drop an image here, or click to select" }), _jsxs("p", { className: "cls_profile_picture_upload_tab_dropzone_hint text-xs text-[var(--hazo-text-muted)] text-center mt-1", children: ["JPG or PNG, max ", Math.round(maxSize / 1024), "KB"] })] }), error && (_jsx("p", { className: "cls_profile_picture_upload_tab_error text-sm text-red-600", role: "alert", children: error }))] }), _jsxs("div", { className: "cls_profile_picture_upload_tab_preview_container flex flex-col gap-2", children: [_jsx(Label, { className: "cls_profile_picture_upload_tab_preview_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: isNewImage ? "Preview (new)" : "Preview (current)" }), _jsx("div", { className: "cls_profile_picture_upload_tab_preview_content flex flex-col items-center justify-center border border-[var(--hazo-border)] rounded-lg p-6 bg-[var(--hazo-bg-subtle)] min-h-[200px]", children: compressing ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_compressing flex flex-col items-center gap-2", children: [_jsx(Loader2, { className: "h-8 w-8 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_compressing_text text-sm text-[var(--hazo-text-muted)]", children: "Compressing image..." })] })) : uploading ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_uploading flex flex-col items-center gap-2", children: [_jsx(Loader2, { className: "h-8 w-8 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_uploading_text text-sm text-[var(--hazo-text-muted)]", children: "Uploading..." })] })) : preview ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_image_container flex flex-col items-center gap-4", children: [_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_image_wrapper relative", children: [_jsxs(Avatar, { className: "cls_profile_picture_upload_tab_preview_avatar h-32 w-32", children: [_jsx(AvatarImage, { src: preview, alt: "Uploaded profile picture preview", className: "cls_profile_picture_upload_tab_preview_avatar_image" }), _jsx(AvatarFallback, { className: "cls_profile_picture_upload_tab_preview_avatar_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)] text-3xl", children: getInitials() })] }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "ghost", size: "icon", className: "cls_profile_picture_upload_tab_preview_remove absolute -top-2 -right-2 rounded-full h-6 w-6 border border-[var(--hazo-border-emphasis)] bg-white hover:bg-[var(--hazo-bg-subtle)]", "aria-label": "Remove preview", children: _jsx(X, { className: "h-4 w-4", "aria-hidden": "true" }) })] }), _jsx("p", { className: "cls_profile_picture_upload_tab_preview_success_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Preview of your uploaded photo" })] })) : (_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_empty flex flex-col items-center gap-2", children: [_jsx(Avatar, { className: "cls_profile_picture_upload_tab_preview_empty_avatar h-32 w-32", children: _jsx(AvatarFallback, { className: "cls_profile_picture_upload_tab_preview_empty_avatar_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)] text-3xl", children: getInitials() }) }), _jsx("p", { className: "cls_profile_picture_upload_tab_preview_empty_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Upload an image to see preview" })] })) })] })] })
|
|
198
|
+
}, children: [_jsx("input", { id: "file-upload-input", type: "file", accept: allowedImageMimeTypes.join(","), onChange: handleChange, disabled: disabled || !uploadEnabled, className: "hidden", "aria-label": "Upload profile picture" }), _jsx(Upload, { className: "h-8 w-8 text-[var(--hazo-text-subtle)] mb-2", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_dropzone_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Drag and drop an image here, or click to select" }), _jsxs("p", { className: "cls_profile_picture_upload_tab_dropzone_hint text-xs text-[var(--hazo-text-muted)] text-center mt-1", children: ["JPG or PNG, max ", Math.round(maxSize / 1024), "KB"] })] }), error && (_jsx("p", { className: "cls_profile_picture_upload_tab_error text-sm text-red-600", role: "alert", children: error }))] }), _jsxs("div", { className: "cls_profile_picture_upload_tab_preview_container flex flex-col gap-2", children: [_jsx(Label, { className: "cls_profile_picture_upload_tab_preview_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: isNewImage ? "Preview (new)" : "Preview (current)" }), _jsx("div", { className: "cls_profile_picture_upload_tab_preview_content flex flex-col items-center justify-center border border-[var(--hazo-border)] rounded-lg p-6 bg-[var(--hazo-bg-subtle)] min-h-[200px]", children: compressing ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_compressing flex flex-col items-center gap-2", children: [_jsx(Loader2, { className: "h-8 w-8 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_compressing_text text-sm text-[var(--hazo-text-muted)]", children: "Compressing image..." })] })) : uploading ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_uploading flex flex-col items-center gap-2", children: [_jsx(Loader2, { className: "h-8 w-8 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }), _jsx("p", { className: "cls_profile_picture_upload_tab_uploading_text text-sm text-[var(--hazo-text-muted)]", children: "Uploading..." })] })) : preview ? (_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_image_container flex flex-col items-center gap-4", children: [_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_image_wrapper relative", children: [_jsxs(Avatar, { className: "cls_profile_picture_upload_tab_preview_avatar h-32 w-32", children: [_jsx(AvatarImage, { src: preview, alt: "Uploaded profile picture preview", className: "cls_profile_picture_upload_tab_preview_avatar_image" }), _jsx(AvatarFallback, { className: "cls_profile_picture_upload_tab_preview_avatar_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)] text-3xl", children: getInitials() })] }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "ghost", size: "icon", className: "cls_profile_picture_upload_tab_preview_remove absolute -top-2 -right-2 rounded-full h-6 w-6 border border-[var(--hazo-border-emphasis)] bg-white hover:bg-[var(--hazo-bg-subtle)]", "aria-label": "Remove preview", children: _jsx(X, { className: "h-4 w-4", "aria-hidden": "true" }) })] }), _jsx("p", { className: "cls_profile_picture_upload_tab_preview_success_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Preview of your uploaded photo" })] })) : (_jsxs("div", { className: "cls_profile_picture_upload_tab_preview_empty flex flex-col items-center gap-2", children: [_jsx(Avatar, { className: "cls_profile_picture_upload_tab_preview_empty_avatar h-32 w-32", children: _jsx(AvatarFallback, { className: "cls_profile_picture_upload_tab_preview_empty_avatar_fallback bg-[var(--hazo-bg-emphasis)] text-[var(--hazo-text-muted)] text-3xl", children: getInitials() }) }), _jsx("p", { className: "cls_profile_picture_upload_tab_preview_empty_text text-sm text-[var(--hazo-text-muted)] text-center", children: "Upload an image to see preview" })] })) })] })] }), _jsx(HazoUiImageCropperDialog, { open: cropFile !== null, onOpenChange: (open) => { if (!open && !uploading && !compressing)
|
|
199
|
+
setCropFile(null); }, file: cropFile, onConfirm: handle_crop_confirm, title: cropTitle, confirmLabel: cropConfirmLabel, cancelLabel: cropCancelLabel, zoomLabel: cropZoomLabel })] }));
|
|
169
200
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_auth",
|
|
3
|
-
"version": "10.2.
|
|
3
|
+
"version": "10.2.3",
|
|
4
4
|
"description": "Zero-config authentication UI components for Next.js with RBAC, OAuth, scope-based multi-tenancy, and invitations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authentication",
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
"hazo_logs": "^2.0.3",
|
|
262
262
|
"hazo_notify": "^6.1.3",
|
|
263
263
|
"hazo_secure": "^1.1.0",
|
|
264
|
-
"hazo_ui": "^4.
|
|
264
|
+
"hazo_ui": "^4.2.0",
|
|
265
265
|
"input-otp": "^1.4.0",
|
|
266
266
|
"lucide-react": "^0.553.0",
|
|
267
267
|
"next": "^14.0.0",
|