@appcorp/shadcn 1.1.24 → 1.1.25

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.
@@ -33,6 +33,8 @@ export type EnhancedDropzoneProps = {
33
33
  onChange?: (files: File[]) => void;
34
34
  /** Called when a remote URL preview is removed by the user. */
35
35
  onRemoveRemote?: (url: string) => void;
36
+ /** Enable image resize/crop functionality. When true, opens a dialog to crop images to square aspect ratio. */
37
+ resize?: boolean;
36
38
  /** `data-testid` for the root dropzone container. */
37
39
  testIdDropzone?: string;
38
40
  /** `data-testid` for the hidden file input element. */
@@ -60,13 +60,18 @@ var react_dropzone_1 = require("react-dropzone");
60
60
  var utils_1 = require("../lib/utils");
61
61
  var carousel_1 = require("./ui/carousel");
62
62
  var button_1 = require("./ui/button");
63
+ var image_resize_dialog_1 = require("./image-resize-dialog");
63
64
  var lucide_react_1 = require("lucide-react");
64
65
  var EnhancedDropzone = function (_a) {
65
- var id = _a.id, label = _a.label, info = _a.info, error = _a.error, accept = _a.accept, _b = _a.maxFiles, maxFiles = _b === void 0 ? 10 : _b, maxSize = _a.maxSize, minSize = _a.minSize, disabled = _a.disabled, _c = _a.value, value = _c === void 0 ? [] : _c, onChange = _a.onChange, onRemoveRemote = _a.onRemoveRemote, className = _a.className, testIdDropzone = _a.testIdDropzone, testIdInput = _a.testIdInput, testIdPrev = _a.testIdPrev, testIdNext = _a.testIdNext, testIdPreviewPrefix = _a.testIdPreviewPrefix, testIdImagePrefix = _a.testIdImagePrefix, testIdRemovePrefix = _a.testIdRemovePrefix, testIdCount = _a.testIdCount, testIdEmptyIcon = _a.testIdEmptyIcon, testIdEmptyTitle = _a.testIdEmptyTitle, testIdEmptySubtitle = _a.testIdEmptySubtitle, testIdEmptyNote = _a.testIdEmptyNote, testIdMessage = _a.testIdMessage;
66
+ var id = _a.id, label = _a.label, info = _a.info, error = _a.error, accept = _a.accept, _b = _a.maxFiles, maxFiles = _b === void 0 ? 10 : _b, maxSize = _a.maxSize, minSize = _a.minSize, disabled = _a.disabled, _c = _a.value, value = _c === void 0 ? [] : _c, onChange = _a.onChange, onRemoveRemote = _a.onRemoveRemote, _d = _a.resize, resize = _d === void 0 ? false : _d, className = _a.className, testIdDropzone = _a.testIdDropzone, testIdInput = _a.testIdInput, testIdPrev = _a.testIdPrev, testIdNext = _a.testIdNext, testIdPreviewPrefix = _a.testIdPreviewPrefix, testIdImagePrefix = _a.testIdImagePrefix, testIdRemovePrefix = _a.testIdRemovePrefix, testIdCount = _a.testIdCount, testIdEmptyIcon = _a.testIdEmptyIcon, testIdEmptyTitle = _a.testIdEmptyTitle, testIdEmptySubtitle = _a.testIdEmptySubtitle, testIdEmptyNote = _a.testIdEmptyNote, testIdMessage = _a.testIdMessage;
66
67
  // Local files selected by user
67
- var _d = (0, react_1.useState)([]), localFiles = _d[0], setLocalFiles = _d[1];
68
+ var _e = (0, react_1.useState)([]), localFiles = _e[0], setLocalFiles = _e[1];
68
69
  // Track object URLs created for local files
69
70
  var localPreviewsRef = (0, react_1.useRef)(new Map());
71
+ // State for resize dialog
72
+ var _f = (0, react_1.useState)(false), resizeDialogOpen = _f[0], setResizeDialogOpen = _f[1];
73
+ var _g = (0, react_1.useState)(null), pendingFile = _g[0], setPendingFile = _g[1];
74
+ var _h = (0, react_1.useState)(""), pendingFileUrl = _h[0], setPendingFileUrl = _h[1];
70
75
  // Create object URLs for local files synchronously to avoid loading state
71
76
  var createObjectURLs = (0, react_1.useCallback)(function () {
72
77
  var map = localPreviewsRef.current;
@@ -104,6 +109,14 @@ var EnhancedDropzone = function (_a) {
104
109
  map.clear();
105
110
  };
106
111
  }, []);
112
+ // Cleanup pending file URL when dialog closes
113
+ (0, react_1.useEffect)(function () {
114
+ if (!resizeDialogOpen && pendingFileUrl) {
115
+ URL.revokeObjectURL(pendingFileUrl);
116
+ setPendingFileUrl("");
117
+ setPendingFile(null);
118
+ }
119
+ }, [resizeDialogOpen, pendingFileUrl]);
107
120
  var dropzoneOptions = {
108
121
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
122
  accept: (Array.isArray(accept) ? undefined : accept) || undefined,
@@ -112,15 +125,26 @@ var EnhancedDropzone = function (_a) {
112
125
  minSize: minSize,
113
126
  disabled: disabled,
114
127
  onDrop: (0, react_1.useCallback)(function (acceptedFiles) {
115
- setLocalFiles(function (prev) {
116
- var combined = __spreadArray(__spreadArray([], prev, true), acceptedFiles, true);
117
- var limited = maxFiles ? combined.slice(0, maxFiles) : combined;
118
- onChange === null || onChange === void 0 ? void 0 : onChange(limited);
119
- return limited;
120
- });
121
- }, [maxFiles, onChange]),
128
+ if (resize && acceptedFiles.length > 0) {
129
+ // If resize is enabled, open the resize dialog with the first file
130
+ var file = acceptedFiles[0];
131
+ var url = URL.createObjectURL(file);
132
+ setPendingFile(file);
133
+ setPendingFileUrl(url);
134
+ setResizeDialogOpen(true);
135
+ }
136
+ else {
137
+ // If resize is disabled, add files directly
138
+ setLocalFiles(function (prev) {
139
+ var combined = __spreadArray(__spreadArray([], prev, true), acceptedFiles, true);
140
+ var limited = maxFiles ? combined.slice(0, maxFiles) : combined;
141
+ onChange === null || onChange === void 0 ? void 0 : onChange(limited);
142
+ return limited;
143
+ });
144
+ }
145
+ }, [maxFiles, onChange, resize]),
122
146
  };
123
- var _e = (0, react_dropzone_1.useDropzone)(dropzoneOptions), getRootProps = _e.getRootProps, getInputProps = _e.getInputProps, isDragActive = _e.isDragActive;
147
+ var _j = (0, react_dropzone_1.useDropzone)(dropzoneOptions), getRootProps = _j.getRootProps, getInputProps = _j.getInputProps, isDragActive = _j.isDragActive;
124
148
  // Remove remote URL
125
149
  var handleRemoveRemote = (0, react_1.useCallback)(function (url) {
126
150
  onRemoveRemote === null || onRemoveRemote === void 0 ? void 0 : onRemoveRemote(url);
@@ -133,6 +157,19 @@ var EnhancedDropzone = function (_a) {
133
157
  return updated;
134
158
  });
135
159
  }, [onChange]);
