@mgonza02/goal-image-gallery 1.2.0 → 1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to the GoalImageGallery project will be documented in this f
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.3] - 2026-10-12
9
+
10
+ ### Added
11
+ - **`defaultImageCode`**: Highlights the default image in the grid.
12
+ - **`onSetAsDefault(imageCode)`**: Star action when `multiple` and at least two images (requires handler).
13
+ - **`onDelete(imageCode)`**, **`onUpdate(oldCode, newCode)`**, **`onAdd(imageCode)`**: Fired after successful gallery operations.
14
+
15
+ ## [1.2.2] - 2026-10-12
16
+
17
+ ### Added
18
+ - **`maxItems`**: Cap gallery size; hides the add tile and blocks uploads beyond the limit.
19
+ - **`allowDelete` / `minItems`**: Configurable delete in single- and multi-image mode (delete was previously limited to multi with 2+ images).
20
+ - **`minImageWidth` / `minImageHeight`**: Minimum dimension constraints, passed to custom selectors and `validateImageFile`.
21
+ - **`validateImageFile`**, **`validateImageFileSync`**, **`countGalleryImages`** exported from `image-utils`.
22
+
23
+ ### Changed
24
+ - Delete API payload includes `action: 'delete'`, `image_codes`, and `deletedImageCode` for backends that branch on operation type.
25
+
26
+ ## [1.2.1] - 2026-05-22
27
+
28
+ ### 🐛 Bug Fixes
29
+ - **Stack alignItems**: Moved `alignItems` prop inside the `sx` prop in `ImageGalleryEmptyState` component to prevent React from passing it down as an unrecognized attribute to DOM elements.
30
+
8
31
  ## [1.2.0] - 2026-04-29
9
32
 
10
33
  ### ⬆️ Dependency Upgrades
