@mgonza02/goal-image-gallery 1.2.1 → 1.2.4

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.4] - 2026-10-12
9
+
10
+ ### Added
11
+ - **Floating Add Button**: Replaced the static '+' grid item with a floating action button (Fab) over the image gallery when `canAdd` is active.
12
+
13
+ ## [1.2.3] - 2026-10-12
14
+
15
+ ### Added
16
+ - **`defaultImageCode`**: Highlights the default image in the grid.
17
+ - **`onSetAsDefault(imageCode)`**: Star action when `multiple` and at least two images (requires handler).
18
+ - **`onDelete(imageCode)`**, **`onUpdate(oldCode, newCode)`**, **`onAdd(imageCode)`**: Fired after successful gallery operations.
19
+
20
+ ## [1.2.2] - 2026-10-12
21
+
22
+ ### Added
23
+ - **`maxItems`**: Cap gallery size; hides the add tile and blocks uploads beyond the limit.
24
+ - **`allowDelete` / `minItems`**: Configurable delete in single- and multi-image mode (delete was previously limited to multi with 2+ images).
25
+ - **`minImageWidth` / `minImageHeight`**: Minimum dimension constraints, passed to custom selectors and `validateImageFile`.
26
+ - **`validateImageFile`**, **`validateImageFileSync`**, **`countGalleryImages`** exported from `image-utils`.
27
+
28
+ ### Changed
29
+ - Delete API payload includes `action: 'delete'`, `image_codes`, and `deletedImageCode` for backends that branch on operation type.
30
+
8
31
  ## [1.2.1] - 2026-05-22
9
32
 
10
33
  ### 🐛 Bug Fixes
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
 