160
+ // Handle crop completion from resize dialog
161
+ var handleCropComplete = (0, react_1.useCallback)(function (croppedImage) {
162
+ setLocalFiles(function (prev) {
163
+ var combined = __spreadArray(__spreadArray([], prev, true), [croppedImage], false);
164
+ var limited = maxFiles ? combined.slice(0, maxFiles) : combined;
165
+ onChange === null || onChange === void 0 ? void 0 : onChange(limited);
166
+ return limited;
167
+ });
168
+ }, [maxFiles, onChange]);
169
+ // Handle resize dialog close
170
+ var handleResizeDialogClose = (0, react_1.useCallback)(function () {
171
+ setResizeDialogOpen(false);
172
+ }, []);
136
173
  // Get all preview URLs (remote + local)
137
174
  var allPreviews = __spreadArray(__spreadArray([], value.map(function (url) { return ({ type: "remote", url: url, index: 0 }); }), true), localFiles.map(function (file, index) { return ({
138
175
  type: "local",
@@ -182,6 +219,7 @@ var EnhancedDropzone = function (_a) {
182
219
  "Up to ",
183
220
  maxFiles,
184
221
  " files")))))),
185
- (error || info) && (react_1.default.createElement("div", { className: "mt-2", "data-testid": testIdMessage }, error ? (react_1.default.createElement("p", { className: "text-xs text-destructive" }, error)) : info ? (react_1.default.createElement("p", { className: "text-xs text-blue-600 dark:text-blue-400" }, info)) : null))));
222
+ (error || info) && (react_1.default.createElement("div", { className: "mt-2", "data-testid": testIdMessage }, error ? (react_1.default.createElement("p", { className: "text-xs text-destructive" }, error)) : info ? (react_1.default.createElement("p", { className: "text-xs text-blue-600 dark:text-blue-400" }, info)) : null)),
223
+ resize && pendingFile && (react_1.default.createElement(image_resize_dialog_1.ImageResizeDialog, { open: resizeDialogOpen, onClose: handleResizeDialogClose, imageUrl: pendingFileUrl, onCropComplete: handleCropComplete, fileName: pendingFile.name }))));
186
224
  };
