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.
@@ -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":"AAcA,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;CAClC,CAAC;AAGF;;;;;;GAMG;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,GACjE,EAAE,4BAA4B,2CAmS9B"}
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 handleFile = useCallback(async (file) => {
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.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.0.0",
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",