@@ -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,15 @@ const GoalImageGallery = _ref => {
259
282
  zoomLevel,
260
283
  loading,
261
284
  removable,
285
+ canSetDefault,
286
+ canAdd,
262
287
  setZoomLevel,
263
288
  handleOpenImage,
264
289
  handleCloseImage,
265
290
  handleImageSelect,
266
291
  handleCancel,
267
292
  handleRemoveImage,
293
+ handleSetAsDefault,
268
294
  handleAfterUpload
269
295
  } = useImageGallery({
270
296
  imageCodes,
@@ -275,7 +301,14 @@ const GoalImageGallery = _ref => {
275
301
  ownerEntity,
276
302
  currentCompany,
277
303
  onRefresh,
278
- showError
304
+ showError,
305
+ maxItems,
306
+ allowDelete,
307
+ minItems,
308
+ onSetAsDefault,
309
+ onDelete,
310
+ onUpdate,
311
+ onAdd
279
312
  });
280
313
 
281
314
  // Zoom and navigation handlers
@@ -338,7 +371,7 @@ const GoalImageGallery = _ref => {
338
371
  multiple: multiple,
339
372
  title: multiple ? "Seleccionar Imágenes" : "Seleccionar Imagen",
340
373
  uploadMessage: multiple ? "Arrastra imágenes aquí o haz clic para seleccionar" : "Arrastra una imagen aquí o haz clic para seleccionar",
341
- ...imageSelectorProps
374
+ ...mergedSelectorProps
342
375
  }) : /*#__PURE__*/_jsxs("div", {
343
376
  style: {
344
377
  padding: "20px",
@@ -380,7 +413,11 @@ const GoalImageGallery = _ref => {
380
413
  onImageClick: handleOpenImage,
381
414
  onImageSelect: handleImageSelect,
382
415
  onRemoveImage: handleRemoveImage,
383
- onDownloadImage: handleDownloadImage
416
+ onSetAsDefault: handleSetAsDefault,
417
+ onDownloadImage: handleDownloadImage,
418
+ canSetDefault: canSetDefault,
419
+ canAdd: canAdd,
420
+ defaultImageCode: defaultImageCode
384
421
  }), /*#__PURE__*/_jsx(ImageGalleryModal, {
385
422
  open: openImage !== null,
386
423
  imageUrl: openImage,
@@ -413,6 +450,16 @@ GoalImageGallery.propTypes = {
413
450
  hasPermission: PropTypes.func,
414
451
  currentCompany: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
415
452
  slot: PropTypes.object,
416
- slotProps: PropTypes.object
453
+ slotProps: PropTypes.object,
454
+ maxItems: PropTypes.number,
455
+ allowDelete: PropTypes.bool,
456
+ minItems: PropTypes.number,
457
+ minImageWidth: PropTypes.number,
458
+ minImageHeight: PropTypes.number,
459
+ defaultImageCode: PropTypes.string,
460
+ onSetAsDefault: PropTypes.func,
461
+ onDelete: PropTypes.func,
462
+ onUpdate: PropTypes.func,
463
+ onAdd: PropTypes.func
417
464
  };
418
465
  export default GoalImageGallery;
@@ -1,5 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
- import { Grid, Card, CardContent } from '@mui/material';
2
+ import { Grid, Card, CardContent, Fab, Tooltip } from '@mui/material';
3
+ import { Add as AddIcon } from '@mui/icons-material';
3
4
  import { useTheme } from '@mui/material/styles';
4
5
  import ImageGalleryItem from './ImageGalleryItem';
5
6
  import ImageGalleryEmptyState from './ImageGalleryEmptyState';
@@ -8,7 +9,7 @@ import { StyledImageGridContainer } from '../styles/styled-components';
8
9
  /**
9
10
  * Image grid component for displaying gallery images
10
11
  */
11
- import { jsx as _jsx } from "react/jsx-runtime";
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
13
  const ImageGalleryGrid = _ref => {
13
14
  let {
14
15
  imageList,
@@ -25,7 +26,11 @@ const ImageGalleryGrid = _ref => {
25
26
  onImageClick,
26
27
  onImageSelect,
27
28
  onRemoveImage,
28
- onDownloadImage
29
+ onSetAsDefault,
30
+ onDownloadImage,
31
+ canSetDefault = false,
32
+ canAdd = false,
33
+ defaultImageCode
29
34
  } = _ref;
30
35
  const theme = useTheme();
31
36
  if (imageList.length === 0) {
@@ -41,9 +46,12 @@ const ImageGalleryGrid = _ref => {
41
46
  })
42
47
  });
43
48
  }
44
- return /*#__PURE__*/_jsx(Card, {
49
+ return /*#__PURE__*/_jsxs(Card, {
45
50
  elevation: 2,
46
- children: /*#__PURE__*/_jsx(CardContent, {
51
+ sx: {
52
+ position: 'relative'
53
+ },
54
+ children: [/*#__PURE__*/_jsx(CardContent, {
47
55
  children: /*#__PURE__*/_jsx(StyledImageGridContainer, {
48
56
  children: /*#__PURE__*/_jsx(Grid, {
49
57
  container: true,
@@ -70,12 +78,30 @@ const ImageGalleryGrid = _ref => {
70
78
  onImageClick: onImageClick,
71
79
  onImageSelect: onImageSelect,
72
80
  onRemoveImage: onRemoveImage,
73
- onDownloadImage: onDownloadImage
81
+ onSetAsDefault: onSetAsDefault,
82
+ onDownloadImage: onDownloadImage,
83
+ canSetDefault: canSetDefault,
84
+ isDefault: !!defaultImageCode && image.image_code === defaultImageCode
74
85
  })
75
86
  }, image.id))
76
87
  })
77
88
  })
78
- })
89
+ }), canAdd && /*#__PURE__*/_jsx(Tooltip, {
90
+ title: "Agregar imagen",
91
+ children: /*#__PURE__*/_jsx(Fab, {
92
+ color: "primary",
93
+ size: "small",
94
+ "aria-label": "Agregar imagen",
95
+ onClick: () => onImageSelect(null),
96
+ sx: {
97
+ position: 'absolute',
98
+ bottom: 16,
99
+ right: 16,
100
+ zIndex: 3
101
+ },
102
+ children: /*#__PURE__*/_jsx(AddIcon, {})
103
+ })
104
+ })]
79
105
  });
80
106
  };
81
107
  ImageGalleryGrid.propTypes = {
@@ -93,6 +119,10 @@ ImageGalleryGrid.propTypes = {
93
119
  onImageClick: PropTypes.func.isRequired,
94
120
  onImageSelect: PropTypes.func.isRequired,
95
121
  onRemoveImage: PropTypes.func.isRequired,
96
- onDownloadImage: PropTypes.func.isRequired
122
+ onSetAsDefault: PropTypes.func,
123
+ onDownloadImage: PropTypes.func.isRequired,
124
+ canSetDefault: PropTypes.bool,
125
+ canAdd: PropTypes.bool,
126
+ defaultImageCode: PropTypes.string
97
127
  };
98
128
  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,8 @@ 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);
35
+ const [canAdd, setCanAdd] = useState(false);
26
36
 
27
37
  // Process image codes into image list