package/README.md CHANGED
@@ -312,6 +312,16 @@ const CustomImageSelector = ({
312
312
  | `imageHandlerApi` | `function` | `undefined` | No | API function for image operations (required for editing) |
313
313
  | `afterUpload` | `function` | `undefined` | No | Callback function called after successful upload |
314
314
  | `multiple` | `boolean` | `false` | No | Allow multiple image selection and upload |
315
+ | `maxItems` | `number` | `undefined` | No | Maximum number of images in the gallery (hides add control when reached) |
316
+ | `allowDelete` | `boolean` | `true` | No | Show delete action when `canEdit` is enabled |
317
+ | `minItems` | `number` | `0` | No | Minimum images that must remain (disables delete at this count) |
318
+ | `minImageWidth` | `number` | `undefined` | No | Minimum image width in pixels (forwarded to selector / `validateImageFile`) |
319
+ | `minImageHeight` | `number` | `undefined` | No | Minimum image height in pixels (forwarded to selector / `validateImageFile`) |
320
+ | `defaultImageCode` | `string` | `undefined` | No | Marks which image is the default in the grid |
321
+ | `onSetAsDefault` | `(imageCode) => void` | `undefined` | No | User chose default (only when `multiple` and 2+ images) |
322
+ | `onDelete` | `(imageCode) => void` | `undefined` | No | After successful delete |
323
+ | `onUpdate` | `(oldImageCode, newImageCode) => void` | `undefined` | No | After successful replace |
324
+ | `onAdd` | `(imageCode) => void` | `undefined` | No | After each new image is added |
315
325
 
316
326
  ### Display Props
317
327
 
@@ -1106,6 +1116,7 @@ When contributing to this component:
1106
1116
  For detailed information about changes, new features, and updates across versions, see [CHANGELOG.md](CHANGELOG.md).
1107
1117
 
1108
1118
  ### Recent Updates
1119
+ - **v1.2.1**: Fixed warning about invalid `alignItems` prop being passed to DOM elements by Stack components
1109
1120
  - **v1.2.0**: Upgraded to MUI v9, migrated to pnpm, Docker build pipeline, SOLID principles documentation
1110
1121
  - **v1.1.1**: Modal responsiveness, accessibility improvements, styled-components replaced with MUI Box
1111
1122
  - **v1.1.0**: Fixed handleAfterUpload array structure for bulk uploads
@@ -5,7 +5,7 @@ import { useMediaQuery } from "@mui/material";
5
5
  import ImageGalleryGrid from "./components/ImageGalleryGrid";
6
6
  import ImageGalleryModal from "./components/ImageGalleryModal";
7
7
  import { useImageGallery, useImageZoom, useClipboard, useKeyboardNavigation } from "./hooks/use-image-gallery";
8
- import { downloadImage } from "./utils/image-utils";
8
+ import { countGalleryImages, downloadImage } from "./utils/image-utils";
9
9
 
10
10
  /**
11
11
  * GoalImageGallery - Advanced React Image Gallery Component
@@ -233,7 +233,17 @@ const GoalImageGallery = _ref => {
233
233
  hasPermission = () => true,
234
234
  currentCompany = 0,
235
235
  slot = {},
236
- slotProps = {}
236
+ slotProps = {},
237
+ maxItems,
238
+ allowDelete = true,
239
+ minItems = 0,
240
+ minImageWidth,
241
+ minImageHeight,
242
+ defaultImageCode,
243
+ onSetAsDefault,
244
+ onDelete,
245
+ onUpdate,
246
+ onAdd
237
247
  } = _ref;
238
248
  const theme = useTheme();
239
249
  const [updatable, setUpdatable] = useState(false);
@@ -241,8 +251,21 @@ const GoalImageGallery = _ref => {
241
251
  selector: ImageSelector
242
252
  } = slot;
243
253
  const {
244
- selector: imageSelectorProps
254
+ selector: imageSelectorProps = {}
245
255
  } = slotProps;
256
+ const currentImageCount = countGalleryImages((Array.isArray(imageCodes) ? imageCodes : (imageCodes || "").split(/[\s,]+/).filter(Boolean)).map((code, index) => ({
257
+ id: index,
258
+ image_code: code
259
+ })));
260
+ const mergedSelectorProps = {
261
+ ...imageSelectorProps,
262
+ maxItems,
263
+ currentImageCount,
264
+ minImageWidth,
265
+ minImageHeight,
266
+ minWidth: imageSelectorProps.minWidth ?? minImageWidth,
267
+ minHeight: imageSelectorProps.minHeight ?? minImageHeight
268
+ };
246
269
 
247
270
  // Initialize permissions
248
271
  useEffect(() => {
@@ -259,12 +282,14 @@ const GoalImageGallery = _ref => {
259
282
  zoomLevel,
260
283
  loading,
261
284
  removable,
285
+ canSetDefault,
262
286
  setZoomLevel,
263
287
  handleOpenImage,
264
288
  handleCloseImage,
265
289
  handleImageSelect,
266
290
  handleCancel,
267
291
  handleRemoveImage,
292
+ handleSetAsDefault,
268
293
  handleAfterUpload
269
294
  } = useImageGallery({
270
295
  imageCodes,
@@ -275,7 +300,14 @@ const GoalImageGallery = _ref => {
275
300
  ownerEntity,
276
301
  currentCompany,
277
302
  onRefresh,
278
- showError
303
+ showError,
304
+ maxItems,
305
+ allowDelete,
306
+ minItems,
307
+ onSetAsDefault,
308
+ onDelete,
309
+ onUpdate,
310
+ onAdd
279
311
  });
280
312
 
281
313
  // Zoom and navigation handlers
@@ -338,7 +370,7 @@ const GoalImageGallery = _ref => {
338
370
  multiple: multiple,
339
371
  title: multiple ? "Seleccionar Imágenes" : "Seleccionar Imagen",
340
372
  uploadMessage: multiple ? "Arrastra imágenes aquí o haz clic para seleccionar" : "Arrastra una imagen aquí o haz clic para seleccionar",
341
- ...imageSelectorProps
373
+ ...mergedSelectorProps
342
374
  }) : /*#__PURE__*/_jsxs("div", {
343
375
  style: {
344
376
  padding: "20px",
@@ -380,7 +412,10 @@ const GoalImageGallery = _ref => {
380
412
  onImageClick: handleOpenImage,
381
413
  onImageSelect: handleImageSelect,
382
414
  onRemoveImage: handleRemoveImage,
383
- onDownloadImage: handleDownloadImage
415
+ onSetAsDefault: handleSetAsDefault,
416
+ onDownloadImage: handleDownloadImage,
417
+ canSetDefault: canSetDefault,
418
+ defaultImageCode: defaultImageCode
384
419
  }), /*#__PURE__*/_jsx(ImageGalleryModal, {
385
420
  open: openImage !== null,
386
421
  imageUrl: openImage,
@@ -413,6 +448,16 @@ GoalImageGallery.propTypes = {
413
448
  hasPermission: PropTypes.func,
414
449
  currentCompany: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
415
450
  slot: PropTypes.object,
416
- slotProps: PropTypes.object
451
+ slotProps: PropTypes.object,
452
+ maxItems: PropTypes.number,
453
+ allowDelete: PropTypes.bool,
454
+ minItems: PropTypes.number,
455
+ minImageWidth: PropTypes.number,
456
+ minImageHeight: PropTypes.number,
457
+ defaultImageCode: PropTypes.string,
458
+ onSetAsDefault: PropTypes.func,
459
+ onDelete: PropTypes.func,
460
+ onUpdate: PropTypes.func,
461
+ onAdd: PropTypes.func
417
462
  };
418
463
  export default GoalImageGallery;
@@ -18,7 +18,9 @@ const ImageGalleryEmptyState = _ref => {
18
18
  return /*#__PURE__*/_jsx(StyledEmptyStateContainer, {
19
19
  children: /*#__PURE__*/_jsxs(Stack, {
20
20
  spacing: 3,
21
- alignItems: "center",
21
+ sx: {
22
+ alignItems: 'center'
23
+ },
22
24
  children: [/*#__PURE__*/_jsx(ImageIcon, {
23
25
  sx: {
24
26
  fontSize: 64,
@@ -42,9 +44,9 @@ const ImageGalleryEmptyState = _ref => {
42
44
  }), /*#__PURE__*/_jsxs(Stack, {
43
45
  direction: "row",
44
46
  spacing: 1,
45
- alignItems: "center",
46
47
  sx: {
47
- mt: 2
48
+ mt: 2,
49
+ alignItems: 'center'
48
50
  },
49
51
  children: [/*#__PURE__*/_jsx(Chip, {
50
52
  icon: /*#__PURE__*/_jsx(UploadIcon, {}),
@@ -25,7 +25,10 @@ const ImageGalleryGrid = _ref => {
25
25
  onImageClick,
26
26
  onImageSelect,
27
27
  onRemoveImage,
28
- onDownloadImage
28
+ onSetAsDefault,
29
+ onDownloadImage,
30
+ canSetDefault = false,
31
+ defaultImageCode
29
32
  } = _ref;
30
33
  const theme = useTheme();
31
34
  if (imageList.length === 0) {
@@ -70,7 +73,10 @@ const ImageGalleryGrid = _ref => {
70
73
  onImageClick: onImageClick,
71
74
  onImageSelect: onImageSelect,
72
75
  onRemoveImage: onRemoveImage,
73
- onDownloadImage: onDownloadImage
76
+ onSetAsDefault: onSetAsDefault,
77
+ onDownloadImage: onDownloadImage,
78
+ canSetDefault: canSetDefault,
79
+ isDefault: !!defaultImageCode && image.image_code === defaultImageCode
74
80
  })
75
81
  }, image.id))
76
82
  })
@@ -93,6 +99,9 @@ ImageGalleryGrid.propTypes = {
93
99
  onImageClick: PropTypes.func.isRequired,
94
100
  onImageSelect: PropTypes.func.isRequired,
95
101
  onRemoveImage: PropTypes.func.isRequired,
96
- onDownloadImage: PropTypes.func.isRequired
102
+ onSetAsDefault: PropTypes.func,
103
+ onDownloadImage: PropTypes.func.isRequired,
104
+ canSetDefault: PropTypes.bool,
105
+ defaultImageCode: PropTypes.string
97
106
  };
98
107
  export default ImageGalleryGrid;
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Box, IconButton, Skeleton, Tooltip, Typography, Button } from '@mui/material';
4
4
  import { alpha, useTheme } from '@mui/material/styles';
5
- import { ChangeCircle as ChangeIcon, Download as DownloadIcon, Visibility as ViewIcon, Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material';
5
+ import { ChangeCircle as ChangeIcon, Download as DownloadIcon, Visibility as ViewIcon, Add as AddIcon, Delete as DeleteIcon, Star as StarIcon, StarBorder as StarBorderIcon } from '@mui/icons-material';
6
6
  import { getImageContainerStyles, getImageOverlayStyles } from '../styles/styled-components';
7
7
  import { handleImageError } from '../utils/image-utils';
8
8
 
@@ -21,9 +21,12 @@ const ImageGalleryItem = _ref => {
21
21
  removable = false,
22
22
  allowDownload = false,
23
23
  showImageInfo = false,
24
+ canSetDefault = false,
25
+ isDefault = false,
24
26
  onImageClick,
25
27
  onImageSelect,
26
28
  onRemoveImage,
29
+ onSetAsDefault,
27
30
  onDownloadImage
28
31
  } = _ref;
29
32
  const theme = useTheme();
@@ -65,7 +68,31 @@ const ImageGalleryItem = _ref => {
65
68
  }
66
69
  return /*#__PURE__*/_jsxs(Box, {
67
70
  sx: getImageContainerStyles(theme),
68
- children: [loading && /*#__PURE__*/_jsx(Skeleton, {
71
+ children: [isDefault && /*#__PURE__*/_jsxs(Box, {
72
+ sx: {
73
+ position: 'absolute',
74
+ top: 8,
75
+ left: 8,
76
+ zIndex: 2,
77
+ display: 'flex',
78
+ alignItems: 'center',
79
+ gap: 0.5,
80
+ px: 1,
81
+ py: 0.25,
82
+ borderRadius: 1,
83
+ backgroundColor: alpha(theme.palette.warning.main, 0.92),
84
+ color: theme.palette.warning.contrastText
85
+ },
86
+ children: [/*#__PURE__*/_jsx(StarIcon, {
87
+ sx: {
88
+ fontSize: 16
89
+ }
90
+ }), /*#__PURE__*/_jsx(Typography, {
91
+ variant: "caption",
92
+ fontWeight: 600,
93
+ children: "Predeterminada"
94
+ })]
95
+ }), loading && /*#__PURE__*/_jsx(Skeleton, {
69
96
  variant: "rectangular",
70
97
  width: "100%",
71
98
  height: 200,
@@ -111,6 +138,17 @@ const ImageGalleryItem = _ref => {
111
138
  },
112
139
  children: /*#__PURE__*/_jsx(DownloadIcon, {})
113
140
  })
141
+ }), canSetDefault && !isDefault && /*#__PURE__*/_jsx(Tooltip, {
142
+ title: "Usar como predeterminada",
143
+ children: /*#__PURE__*/_jsx(IconButton, {
144
+ size: "small",
145
+ onClick: e => {
146
+ e.stopPropagation();
147
+ onSetAsDefault?.(image);
148
+ },
149
+ color: "warning",
150
+ children: /*#__PURE__*/_jsx(StarBorderIcon, {})
151
+ })
114
152
  }), updatable && /*#__PURE__*/_jsx(Tooltip, {
115
153
  title: "Cambiar imagen",
116
154
  children: /*#__PURE__*/_jsx(IconButton, {
@@ -163,9 +201,12 @@ ImageGalleryItem.propTypes = {
163
201
  removable: PropTypes.bool,
164
202
  allowDownload: PropTypes.bool,
165
203
  showImageInfo: PropTypes.bool,
204
+ canSetDefault: PropTypes.bool,
205
+ isDefault: PropTypes.bool,
166
206
  onImageClick: PropTypes.func.isRequired,
167
207
  onImageSelect: PropTypes.func.isRequired,
168
208
  onRemoveImage: PropTypes.func.isRequired,
209
+ onSetAsDefault: PropTypes.func,
169
210
  onDownloadImage: PropTypes.func.isRequired
170
211
  };
171
212
  export default ImageGalleryItem;
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState, useCallback } from 'react';
2
+ import { countGalleryImages } from '../utils/image-utils';
2
3
 
3
4
  /**
4
5
  * Custom hook for managing image gallery state
@@ -13,7 +14,14 @@ export const useImageGallery = _ref => {
13
14
  ownerEntity,
14
15
  currentCompany,
15
16
  onRefresh,
16
- showError
17
+ showError,
18
+ maxItems,
19
+ allowDelete = true,
20
+ minItems = 0,
21
+ onSetAsDefault,
22
+ onDelete,
23
+ onUpdate,
24
+ onAdd
17
25
  } = _ref;
18
26
  const [openImage, setOpenImage] = useState(null);
19
27
  const [selectedImageIndex, setSelectedImageIndex] = useState(0);
@@ -23,6 +31,7 @@ export const useImageGallery = _ref => {
23
31
  const [zoomLevel, setZoomLevel] = useState(1);
24
32
  const [loading, setLoading] = useState(false);
25
33
  const [removable, setRemovable] = useState(false);
34
+ const [canSetDefault, setCanSetDefault] = useState(false);
26
35
 
27
36
  // Process image codes into image list
28
37
  useEffect(() => {
@@ -40,17 +49,19 @@ export const useImageGallery = _ref => {
40
49
  } else {
41
50
  newList = [];
42
51
  }
43
- setRemovable(multiple && newList.length > 1);
44
-
45
- // Add plus button for multiple mode
46
- if (updatable && multiple) {
52
+ const imageCount = newList.length;
53
+ const canRemoveMore = imageCount > minItems;
54
+ setRemovable(!!(updatable && allowDelete && imageCount > 0 && canRemoveMore));
55
+ const atMaxItems = maxItems != null && imageCount >= maxItems;
56
+ if (updatable && multiple && !atMaxItems) {
47
57
  newList.push({
48
58
  image_code: '+',
49
59
  id: newList.length
50
60
  });
51
61
  }
62
+ setCanSetDefault(!!(multiple && imageCount >= 2 && onSetAsDefault));
52
63
  setImageList(newList);
53
- }, [imageCodes, canEdit, multiple, updatable]);
64
+ }, [imageCodes, canEdit, multiple, updatable, maxItems, allowDelete, minItems, onSetAsDefault]);
54
65
 
55
66
  // Image handlers
56
67
  const handleOpenImage = useCallback(function (imageUrl) {
@@ -64,9 +75,16 @@ export const useImageGallery = _ref => {
64
75
  setZoomLevel(1);
65
76
  }, []);
66
77
  const handleImageSelect = useCallback(image => {
78
+ if (image === null && maxItems != null) {
79
+ const currentCount = countGalleryImages(imageList);
80
+ if (currentCount >= maxItems) {
81
+ showError?.(`Máximo ${maxItems} imágenes permitidas`);
82
+ return;
83
+ }
84
+ }
67
85
  setSelectedImage(image);
68
86
  setSelectingImage(true);
69
- }, []);
87
+ }, [imageList, maxItems, showError]);
70
88
  const handleCancel = useCallback(() => {
71
89
  setSelectingImage(false);
72
90
  }, []);
@@ -83,9 +101,13 @@ export const useImageGallery = _ref => {
83
101
  ...ownerEntity,
84
102
  companyId: currentCompany,
85
103
  image_code: imagesToSave,
86
- company_id: currentCompany
104
+ image_codes: imagesToSave,
105
+ company_id: currentCompany,
106
+ action: 'delete',
107
+ deletedImageCode: image.image_code
87
108
  });
88
109
  if (result.success) {
110
+ onDelete?.(image.image_code);
89
111
  onRefresh?.(result);
90
112
  } else {
91
113
  showError(result.message);
@@ -95,7 +117,13 @@ export const useImageGallery = _ref => {
95
117
  } finally {
96
118
  setLoading(false);
97
119
  }
98
- }, [ownerEntity, currentCompany, imageList, onRefresh, showError, imageHandlerApi]);
120
+ }, [ownerEntity, currentCompany, imageList, onRefresh, onDelete, showError, imageHandlerApi]);
121
+ const handleSetAsDefault = useCallback(image => {
122
+ if (!image?.image_code || image.image_code === '+') {
123
+ return;
124
+ }
125
+ onSetAsDefault?.(image.image_code);
126
+ }, [onSetAsDefault]);
99
127
 
100
128
  /**
101
129
  * Handles the upload completion for single or multiple images
@@ -149,19 +177,47 @@ export const useImageGallery = _ref => {
149
177
  }
150
178
  });
151
179
  }
180
+ if (maxItems != null && imagesToSave.length > maxItems) {
181
+ showError?.(`Máximo ${maxItems} imágenes permitidas (se intentaron guardar ${imagesToSave.length})`);
182
+ return;
183
+ }
184
+ const existingCodes = imageList.reduce((acc, img) => {
185
+ if (img.image_code !== '+') {
186
+ acc.push(img.image_code);
187
+ }
188
+ return acc;
189
+ }, []);
152
190
  const result = await imageHandlerApi({
153
191
  ...ownerEntity,
154
192
  companyId: currentCompany,
155
193
  company_id: currentCompany,
156
- image_code: imagesToSave
194
+ image_code: imagesToSave,
195
+ image_codes: imagesToSave
157
196
  });
158
197
  if (result.success) {
198
+ if (currentImageCode) {
199
+ const newPrimaryCode = imagesArray[0]?.code;
200
+ if (newPrimaryCode && newPrimaryCode !== currentImageCode) {
201
+ onUpdate?.(currentImageCode, newPrimaryCode);
202
+ }
203
+ imagesArray.slice(1).forEach(image => {
204
+ if (image?.code) {
205
+ onAdd?.(image.code);
206
+ }
207
+ });
208
+ } else {
209
+ imagesArray.forEach(image => {
210
+ if (image?.code && !existingCodes.includes(image.code)) {
211
+ onAdd?.(image.code);
212
+ }
213
+ });
214
+ }
159
215
  setSelectingImage(false);
160
216
  onRefresh?.(result);
161
217
  } else {
162
218
  showError(result.message);
163
219
  }
164
- }, [ownerEntity, currentCompany, imageList, selectedImage, imageHandlerApi, onRefresh, showError]);
220
+ }, [ownerEntity, currentCompany, imageList, selectedImage, imageHandlerApi, onRefresh, onAdd, onUpdate, showError, maxItems]);
165
221
  return {
166
222
  openImage,
167
223
  selectedImageIndex,
@@ -171,12 +227,14 @@ export const useImageGallery = _ref => {
171
227
  zoomLevel,
172
228
  loading,
173
229
  removable,
230
+ canSetDefault,
174
231
  setZoomLevel,
175
232
  handleOpenImage,
176
233
  handleCloseImage,
177
234
  handleImageSelect,
178
235
  handleCancel,
179
236
  handleRemoveImage,
237
+ handleSetAsDefault,
180
238
  handleAfterUpload
181
239
  };
182
240
  };
package/lib/index.d.ts CHANGED
@@ -23,6 +23,26 @@ export interface GoalImageGalleryProps {
23
23
  slotProps?: {
24
24
  selector?: object;
25
25
  };
26
+ /** Maximum number of images allowed in the gallery */
27
+ maxItems?: number;
28
+ /** When false, hides delete actions */
29
+ allowDelete?: boolean;
30
+ /** Minimum images that must remain (delete disabled at or below this count) */
31
+ minItems?: number;
32
+ /** Minimum image width in pixels (passed to selector and validateImageFile) */
33
+ minImageWidth?: number;
34
+ /** Minimum image height in pixels (passed to selector and validateImageFile) */
35
+ minImageHeight?: number;
36
+ /** Image code marked as default (badge + star state) */
37
+ defaultImageCode?: string;
38
+ /** Called when user sets an image as default (multi-image galleries with 2+ images) */
39
+ onSetAsDefault?: (imageCode: string) => void;
40
+ /** Called after a successful delete */
41
+ onDelete?: (imageCode: string) => void;
42
+ /** Called after a successful replace upload */
43
+ onUpdate?: (oldImageCode: string, newImageCode: string) => void;
44
+ /** Called after each new image is added */
45
+ onAdd?: (imageCode: string) => void;
26
46
  }
27
47
 
28
48
  // Image item interface
@@ -54,6 +74,13 @@ export interface UseImageGalleryOptions {
54
74
  currentCompany?: string;
55
75
  onRefresh?: (result: any) => void;
56
76
  showError?: (message: string) => void;
77
+ maxItems?: number;
78
+ allowDelete?: boolean;
79
+ minItems?: number;
80
+ onSetAsDefault?: (imageCode: string) => void;
81
+ onDelete?: (imageCode: string) => void;
82
+ onUpdate?: (oldImageCode: string, newImageCode: string) => void;
83
+ onAdd?: (imageCode: string) => void;
57
84
  }
58
85
 
59
86
  export interface UseImageGalleryReturn {
@@ -65,12 +92,14 @@ export interface UseImageGalleryReturn {
65
92
  zoomLevel: number;
66
93
  loading: boolean;
67
94
  removable: boolean;
95
+ canSetDefault: boolean;
68
96
  setZoomLevel: (level: number) => void;
69
97
  handleOpenImage: (imageUrl: string, index?: number) => void;
70
98
  handleCloseImage: () => void;
71
99
  handleImageSelect: (image: ImageItem | null) => void;
72
100
  handleCancel: () => void;
73
101
  handleRemoveImage: (image: ImageItem) => Promise<void>;
102
+ handleSetAsDefault: (image: ImageItem) => void;
74
103
  handleAfterUpload: (images: UploadedImage | UploadedImage[]) => Promise<void>;
75
104
  }
76
105
 
@@ -105,7 +134,10 @@ export interface ImageGalleryGridProps {
105
134
  onImageClick: (imageUrl: string, index: number) => void;
106
135
  onImageSelect: (image: ImageItem | null) => void;
107
136
  onRemoveImage: (image: ImageItem) => void;
137
+ onSetAsDefault?: (image: ImageItem) => void;
108
138
  onDownloadImage: (imageCode: string) => void;
139
+ canSetDefault?: boolean;
140
+ defaultImageCode?: string;
109
141
  }
110
142
 
111
143
  export interface ImageGalleryModalProps {
@@ -135,7 +167,10 @@ export interface ImageGalleryItemProps {
135
167
  onImageClick: (imageUrl: string, index: number) => void;
136
168
  onImageSelect: (image: ImageItem) => void;
137
169
  onRemoveImage: (image: ImageItem) => void;
170
+ onSetAsDefault?: (image: ImageItem) => void;
138
171
  onDownloadImage: (imageCode: string) => void;
172
+ canSetDefault?: boolean;
173
+ isDefault?: boolean;
139
174
  }
140
175
 
141
176
  export interface ImageGalleryEmptyStateProps {
@@ -200,3 +235,20 @@ export declare const debounce: Debounce;
200
235
  export declare const isTouchDevice: IsTouchDevice;
201
236
  export declare const getImageDimensions: GetImageDimensions;
202
237
  export declare const createImagePreview: CreateImagePreview;
238
+ export declare const countGalleryImages: (imageList: ImageItem[]) => number;
239
+ export declare const validateImageFileSync: (
240
+ file: File,
241
+ options?: {
242
+ maxSize?: number;
243
+ allowedTypes?: string[];
244
+ }
245
+ ) => { isValid: boolean; errors: string[] };
246
+ export declare const validateImageFile: (
247
+ file: File,
248
+ options?: {
249
+ maxSize?: number;
250
+ allowedTypes?: string[];
251
+ minWidth?: number;
252
+ minHeight?: number;
253
+ }
254
+ ) => Promise<{ isValid: boolean; errors: string[]; width?: number; height?: number }>;
package/lib/index.js CHANGED
@@ -20,7 +20,7 @@ export { useImageGallery, useImageZoom, useClipboard, useKeyboardNavigation } fr
20
20
  export { StyledImageContainer, ImageOverlay, StyledModalContainer, StyledModalHeader, StyledModalBody, StyledModalFooter, StyledEmptyStateContainer, StyledImageGridContainer } from './styles/styled-components';
21
21
 
22
22
  // Utility exports
23
- export { handleImageError, downloadImage, processImageCodes, getGridColumns, isValidImageFile, formatFileSize, generateId, debounce, isTouchDevice, getImageDimensions, createImagePreview } from './utils/image-utils';
23
+ export { handleImageError, downloadImage, processImageCodes, getGridColumns, isValidImageFile, formatFileSize, generateId, debounce, isTouchDevice, getImageDimensions, createImagePreview, countGalleryImages, validateImageFile, validateImageFileSync } from './utils/image-utils';
24
24
 
25
25
  // Examples and documentation
26
26
  export { default as GoalImageGalleryExamples } from './examples';
@@ -66,25 +66,9 @@ export const utils = {
66
66
  * @param {Object} options - Validation options
67
67
  * @returns {Object} - Validation result
68
68
  */
69
- validateImageFile: function (file) {
70
- let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
71
- const {
72
- maxSize = 5 * 1024 * 1024,
73
- // 5MB
74
- allowedTypes = FEATURES.SUPPORTED_FORMATS
75
- } = options;
76
- const errors = [];
77
- if (file.size > maxSize) {
78
- errors.push(`File size exceeds ${maxSize / 1024 / 1024}MB limit`);
79
- }
80
- if (!allowedTypes.includes(file.type)) {
81
- errors.push(`File type ${file.type} is not supported`);
82
- }
83
- return {
84
- isValid: errors.length === 0,
85
- errors
86
- };
87
- },
69
+ validateImageFile,
70
+ validateImageFileSync,
71
+ countGalleryImages,
88
72
  /**
89
73
  * Create image handler API wrapper
90
74
  * @param {Function} uploadFunction - Upload function
@@ -136,13 +120,29 @@ export const CONFIG = {
136
120
  };
137
121
 
138
122
  // Export version info
139
- export const VERSION = '2.0.0';
123
+ export const VERSION = '1.2.3';
140
124
  export const CHANGELOG = [{
141
- version: '2.0.0',
142
- date: '2024-01-01',
143
- changes: ['Added multi-method upload support', 'Enhanced UI/UX with Material Design 3', 'Improved accessibility features', 'Added keyboard navigation', 'Mobile optimization', 'Comprehensive documentation']
125
+ version: '1.2.3',
126
+ date: '2026-10-12',
127
+ changes: ['Added defaultImageCode, onSetAsDefault, onDelete, onUpdate, and onAdd lifecycle callbacks', 'Set-as-default control shown only when multiple is true and there are at least two images']
128
+ }, {
129
+ version: '1.2.2',
130
+ date: '2026-10-12',
131
+ changes: ['Added allowDelete, maxItems, minItems, minImageWidth, and minImageHeight props', 'Delete is available for single and multiple galleries when editing (respects minItems)', 'Exported validateImageFile async helper with minimum dimension checks']
132
+ }, {
133
+ version: '1.2.1',
134
+ date: '2026-05-22',
135
+ changes: ['Fixed warning about invalid alignItems prop being passed to DOM elements by Stack components']
136
+ }, {
137
+ version: '1.2.0',
138
+ date: '2026-04-29',
139
+ changes: ['Upgraded to MUI v9', 'Migrated to pnpm package manager', 'Implemented Docker build pipeline', 'Added SOLID principles documentation']
140
+ }, {
141
+ version: '1.1.1',
142
+ date: '2025-07-14',
143
+ changes: ['Enhanced modal responsiveness and accessibility', 'Replaced styled-components with MUI Box']
144
144
  }, {
145
145
  version: '1.0.0',
146
- date: '2023-12-01',
147
- changes: ['Initial release', 'Basic image gallery functionality', 'File upload support', 'Image viewer modal']
146
+ date: '2025-07-11',
147
+ changes: ['Initial release with multi-method upload support', 'Enhanced UI/UX with Material Design 3', 'Added keyboard navigation and accessibility features']
148
148
  }];
@@ -47,13 +47,94 @@ export const getGridColumns = (isMobile, isTablet, multiple) => {
47
47
  if (isTablet) return multiple ? 4 : 6; // 3 or 2 columns on tablet
48
48
  return multiple ? 3 : 4; // 4 or 3 columns on desktop
49
49
  };
50
+ const DEFAULT_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
50
51
 
51
52
  /**
52
53
  * Validate image file type
53
54
  */
54
55
  export const isValidImageFile = file => {
55
- const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
56
- return validTypes.includes(file.type);
56
+ return DEFAULT_IMAGE_TYPES.includes(file.type);
57
+ };
58
+
59
+ /**
60
+ * Count gallery images (excludes the "+" add placeholder).
61
+ */
62
+ export const countGalleryImages = function () {
63
+ let imageList = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
64
+ return imageList.filter(img => img?.image_code && img.image_code !== '+').length;
65
+ };
66
+
67
+ /**
68
+ * Validate image file size and type (sync). Use validateImageFile for dimension checks.
69
+ */
70
+ export const validateImageFileSync = function (file) {
71
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
72
+ const {
73
+ maxSize = 25 * 1024 * 1024,
74
+ allowedTypes = DEFAULT_IMAGE_TYPES
75
+ } = options;
76
+ const errors = [];
77
+ if (!file) {
78
+ errors.push('No se ha seleccionado ningún archivo');
79
+ } else {
80
+ if (maxSize && file.size > maxSize) {
81
+ errors.push(`El tamaño del archivo supera el límite de ${formatFileSize(maxSize)}`);
82
+ }
83
+ if (allowedTypes.length && !allowedTypes.includes(file.type)) {
84
+ errors.push(`El tipo de archivo ${file.type || 'desconocido'} no está permitido`);
85
+ }
86
+ }
87
+ return {
88
+ isValid: errors.length === 0,
89
+ errors
90
+ };
91
+ };
92
+
93
+ /**
94
+ * Validate image file including optional minimum width and height (async).
95
+ */
96
+ export const validateImageFile = async function (file) {
97
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
98
+ const {
99
+ minWidth,
100
+ minHeight,
101
+ minImageWidth,
102
+ minImageHeight,
103
+ ...syncOptions
104
+ } = options;
105
+ const resolvedMinWidth = minWidth ?? minImageWidth;
106
+ const resolvedMinHeight = minHeight ?? minImageHeight;
107
+ const syncResult = validateImageFileSync(file, syncOptions);
108
+ if (!syncResult.isValid || !resolvedMinWidth && !resolvedMinHeight) {
109
+ return syncResult;
110
+ }
111
+ const objectUrl = URL.createObjectURL(file);
112
+ try {
113
+ const {
114
+ width,
115
+ height
116
+ } = await getImageDimensions(objectUrl);
117
+ const errors = [...syncResult.errors];
118
+ if (resolvedMinWidth && width < resolvedMinWidth) {
119
+ errors.push(`El ancho mínimo es ${resolvedMinWidth}px (la imagen tiene ${width}px)`);
120
+ }
121
+ if (resolvedMinHeight && height < resolvedMinHeight) {
122
+ errors.push(`El alto mínimo es ${resolvedMinHeight}px (la imagen tiene ${height}px)`);
123
+ }
124
+ return {
125
+ isValid: errors.length === 0,
126
+ errors,
127
+ width,
128
+ height
129
+ };
130
+ } catch {
131
+ return {
132
+ isValid: false,
133
+ errors: [...syncResult.errors, 'No se pudieron leer las dimensiones de la imagen']
134
+ };
135
+ } finally {
136
+ URL.revokeObjectURL(objectUrl);
137
+ }
57
138
  };
58
139
 
59
140
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgonza02/goal-image-gallery",
3
- "version": "1.2.0",
3
+ "version": "1.2.3",
4
4
  "description": "Advanced, accessible, and highly customizable React image gallery component with multi-method upload, responsive design, and extensive TypeScript support",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.js",