187
225
  exports.EnhancedDropzone = EnhancedDropzone;
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ /**
3
+ * Props for `ImageResizeDialog` component.
4
+ */
5
+ export type ImageResizeDialogProps = {
6
+ /** Whether the dialog is open. */
7
+ open: boolean;
8
+ /** Called when the dialog should close (user cancels or saves). */
9
+ onClose: () => void;
10
+ /** The image URL to be resized/cropped. */
11
+ imageUrl: string;
12
+ /** Called when the user confirms the crop. Receives the cropped image as a File. */
13
+ onCropComplete: (croppedImage: File) => void;
14
+ /** Original file name for the cropped output. */
15
+ fileName?: string;
16
+ };
17
+ /**
18
+ * A dialog component that allows users to crop/resize an image to a square aspect ratio.
19
+ * Uses react-easy-crop for the cropping functionality.
20
+ */
21
+ export declare const ImageResizeDialog: React.FC<ImageResizeDialogProps>;
@@ -0,0 +1,180 @@
1
+ "use client";
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
37
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
38
+ return new (P || (P = Promise))(function (resolve, reject) {
39
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
40
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
41
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
42
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
43
+ });
44
+ };
45
+ var __generator = (this && this.__generator) || function (thisArg, body) {
46
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
47
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
48
+ function verb(n) { return function (v) { return step([n, v]); }; }
49
+ function step(op) {
50
+ if (f) throw new TypeError("Generator is already executing.");
51
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
52
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
53
+ if (y = 0, t) op = [op[0] & 2, t.value];
54
+ switch (op[0]) {
55
+ case 0: case 1: t = op; break;
56
+ case 4: _.label++; return { value: op[1], done: false };
57
+ case 5: _.label++; y = op[1]; op = [0]; continue;
58
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
59
+ default:
60
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
61
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
62
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
63
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
64
+ if (t[2]) _.ops.pop();
65
+ _.trys.pop(); continue;
66
+ }
67
+ op = body.call(thisArg, _);
68
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
69
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
70
+ }
71
+ };
72
+ var __importDefault = (this && this.__importDefault) || function (mod) {
73
+ return (mod && mod.__esModule) ? mod : { "default": mod };
74
+ };
75
+ Object.defineProperty(exports, "__esModule", { value: true });
76
+ exports.ImageResizeDialog = void 0;
77
+ var react_1 = __importStar(require("react"));
78
+ var react_easy_crop_1 = __importDefault(require("react-easy-crop"));
79
+ var dialog_1 = require("./ui/dialog");
80
+ var button_1 = require("./ui/button");
81
+ var slider_1 = require("./ui/slider");
82
+ /**
83
+ * Create a cropped image from the provided source and crop area.
84
+ */
85
+ function getCroppedImg(imageSrc_1, pixelCrop_1) {
86
+ return __awaiter(this, arguments, void 0, function (imageSrc, pixelCrop, fileName) {
87
+ var image, canvas, ctx;
88
+ if (fileName === void 0) { fileName = "cropped-image.jpg"; }
89
+ return __generator(this, function (_a) {
90
+ switch (_a.label) {
91
+ case 0: return [4 /*yield*/, createImage(imageSrc)];
92
+ case 1:
93
+ image = _a.sent();
94
+ canvas = document.createElement("canvas");
95
+ ctx = canvas.getContext("2d");
96
+ if (!ctx) {
97
+ throw new Error("No 2d context");
98
+ }
99
+ // Set canvas size to match the crop area
100
+ canvas.width = pixelCrop.width;
101
+ canvas.height = pixelCrop.height;
102
+ // Draw the cropped image
103
+ ctx.drawImage(image, pixelCrop.x, pixelCrop.y, pixelCrop.width, pixelCrop.height, 0, 0, pixelCrop.width, pixelCrop.height);
104
+ // Convert canvas to blob and then to File
105
+ return [2 /*return*/, new Promise(function (resolve, reject) {
106
+ canvas.toBlob(function (blob) {
107
+ if (!blob) {
108
+ reject(new Error("Canvas is empty"));
109
+ return;
110
+ }
111
+ var file = new File([blob], fileName, { type: "image/jpeg" });
112
+ resolve(file);
113
+ }, "image/jpeg");
114
+ })];
115
+ }
116
+ });
117
+ });
118
+ }
119
+ /**
120
+ * Create an HTMLImageElement from a URL.
121
+ */
122
+ function createImage(url) {
123
+ return new Promise(function (resolve, reject) {
124
+ var image = new Image();
125
+ image.addEventListener("load", function () { return resolve(image); });
126
+ image.addEventListener("error", function (error) { return reject(error); });
127
+ image.src = url;
128
+ });
129
+ }
130
+ /**
131
+ * A dialog component that allows users to crop/resize an image to a square aspect ratio.
132
+ * Uses react-easy-crop for the cropping functionality.
133
+ */
134
+ var ImageResizeDialog = function (_a) {
135
+ var open = _a.open, onClose = _a.onClose, imageUrl = _a.imageUrl, onCropComplete = _a.onCropComplete, _b = _a.fileName, fileName = _b === void 0 ? "cropped-image.jpg" : _b;
136
+ var _c = (0, react_1.useState)({ x: 0, y: 0 }), crop = _c[0], setCrop = _c[1];
137
+ var _d = (0, react_1.useState)(1), zoom = _d[0], setZoom = _d[1];
138
+ var _e = (0, react_1.useState)(null), croppedAreaPixels = _e[0], setCroppedAreaPixels = _e[1];
139
+ var handleCropComplete = (0, react_1.useCallback)(function (_croppedArea, croppedAreaPixels) {
140
+ setCroppedAreaPixels(croppedAreaPixels);
141
+ }, []);
142
+ var handleSave = (0, react_1.useCallback)(function () { return __awaiter(void 0, void 0, void 0, function () {
143
+ var croppedImage, error_1;
144
+ return __generator(this, function (_a) {
145
+ switch (_a.label) {
146
+ case 0:
147
+ if (!croppedAreaPixels)
148
+ return [2 /*return*/];
149
+ _a.label = 1;
150
+ case 1:
151
+ _a.trys.push([1, 3, , 4]);
152
+ return [4 /*yield*/, getCroppedImg(imageUrl, croppedAreaPixels, fileName)];
153
+ case 2:
154
+ croppedImage = _a.sent();
155
+ onCropComplete(croppedImage);
156
+ onClose();
157
+ return [3 /*break*/, 4];
158
+ case 3:
159
+ error_1 = _a.sent();
160
+ console.error("Failed to crop image:", error_1);
161
+ return [3 /*break*/, 4];
162
+ case 4: return [2 /*return*/];
163
+ }
164
+ });
165
+ }); }, [croppedAreaPixels, imageUrl, fileName, onCropComplete, onClose]);
166
+ return (react_1.default.createElement(dialog_1.Dialog, { open: open, onOpenChange: onClose },
167
+ react_1.default.createElement(dialog_1.DialogContent, { className: "max-w-3xl", "data-slot": "image-resize-dialog", "data-testid": "image-resize-dialog" },
168
+ react_1.default.createElement(dialog_1.DialogHeader, null,
169
+ react_1.default.createElement(dialog_1.DialogTitle, null, "Resize Image"),
170
+ react_1.default.createElement(dialog_1.DialogDescription, null, "Adjust the crop area to create a square image. Use the slider to zoom in or out.")),
171
+ react_1.default.createElement("div", { className: "relative h-[400px] w-full bg-muted rounded-md overflow-hidden" },
172
+ react_1.default.createElement(react_easy_crop_1.default, { image: imageUrl, crop: crop, zoom: zoom, aspect: 1, onCropChange: setCrop, onCropComplete: handleCropComplete, onZoomChange: setZoom })),
173
+ react_1.default.createElement("div", { className: "space-y-2" },
174
+ react_1.default.createElement("label", { className: "text-sm font-medium" }, "Zoom"),
175
+ react_1.default.createElement(slider_1.Slider, { value: [zoom], onValueChange: function (values) { return setZoom(values[0] || 1); }, min: 1, max: 3, step: 0.1, className: "w-full", "data-testid": "zoom-slider" })),
176
+ react_1.default.createElement(dialog_1.DialogFooter, null,
177
+ react_1.default.createElement(button_1.Button, { variant: "outline", onClick: onClose, "data-testid": "cancel-button" }, "Cancel"),
178
+ react_1.default.createElement(button_1.Button, { onClick: handleSave, "data-testid": "save-button" }, "Save")))));
179
+ };
180
+ exports.ImageResizeDialog = ImageResizeDialog;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appcorp/shadcn",
3
- "version": "1.1.24",
3
+ "version": "1.1.25",
4
4
  "scripts": {
5
5
  "build:next": "next build",
6
6
  "build:storybook": "storybook build -c .storybook -o .out",
@@ -123,6 +123,7 @@
123
123
  },
124
124
  "packageManager": "yarn@4.12.0",
125
125
  "dependencies": {
126
- "@tabler/icons-react": "^3.36.1"
126
+ "@tabler/icons-react": "^3.36.1",
127
+ "react-easy-crop": "^5.5.6"
127
128
  }
128
129
  }