28
38
  useEffect(() => {
@@ -40,17 +50,14 @@ export const useImageGallery = _ref => {
40
50
  } else {
41
51
  newList = [];
42
52
  }
43
- setRemovable(multiple && newList.length > 1);
44
-
45
- // Add plus button for multiple mode
46
- if (updatable && multiple) {
47
- newList.push({
48
- image_code: '+',
49
- id: newList.length
50
- });
51
- }
53
+ const imageCount = newList.length;
54
+ const canRemoveMore = imageCount > minItems;
55
+ setRemovable(!!(updatable && allowDelete && imageCount > 0 && canRemoveMore));
56
+ const atMaxItems = maxItems != null && imageCount >= maxItems;
57
+ setCanAdd(!!(updatable && multiple && !atMaxItems));
58
+ setCanSetDefault(!!(multiple && imageCount >= 2 && onSetAsDefault));
52
59
  setImageList(newList);
53
- }, [imageCodes, canEdit, multiple, updatable]);
60
+ }, [imageCodes, canEdit, multiple, updatable, maxItems, allowDelete, minItems, onSetAsDefault]);
54
61
 
55
62
  // Image handlers
56
63
  const handleOpenImage = useCallback(function (imageUrl) {
@@ -64,9 +71,16 @@ export const useImageGallery = _ref => {
64
71
  setZoomLevel(1);
65
72
  }, []);
66
73
  const handleImageSelect = useCallback(image => {
74
+ if (image === null && maxItems != null) {
75
+ const currentCount = countGalleryImages(imageList);
76
+ if (currentCount >= maxItems) {
77
+ showError?.(`Máximo ${maxItems} imágenes permitidas`);
78
+ return;
79
+ }
80
+ }
67
81
  setSelectedImage(image);
68
82
  setSelectingImage(true);
69
- }, []);
83
+ }, [imageList, maxItems, showError]);
70
84
  const handleCancel = useCallback(() => {
71
85
  setSelectingImage(false);
72
86
  }, []);
@@ -83,9 +97,13 @@ export const useImageGallery = _ref => {
83
97
  ...ownerEntity,
84
98
  companyId: currentCompany,
85
99
  image_code: imagesToSave,
86
- company_id: currentCompany
100
+ image_codes: imagesToSave,
101
+ company_id: currentCompany,
102
+ action: 'delete',
103
+ deletedImageCode: image.image_code
87
104
  });
88
105
  if (result.success) {
106
+ onDelete?.(image.image_code);
89
107
  onRefresh?.(result);
90
108
  } else {
91
109
  showError(result.message);
@@ -95,7 +113,13 @@ export const useImageGallery = _ref => {
95
113
  } finally {
96
114
  setLoading(false);
97
115
  }
98
- }, [ownerEntity, currentCompany, imageList, onRefresh, showError, imageHandlerApi]);
116
+ }, [ownerEntity, currentCompany, imageList, onRefresh, onDelete, showError, imageHandlerApi]);
117
+ const handleSetAsDefault = useCallback(image => {
118
+ if (!image?.image_code || image.image_code === '+') {
119
+ return;
120
+ }
121
+ onSetAsDefault?.(image.image_code);
122
+ }, [onSetAsDefault]);
99
123
 
100
124
  /**
101
125
  * Handles the upload completion for single or multiple images
@@ -149,19 +173,47 @@ export const useImageGallery = _ref => {
149
173
  }
150
174
  });
151
175
  }
176
+ if (maxItems != null && imagesToSave.length > maxItems) {
177
+ showError?.(`Máximo ${maxItems} imágenes permitidas (se intentaron guardar ${imagesToSave.length})`);
178
+ return;
179
+ }
180
+ const existingCodes = imageList.reduce((acc, img) => {
181
+ if (img.image_code !== '+') {
182
+ acc.push(img.image_code);
183
+ }
184
+ return acc;
185
+ }, []);
152
186
  const result = await imageHandlerApi({
153
187
  ...ownerEntity,
154
188
  companyId: currentCompany,
155
189
  company_id: currentCompany,
156
- image_code: imagesToSave
190
+ image_code: imagesToSave,
191
+ image_codes: imagesToSave
157
192
  });
158
193
  if (result.success) {
194
+ if (currentImageCode) {
195
+ const newPrimaryCode = imagesArray[0]?.code;
196
+ if (newPrimaryCode && newPrimaryCode !== currentImageCode) {
197
+ onUpdate?.(currentImageCode, newPrimaryCode);
198
+ }
199
+ imagesArray.slice(1).forEach(image => {
200
+ if (image?.code) {
201
+ onAdd?.(image.code);
202
+ }
203
+ });
204
+ } else {
205
+ imagesArray.forEach(image => {
206
+ if (image?.code && !existingCodes.includes(image.code)) {
207
+ onAdd?.(image.code);
208
+ }
209
+ });
210
+ }
159
211
  setSelectingImage(false);
160
212
  onRefresh?.(result);
161
213
  } else {
162
214
  showError(result.message);
163
215
  }
164
- }, [ownerEntity, currentCompany, imageList, selectedImage, imageHandlerApi, onRefresh, showError]);
216
+ }, [ownerEntity, currentCompany, imageList, selectedImage, imageHandlerApi, onRefresh, onAdd, onUpdate, showError, maxItems]);
165
217
  return {
166
218
  openImage,
167
219
  selectedImageIndex,
@@ -171,12 +223,15 @@ export const useImageGallery = _ref => {
171
223
  zoomLevel,
172
224
  loading,
173
225
  removable,
226
+ canSetDefault,
227
+ canAdd,
174
228
  setZoomLevel,
175
229
  handleOpenImage,
176
230
  handleCloseImage,
177
231
  handleImageSelect,
178
232
  handleCancel,
179
233
  handleRemoveImage,
234
+ handleSetAsDefault,
180
235
  handleAfterUpload
181
236
  };
182
237
  };
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,15 @@ export interface UseImageGalleryReturn {
65
92
  zoomLevel: number;
66
93
  loading: boolean;
67
94
  removable: boolean;
95
+ canSetDefault: boolean;
96
+ canAdd: boolean;
68
97
  setZoomLevel: (level: number) => void;
69
98
  handleOpenImage: (imageUrl: string, index?: number) => void;
70
99
  handleCloseImage: () => void;
71
100
  handleImageSelect: (image: ImageItem | null) => void;
72
101
  handleCancel: () => void;
73
102
  handleRemoveImage: (image: ImageItem) => Promise<void>;
103
+ handleSetAsDefault: (image: ImageItem) => void;
74
104
  handleAfterUpload: (images: UploadedImage | UploadedImage[]) => Promise<void>;
75
105
  }
76
106
 
@@ -105,7 +135,11 @@ export interface ImageGalleryGridProps {
105
135
  onImageClick: (imageUrl: string, index: number) => void;
106
136
  onImageSelect: (image: ImageItem | null) => void;
107
137
  onRemoveImage: (image: ImageItem) => void;
138
+ onSetAsDefault?: (image: ImageItem) => void;
108
139
  onDownloadImage: (imageCode: string) => void;
140
+ canSetDefault?: boolean;
141
+ canAdd?: boolean;
142
+ defaultImageCode?: string;
109
143
  }
110
144
 
111
145
  export interface ImageGalleryModalProps {
@@ -135,7 +169,10 @@ export interface ImageGalleryItemProps {
135
169
  onImageClick: (imageUrl: string, index: number) => void;
136
170
  onImageSelect: (image: ImageItem) => void;
137
171
  onRemoveImage: (image: ImageItem) => void;
172
+ onSetAsDefault?: (image: ImageItem) => void;
138
173
  onDownloadImage: (imageCode: string) => void;
174
+ canSetDefault?: boolean;
175
+ isDefault?: boolean;
139
176
  }
140
177
 
141
178
  export interface ImageGalleryEmptyStateProps {
@@ -200,3 +237,20 @@ export declare const debounce: Debounce;
200
237
  export declare const isTouchDevice: IsTouchDevice;
201
238
  export declare const getImageDimensions: GetImageDimensions;
202
239
  export declare const createImagePreview: CreateImagePreview;
240
+ export declare const countGalleryImages: (imageList: ImageItem[]) => number;
241
+ export declare const validateImageFileSync: (
242
+ file: File,
243
+ options?: {
244
+ maxSize?: number;
245
+ allowedTypes?: string[];
246
+ }
247
+ ) => { isValid: boolean; errors: string[] };
248
+ export declare const validateImageFile: (
249
+ file: File,
250
+ options?: {
251
+ maxSize?: number;
252
+ allowedTypes?: string[];
253
+ minWidth?: number;
254
+ minHeight?: number;
255
+ }
256
+ ) => 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,8 +120,20 @@ export const CONFIG = {
136
120
  };
137
121
 
138
122
  // Export version info
139
- export const VERSION = '1.2.1';
123
+ export const VERSION = '1.2.4';
140
124
  export const CHANGELOG = [{
125
+ version: '1.2.4',
126
+ date: '2026-10-12',
127
+ changes: ['Replaced grid add placeholder tile with a floating action button over the image gallery when canAdd is true']
128
+ }, {
129
+ version: '1.2.3',
130
+ date: '2026-10-12',
131
+ 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']
132
+ }, {
133
+ version: '1.2.2',
134
+ date: '2026-10-12',
135
+ 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']
136
+ }, {
141
137
  version: '1.2.1',
142
138
  date: '2026-05-22',
143
139
  changes: ['Fixed warning about invalid alignItems prop being passed to DOM elements by Stack components']
@@ -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.1",
3
+ "version": "1.2.4",
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",