@mgonza02/goal-image-gallery 1.0.2 → 1.0.5
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/DOCUMENTATION_UPDATE_SUMMARY.md +311 -0
- package/IMPLEMENTATION_GUIDE.md +1392 -0
- package/PACKAGE_CLEANUP_SUMMARY.md +146 -0
- package/QUICK_REFERENCE.md +46 -0
- package/README.md +850 -85
- package/lib/GoalImageGallery.js +200 -40
- package/lib/components/ImageGalleryEmptyState.js +3 -8
- package/lib/components/ImageGalleryGrid.js +7 -16
- package/lib/components/ImageGalleryItem.js +5 -12
- package/lib/components/ImageGalleryModal.js +0 -3
- package/lib/examples.js +192 -1
- package/lib/index.d.ts +6 -0
- package/lib/styles/styled-components.js +2 -2
- package/package.json +7 -20
package/README.md
CHANGED
|
@@ -2,7 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
The `GoalImageGallery` is a modern, feature-rich image gallery component for
|
|
5
|
+
The `GoalImageGallery` is a modern, feature-rich image gallery component for React applications. It provides a comprehensive solution for displaying, managing, and uploading images with advanced UI/UX features, accessibility support, and extensive customization options. This component is designed to be highly modular, performant, and easy to integrate into any React application.
|
|
6
|
+
|
|
7
|
+
## 🚀 Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @mgonza02/goal-image-gallery
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { GoalImageGallery } from '@mgonza02/goal-image-gallery';
|
|
15
|
+
|
|
16
|
+
// Basic usage
|
|
17
|
+
<GoalImageGallery imageCodes={['img1', 'img2']} canEdit={false} />
|
|
18
|
+
```
|
|
6
19
|
|
|
7
20
|
## Key Features
|
|
8
21
|
|
|
@@ -10,152 +23,770 @@ The `GoalImageGallery` is a modern, feature-rich image gallery component for the
|
|
|
10
23
|
- **File Upload**: Traditional file selection with drag-and-drop support
|
|
11
24
|
- **Clipboard Paste**: Paste images directly from clipboard using Ctrl+V
|
|
12
25
|
- **URL Loading**: Load images from remote URLs
|
|
26
|
+
- **Batch Upload**: Upload multiple images simultaneously
|
|
13
27
|
|
|
14
28
|
### 🎨 **Modern UI/UX**
|
|
15
|
-
- Responsive
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Dark/
|
|
19
|
-
-
|
|
29
|
+
- **Responsive Design**: Adapts to all screen sizes and devices
|
|
30
|
+
- **Material Design 3**: Latest Material-UI components and styling
|
|
31
|
+
- **Smooth Animations**: Hover effects, transitions, and loading states
|
|
32
|
+
- **Dark/Light Theme**: Automatic theme detection and support
|
|
33
|
+
- **Accessibility**: WCAG 2.1 AA compliant with full keyboard navigation
|
|
20
34
|
|
|
21
35
|
### 🔍 **Advanced Image Viewer**
|
|
22
|
-
- Full-
|
|
23
|
-
- Zoom
|
|
24
|
-
- Image
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
###
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
- Optimized
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
- **Full-Screen Modal**: Immersive image viewing experience
|
|
37
|
+
- **Zoom Controls**: Zoom from 0.5x to 3x with smooth transitions
|
|
38
|
+
- **Image Navigation**: Previous/next with keyboard shortcuts
|
|
39
|
+
- **Download Support**: Direct image download functionality
|
|
40
|
+
- **Image Information**: Display metadata and details
|
|
41
|
+
|
|
42
|
+
### 🛠️ **Customization & Extensibility**
|
|
43
|
+
- **Slot System**: Replace default components with custom implementations
|
|
44
|
+
- **Flexible Props**: Extensive configuration options
|
|
45
|
+
- **TypeScript Support**: Full type definitions included
|
|
46
|
+
- **Hook-Based Architecture**: Reusable custom hooks for complex logic
|
|
47
|
+
|
|
48
|
+
### 📱 **Mobile-First Design**
|
|
49
|
+
- **Touch Gestures**: Swipe, pinch-to-zoom, and tap interactions
|
|
50
|
+
- **Responsive Breakpoints**: Optimized layouts for all devices
|
|
51
|
+
- **Performance Optimized**: Lazy loading and efficient rendering
|
|
52
|
+
- **Offline Support**: Graceful degradation for poor connectivity
|
|
53
|
+
|
|
54
|
+
## Installation & Setup
|
|
55
|
+
|
|
56
|
+
### NPM Installation
|
|
42
57
|
|
|
43
58
|
```bash
|
|
44
59
|
npm install @mgonza02/goal-image-gallery
|
|
45
60
|
```
|
|
46
61
|
|
|
62
|
+
### Peer Dependencies
|
|
63
|
+
|
|
64
|
+
Make sure you have the required peer dependencies installed:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled react react-dom
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Import
|
|
71
|
+
|
|
47
72
|
```javascript
|
|
73
|
+
// Main component
|
|
48
74
|
import { GoalImageGallery } from '@mgonza02/goal-image-gallery';
|
|
75
|
+
|
|
76
|
+
// Individual components and hooks
|
|
77
|
+
import {
|
|
78
|
+
ImageGalleryGrid,
|
|
79
|
+
ImageGalleryModal,
|
|
80
|
+
useImageGallery,
|
|
81
|
+
useImageZoom
|
|
82
|
+
} from '@mgonza02/goal-image-gallery';
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### TypeScript Support
|
|
86
|
+
|
|
87
|
+
The component includes full TypeScript definitions:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { GoalImageGalleryProps } from '@mgonza02/goal-image-gallery';
|
|
91
|
+
|
|
92
|
+
const MyComponent: React.FC<{ images: string[] }> = ({ images }) => {
|
|
93
|
+
return (
|
|
94
|
+
<GoalImageGallery
|
|
95
|
+
imageCodes={images}
|
|
96
|
+
canEdit={true}
|
|
97
|
+
// TypeScript will provide full autocomplete and type checking
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
49
101
|
```
|
|
50
102
|
|
|
51
|
-
##
|
|
103
|
+
## Usage Examples
|
|
104
|
+
|
|
105
|
+
### 1. Basic Read-Only Gallery
|
|
106
|
+
|
|
107
|
+
Perfect for displaying images without editing capabilities:
|
|
52
108
|
|
|
53
|
-
### Simple Gallery
|
|
54
109
|
```javascript
|
|
55
110
|
<GoalImageGallery
|
|
56
111
|
imageCodes={['img1', 'img2', 'img3']}
|
|
57
112
|
canEdit={false}
|
|
113
|
+
emptyMessage="No images to display"
|
|
114
|
+
getImageUrl={({ imageCode }) => `https://api.example.com/images/${imageCode}`}
|
|
58
115
|
/>
|
|
59
116
|
```
|
|
60
117
|
|
|
61
|
-
### Editable Gallery
|
|
118
|
+
### 2. Editable Product Gallery
|
|
119
|
+
|
|
120
|
+
Full-featured gallery with upload, edit, and delete capabilities:
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
const ProductGallery = () => {
|
|
124
|
+
const [productImages, setProductImages] = useState(['prod1', 'prod2']);
|
|
125
|
+
|
|
126
|
+
const handleImageUpload = async (imageData) => {
|
|
127
|
+
try {
|
|
128
|
+
const response = await api.uploadProductImage({
|
|
129
|
+
...imageData,
|
|
130
|
+
productId: product.id,
|
|
131
|
+
companyId: currentCompany
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
data: response.data,
|
|
137
|
+
message: 'Images uploaded successfully'
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
message: error.message
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const refreshProduct = (result) => {
|
|
148
|
+
// Update product data
|
|
149
|
+
setProductImages(prev => [...prev, result.data.code]);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<GoalImageGallery
|
|
154
|
+
imageCodes={productImages}
|
|
155
|
+
canEdit={true}
|
|
156
|
+
ownerEntity={{ id: product.id, type: 'product' }}
|
|
157
|
+
imageHandlerApi={handleImageUpload}
|
|
158
|
+
afterUpload={refreshProduct}
|
|
159
|
+
multiple={true}
|
|
160
|
+
emptyMessage="No product images available"
|
|
161
|
+
showImageInfo={true}
|
|
162
|
+
allowDownload={true}
|
|
163
|
+
permission="EDIT_PRODUCTS"
|
|
164
|
+
hasPermission={(perm) => userPermissions.includes(perm)}
|
|
165
|
+
currentCompany={user.companyId}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 3. User Avatar Gallery
|
|
172
|
+
|
|
173
|
+
Single image gallery for user avatars:
|
|
174
|
+
|
|
62
175
|
```javascript
|
|
63
176
|
<GoalImageGallery
|
|
64
|
-
imageCodes={
|
|
65
|
-
canEdit={
|
|
66
|
-
ownerEntity={{ id: '
|
|
67
|
-
imageHandlerApi={
|
|
68
|
-
afterUpload={
|
|
69
|
-
multiple={
|
|
177
|
+
imageCodes={user.avatar}
|
|
178
|
+
canEdit={isOwner}
|
|
179
|
+
ownerEntity={{ id: user.id, type: 'user' }}
|
|
180
|
+
imageHandlerApi={userImageApi.upload}
|
|
181
|
+
afterUpload={refreshUser}
|
|
182
|
+
multiple={false}
|
|
183
|
+
emptyMessage="No avatar set"
|
|
184
|
+
showImageInfo={false}
|
|
185
|
+
getImageUrl={({ imageCode }) => `https://cdn.example.com/avatars/${imageCode}`}
|
|
70
186
|
/>
|
|
71
187
|
```
|
|
72
188
|
|
|
73
|
-
###
|
|
189
|
+
### 4. Custom Image Selector
|
|
190
|
+
|
|
191
|
+
Using the slot system to provide a custom image selector:
|
|
192
|
+
|
|
74
193
|
```javascript
|
|
194
|
+
// Custom selector component
|
|
195
|
+
const CustomImageSelector = ({
|
|
196
|
+
afterUpload,
|
|
197
|
+
activate,
|
|
198
|
+
onCancel,
|
|
199
|
+
multiple,
|
|
200
|
+
title,
|
|
201
|
+
uploadMessage,
|
|
202
|
+
maxFileSize,
|
|
203
|
+
acceptedFormats,
|
|
204
|
+
customStyles
|
|
205
|
+
}) => {
|
|
206
|
+
const [dragActive, setDragActive] = useState(false);
|
|
207
|
+
const [uploading, setUploading] = useState(false);
|
|
208
|
+
|
|
209
|
+
const handleFiles = async (files) => {
|
|
210
|
+
setUploading(true);
|
|
211
|
+
try {
|
|
212
|
+
// Custom upload logic
|
|
213
|
+
const result = await uploadFiles(files);
|
|
214
|
+
afterUpload(result);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error('Upload failed:', error);
|
|
217
|
+
} finally {
|
|
218
|
+
setUploading(false);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div
|
|
224
|
+
style={{
|
|
225
|
+
...customStyles,
|
|
226
|
+
border: dragActive ? '2px solid #007bff' : '2px dashed #ccc',
|
|
227
|
+
borderRadius: '8px',
|
|
228
|
+
padding: '40px',
|
|
229
|
+
textAlign: 'center',
|
|
230
|
+
backgroundColor: dragActive ? '#f8f9fa' : '#fff'
|
|
231
|
+
}}
|
|
232
|
+
onDragOver={(e) => {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
setDragActive(true);
|
|
235
|
+
}}
|
|
236
|
+
onDragLeave={() => setDragActive(false)}
|
|
237
|
+
onDrop={async (e) => {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
setDragActive(false);
|
|
240
|
+
const files = Array.from(e.dataTransfer.files);
|
|
241
|
+
await handleFiles(files);
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<h3>{title}</h3>
|
|
245
|
+
<p>{uploadMessage}</p>
|
|
246
|
+
{uploading && <p>Uploading...</p>}
|
|
247
|
+
<input
|
|
248
|
+
type="file"
|
|
249
|
+
multiple={multiple}
|
|
250
|
+
accept={acceptedFormats?.join(',')}
|
|
251
|
+
onChange={(e) => handleFiles(Array.from(e.target.files))}
|
|
252
|
+
style={{ margin: '10px 0' }}
|
|
253
|
+
/>
|
|
254
|
+
<br />
|
|
255
|
+
<button onClick={onCancel} disabled={uploading}>
|
|
256
|
+
Cancel
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Usage with custom selector
|
|
75
263
|
<GoalImageGallery
|
|
76
|
-
imageCodes={
|
|
264
|
+
imageCodes={images}
|
|
77
265
|
canEdit={true}
|
|
266
|
+
ownerEntity={{ id: 'entity-123' }}
|
|
267
|
+
imageHandlerApi={handleUpload}
|
|
268
|
+
afterUpload={refreshData}
|
|
269
|
+
multiple={true}
|
|
270
|
+
slot={{
|
|
271
|
+
selector: CustomImageSelector
|
|
272
|
+
}}
|
|
273
|
+
slotProps={{
|
|
274
|
+
selector: {
|
|
275
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
276
|
+
acceptedFormats: ['image/jpeg', 'image/png', 'image/webp'],
|
|
277
|
+
customStyles: {
|
|
278
|
+
borderColor: '#007bff',
|
|
279
|
+
backgroundColor: '#f8f9fa'
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}}
|
|
283
|
+
/>
|
|
284
|
+
```
|
|
78
285
|
ownerEntity={{ id: 'product-123', type: 'product' }}
|
|
79
286
|
imageHandlerApi={handleImageUpload}
|
|
80
287
|
afterUpload={refreshImages}
|
|
81
288
|
multiple={true}
|
|
82
|
-
|
|
83
|
-
|
|
289
|
+
slot={{
|
|
290
|
+
selector: CustomImageSelector
|
|
291
|
+
}}
|
|
292
|
+
slotProps={{
|
|
293
|
+
selector: {
|
|
294
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
295
|
+
acceptedFormats: ['image/jpeg', 'image/png'],
|
|
296
|
+
uploadEndpoint: '/api/upload',
|
|
297
|
+
customStyles: { borderColor: '#007bff' }
|
|
298
|
+
}
|
|
299
|
+
}}
|
|
300
|
+
/>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Props Reference
|
|
304
|
+
|
|
305
|
+
### Core Props
|
|
306
|
+
|
|
307
|
+
| Prop | Type | Default | Required | Description |
|
|
308
|
+
|------|------|---------|----------|-------------|
|
|
309
|
+
| `imageCodes` | `string \| string[]` | `[]` | No | Image codes to display in the gallery |
|
|
310
|
+
| `canEdit` | `boolean` | `false` | No | Enable editing capabilities (upload, delete) |
|
|
311
|
+
| `ownerEntity` | `object` | `undefined` | No | Entity that owns the images (required for editing) |
|
|
312
|
+
| `imageHandlerApi` | `function` | `undefined` | No | API function for image operations (required for editing) |
|
|
313
|
+
| `afterUpload` | `function` | `undefined` | No | Callback function called after successful upload |
|
|
314
|
+
| `multiple` | `boolean` | `false` | No | Allow multiple image selection and upload |
|
|
315
|
+
|
|
316
|
+
### Display Props
|
|
317
|
+
|
|
318
|
+
| Prop | Type | Default | Required | Description |
|
|
319
|
+
|------|------|---------|----------|-------------|
|
|
320
|
+
| `emptyMessage` | `string` | `'No hay imágenes para mostrar'` | No | Message displayed when no images are available |
|
|
321
|
+
| `showImageInfo` | `boolean` | `false` | No | Show image information overlay on hover |
|
|
322
|
+
| `allowDownload` | `boolean` | `false` | No | Enable image download functionality |
|
|
323
|
+
| `getImageUrl` | `function` | `({ imageCode }) => '/images/${imageCode}'` | No | Function to generate image URLs from codes |
|
|
324
|
+
| `noImageUrl` | `string` | `'/images/no-image.png'` | No | Fallback image URL for broken images |
|
|
325
|
+
|
|
326
|
+
### Permission Props
|
|
327
|
+
|
|
328
|
+
| Prop | Type | Default | Required | Description |
|
|
329
|
+
|------|------|---------|----------|-------------|
|
|
330
|
+
| `permission` | `string` | `undefined` | No | Required permission string for editing |
|
|
331
|
+
| `hasPermission` | `function` | `() => true` | No | Function to check if user has required permission |
|
|
332
|
+
| `currentCompany` | `string` | `'default'` | No | Current company identifier for API calls |
|
|
333
|
+
|
|
334
|
+
### Customization Props
|
|
335
|
+
|
|
336
|
+
| Prop | Type | Default | Required | Description |
|
|
337
|
+
|------|------|---------|----------|-------------|
|
|
338
|
+
| `slot` | `object` | `{}` | No | Slot components for customization |
|
|
339
|
+
| `slotProps` | `object` | `{}` | No | Props passed to slot components |
|
|
340
|
+
| `showError` | `function` | `console.error` | No | Function to display error messages |
|
|
341
|
+
|
|
342
|
+
### Advanced Props
|
|
343
|
+
|
|
344
|
+
| Prop | Type | Default | Required | Description |
|
|
345
|
+
|------|------|---------|----------|-------------|
|
|
346
|
+
| `maxFileSize` | `number` | `25 * 1024 * 1024` | No | Maximum file size in bytes (25MB default) |
|
|
347
|
+
| `acceptedFormats` | `string[]` | `['image/jpeg', 'image/png', 'image/gif', 'image/webp']` | No | Accepted image formats |
|
|
348
|
+
| `thumbnailSize` | `number` | `200` | No | Thumbnail size in pixels |
|
|
349
|
+
| `gridColumns` | `number` | `auto` | No | Number of columns in grid (auto-calculated if not provided) |
|
|
350
|
+
| `enableLazyLoading` | `boolean` | `true` | No | Enable lazy loading for images |
|
|
351
|
+
| `enableVirtualization` | `boolean` | `false` | No | Enable virtual scrolling for large image sets |
|
|
352
|
+
|
|
353
|
+
### Prop Examples
|
|
354
|
+
|
|
355
|
+
```javascript
|
|
356
|
+
// Basic configuration
|
|
357
|
+
<GoalImageGallery
|
|
358
|
+
imageCodes={['img1', 'img2', 'img3']}
|
|
359
|
+
canEdit={false}
|
|
360
|
+
/>
|
|
361
|
+
|
|
362
|
+
// Full configuration
|
|
363
|
+
<GoalImageGallery
|
|
364
|
+
imageCodes={product.images}
|
|
365
|
+
canEdit={user.canEdit}
|
|
366
|
+
ownerEntity={{ id: product.id, type: 'product' }}
|
|
367
|
+
imageHandlerApi={async (data) => {
|
|
368
|
+
const response = await api.uploadImage(data);
|
|
369
|
+
return { success: true, data: response };
|
|
370
|
+
}}
|
|
371
|
+
afterUpload={(result) => {
|
|
372
|
+
console.log('Upload successful:', result);
|
|
373
|
+
refreshProduct();
|
|
374
|
+
}}
|
|
375
|
+
multiple={true}
|
|
376
|
+
permission="EDIT_PRODUCTS"
|
|
377
|
+
emptyMessage="No product images yet"
|
|
84
378
|
showImageInfo={true}
|
|
85
379
|
allowDownload={true}
|
|
380
|
+
getImageUrl={({ imageCode, thumbMail }) =>
|
|
381
|
+
`https://api.example.com/images/${imageCode}${thumbMail ? '?thumb=true' : ''}`
|
|
382
|
+
}
|
|
383
|
+
noImageUrl="https://api.example.com/images/placeholder.png"
|
|
384
|
+
hasPermission={(perm) => user.permissions.includes(perm)}
|
|
385
|
+
currentCompany={user.companyId}
|
|
386
|
+
showError={(msg) => toast.error(msg)}
|
|
387
|
+
maxFileSize={10 * 1024 * 1024}
|
|
388
|
+
acceptedFormats={['image/jpeg', 'image/png']}
|
|
389
|
+
thumbnailSize={150}
|
|
390
|
+
gridColumns={3}
|
|
86
391
|
/>
|
|
87
392
|
```
|
|
88
393
|
|
|
89
|
-
##
|
|
394
|
+
## Upload Methods & Features
|
|
395
|
+
|
|
396
|
+
### 1. File Upload (Drag & Drop)
|
|
90
397
|
|
|
91
|
-
|
|
92
|
-
|------|------|---------|-------------|
|
|
93
|
-
| `canEdit` | `boolean` | `false` | Enable editing capabilities |
|
|
94
|
-
| `ownerEntity` | `object` | - | Entity that owns the images |
|
|
95
|
-
| `imageHandlerApi` | `function` | - | API function for image operations |
|
|
96
|
-
| `afterUpload` | `function` | - | Callback after successful upload |
|
|
97
|
-
| `imageCodes` | `string \| array` | - | Image codes to display |
|
|
98
|
-
| `multiple` | `boolean` | `false` | Allow multiple image selection |
|
|
99
|
-
| `permission` | `string` | - | Required permission for editing |
|
|
100
|
-
| `emptyMessage` | `string` | `'No hay imágenes para mostrar'` | Message shown when no images |
|
|
101
|
-
| `showImageInfo` | `boolean` | `false` | Show image information overlay |
|
|
102
|
-
| `allowDownload` | `boolean` | `true` | Enable download functionality |
|
|
398
|
+
The most common method for uploading images:
|
|
103
399
|
|
|
104
|
-
|
|
400
|
+
```javascript
|
|
401
|
+
// Features:
|
|
402
|
+
// - Click to select files
|
|
403
|
+
// - Drag and drop support
|
|
404
|
+
// - Multiple file selection
|
|
405
|
+
// - File type validation
|
|
406
|
+
// - Size limit enforcement
|
|
407
|
+
// - Progress indication
|
|
105
408
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
409
|
+
<GoalImageGallery
|
|
410
|
+
canEdit={true}
|
|
411
|
+
multiple={true}
|
|
412
|
+
maxFileSize={10 * 1024 * 1024} // 10MB
|
|
413
|
+
acceptedFormats={['image/jpeg', 'image/png', 'image/webp']}
|
|
414
|
+
// ... other props
|
|
415
|
+
/>
|
|
416
|
+
```
|
|
111
417
|
|
|
112
|
-
**Supported
|
|
113
|
-
**
|
|
418
|
+
**Supported Features:**
|
|
419
|
+
- **File Types**: JPEG, PNG, GIF, WebP, SVG
|
|
420
|
+
- **Size Limits**: Configurable (default: 25MB)
|
|
421
|
+
- **Batch Upload**: Upload multiple files simultaneously
|
|
422
|
+
- **Progress Tracking**: Visual progress indicators
|
|
423
|
+
- **Error Handling**: File validation and error reporting
|
|
114
424
|
|
|
115
425
|
### 2. Clipboard Paste
|
|
116
|
-
Users can paste images directly from the clipboard:
|
|
117
|
-
- Copy an image from any source (screenshot, browser, etc.)
|
|
118
|
-
- Press `Ctrl+V` anywhere in the gallery
|
|
119
|
-
- The image will be automatically processed and uploaded
|
|
120
426
|
|
|
121
|
-
|
|
427
|
+
Revolutionary clipboard image support:
|
|
428
|
+
|
|
429
|
+
```javascript
|
|
430
|
+
// Usage:
|
|
431
|
+
// 1. Copy image from any source (screenshot, browser, etc.)
|
|
432
|
+
// 2. Focus on the gallery
|
|
433
|
+
// 3. Press Ctrl+V (Cmd+V on Mac)
|
|
434
|
+
// 4. Image is automatically processed and uploaded
|
|
435
|
+
|
|
436
|
+
// Auto-enabled when canEdit={true}
|
|
437
|
+
<GoalImageGallery
|
|
438
|
+
canEdit={true}
|
|
439
|
+
// Clipboard paste is automatically available
|
|
440
|
+
/>
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Clipboard Features:**
|
|
444
|
+
- **Cross-Platform**: Works on Windows, Mac, and Linux
|
|
445
|
+
- **Source Agnostic**: Copy from any application
|
|
446
|
+
- **Auto-Processing**: Automatic image processing and optimization
|
|
447
|
+
- **Format Support**: All major image formats
|
|
448
|
+
- **Browser Support**: Modern browsers (Chrome, Firefox, Safari, Edge)
|
|
122
449
|
|
|
123
450
|
### 3. URL Loading
|
|
124
|
-
Users can load images from URLs:
|
|
125
|
-
- Enter a direct image URL
|
|
126
|
-
- The component validates the URL and loads the image
|
|
127
|
-
- Supports all standard image formats
|
|
128
451
|
|
|
129
|
-
|
|
452
|
+
Load images from external URLs:
|
|
130
453
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
454
|
+
```javascript
|
|
455
|
+
// Users can enter image URLs directly
|
|
456
|
+
// Component validates and loads the image
|
|
457
|
+
// Supports CORS-enabled images
|
|
458
|
+
|
|
459
|
+
const handleUrlUpload = async (imageUrl) => {
|
|
460
|
+
try {
|
|
461
|
+
const response = await fetch(imageUrl);
|
|
462
|
+
const blob = await response.blob();
|
|
463
|
+
|
|
464
|
+
// Process the image
|
|
465
|
+
const result = await processImageBlob(blob);
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
success: true,
|
|
469
|
+
data: result
|
|
470
|
+
};
|
|
471
|
+
} catch (error) {
|
|
472
|
+
return {
|
|
473
|
+
success: false,
|
|
474
|
+
message: 'Failed to load image from URL'
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### 4. Mobile Camera Integration
|
|
481
|
+
|
|
482
|
+
Enhanced mobile support:
|
|
483
|
+
|
|
484
|
+
```javascript
|
|
485
|
+
// Mobile-specific features:
|
|
486
|
+
// - Camera access
|
|
487
|
+
// - Photo library access
|
|
488
|
+
// - Touch gestures
|
|
489
|
+
// - Responsive design
|
|
490
|
+
|
|
491
|
+
<GoalImageGallery
|
|
492
|
+
canEdit={true}
|
|
493
|
+
// Mobile camera is automatically available on mobile devices
|
|
494
|
+
mobileOptimized={true}
|
|
495
|
+
touchGestures={true}
|
|
496
|
+
/>
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Keyboard Shortcuts & Navigation
|
|
500
|
+
|
|
501
|
+
### Image Viewer Shortcuts
|
|
502
|
+
|
|
503
|
+
| Shortcut | Action | Context |
|
|
504
|
+
|----------|---------|---------|
|
|
505
|
+
| `←` / `→` | Navigate between images | Image viewer open |
|
|
506
|
+
| `+` / `=` | Zoom in | Image viewer open |
|
|
507
|
+
| `-` | Zoom out | Image viewer open |
|
|
508
|
+
| `Escape` | Close image viewer | Image viewer open |
|
|
509
|
+
| `Space` | Toggle fullscreen | Image viewer open |
|
|
510
|
+
| `Enter` | Open selected image | Gallery focused |
|
|
136
511
|
|
|
137
512
|
### Gallery Navigation
|
|
138
|
-
- `Tab`: Navigate between interactive elements
|
|
139
|
-
- `Enter` / `Space`: Activate buttons
|
|
140
|
-
- `Escape`: Close dialogs
|
|
141
513
|
|
|
142
|
-
|
|
514
|
+
| Shortcut | Action | Context |
|
|
515
|
+
|----------|---------|---------|
|
|
516
|
+
| `Tab` | Navigate between elements | Gallery focused |
|
|
517
|
+
| `Shift + Tab` | Navigate backwards | Gallery focused |
|
|
518
|
+
| `Enter` / `Space` | Activate button/link | Element focused |
|
|
519
|
+
| `Ctrl + V` | Paste from clipboard | Editing enabled |
|
|
520
|
+
| `Delete` | Remove selected image | Image selected |
|
|
143
521
|
|
|
144
|
-
|
|
522
|
+
### Accessibility Features
|
|
523
|
+
|
|
524
|
+
```javascript
|
|
525
|
+
// Built-in accessibility features:
|
|
526
|
+
<GoalImageGallery
|
|
527
|
+
// ARIA labels are automatically generated
|
|
528
|
+
ariaLabel="Product image gallery"
|
|
529
|
+
ariaDescribedBy="gallery-instructions"
|
|
530
|
+
|
|
531
|
+
// Screen reader support
|
|
532
|
+
screenReaderOptimized={true}
|
|
533
|
+
|
|
534
|
+
// High contrast mode
|
|
535
|
+
highContrastMode={true}
|
|
536
|
+
|
|
537
|
+
// Keyboard navigation
|
|
538
|
+
keyboardNavigation={true}
|
|
539
|
+
|
|
540
|
+
// Focus management
|
|
541
|
+
manageFocus={true}
|
|
542
|
+
/>
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Custom Keyboard Shortcuts
|
|
546
|
+
|
|
547
|
+
```javascript
|
|
548
|
+
import { useKeyboardNavigation } from '@mgonza02/goal-image-gallery';
|
|
549
|
+
|
|
550
|
+
const CustomGallery = () => {
|
|
551
|
+
const { handleKeyPress } = useKeyboardNavigation({
|
|
552
|
+
onPreviousImage: () => console.log('Previous'),
|
|
553
|
+
onNextImage: () => console.log('Next'),
|
|
554
|
+
onCloseImage: () => console.log('Close'),
|
|
555
|
+
onZoomIn: () => console.log('Zoom in'),
|
|
556
|
+
onZoomOut: () => console.log('Zoom out'),
|
|
557
|
+
// Add custom shortcuts
|
|
558
|
+
customShortcuts: {
|
|
559
|
+
'd': () => downloadCurrentImage(),
|
|
560
|
+
'i': () => showImageInfo(),
|
|
561
|
+
'f': () => toggleFullscreen()
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
<div onKeyDown={handleKeyPress}>
|
|
567
|
+
<GoalImageGallery {...props} />
|
|
568
|
+
</div>
|
|
569
|
+
);
|
|
570
|
+
};
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
## API Integration & Data Flow
|
|
574
|
+
|
|
575
|
+
### API Handler Function
|
|
576
|
+
|
|
577
|
+
The `imageHandlerApi` function is the core of the upload system:
|
|
145
578
|
|
|
146
579
|
```javascript
|
|
147
580
|
const handleImageUpload = async (imageData) => {
|
|
148
581
|
try {
|
|
582
|
+
// imageData contains:
|
|
583
|
+
// - files: Array of File objects
|
|
584
|
+
// - metadata: Additional data (owner, company, etc.)
|
|
585
|
+
// - options: Upload options (compression, format, etc.)
|
|
586
|
+
|
|
149
587
|
const response = await api.uploadImages({
|
|
150
|
-
|
|
151
|
-
companyId:
|
|
152
|
-
entityId:
|
|
588
|
+
files: imageData.files,
|
|
589
|
+
companyId: imageData.metadata.companyId,
|
|
590
|
+
entityId: imageData.metadata.entityId,
|
|
591
|
+
entityType: imageData.metadata.entityType,
|
|
592
|
+
folder: imageData.options.folder,
|
|
593
|
+
compression: imageData.options.compression,
|
|
594
|
+
thumbnailSize: imageData.options.thumbnailSize
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Must return standardized response
|
|
598
|
+
return {
|
|
599
|
+
success: true,
|
|
600
|
+
data: response.data, // Array of uploaded image objects
|
|
601
|
+
message: 'Images uploaded successfully',
|
|
602
|
+
codes: response.data.map(img => img.code) // Optional: image codes
|
|
603
|
+
};
|
|
604
|
+
} catch (error) {
|
|
605
|
+
return {
|
|
606
|
+
success: false,
|
|
607
|
+
message: error.message || 'Upload failed',
|
|
608
|
+
error: error
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### API Response Format
|
|
615
|
+
|
|
616
|
+
Expected response format from your API:
|
|
617
|
+
|
|
618
|
+
```javascript
|
|
619
|
+
// Successful response
|
|
620
|
+
{
|
|
621
|
+
success: true,
|
|
622
|
+
data: [
|
|
623
|
+
{
|
|
624
|
+
id: 123,
|
|
625
|
+
code: 'img_abc123',
|
|
626
|
+
url: 'https://cdn.example.com/images/img_abc123.jpg',
|
|
627
|
+
thumbnailUrl: 'https://cdn.example.com/images/img_abc123_thumb.jpg',
|
|
628
|
+
filename: 'product-image.jpg',
|
|
629
|
+
size: 1024000,
|
|
630
|
+
width: 1920,
|
|
631
|
+
height: 1080,
|
|
632
|
+
mimeType: 'image/jpeg',
|
|
633
|
+
uploadedAt: '2023-01-01T00:00:00Z'
|
|
634
|
+
}
|
|
635
|
+
],
|
|
636
|
+
message: 'Upload successful'
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Error response
|
|
640
|
+
{
|
|
641
|
+
success: false,
|
|
642
|
+
message: 'File size too large',
|
|
643
|
+
error: {
|
|
644
|
+
code: 'FILE_TOO_LARGE',
|
|
645
|
+
details: 'Maximum file size is 25MB'
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Real-World API Integration Examples
|
|
651
|
+
|
|
652
|
+
#### 1. REST API Integration
|
|
653
|
+
|
|
654
|
+
```javascript
|
|
655
|
+
const restApiHandler = async (imageData) => {
|
|
656
|
+
const formData = new FormData();
|
|
657
|
+
|
|
658
|
+
// Add files
|
|
659
|
+
imageData.files.forEach((file, index) => {
|
|
660
|
+
formData.append(`files[${index}]`, file);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Add metadata
|
|
664
|
+
formData.append('companyId', imageData.metadata.companyId);
|
|
665
|
+
formData.append('entityId', imageData.metadata.entityId);
|
|
666
|
+
formData.append('entityType', imageData.metadata.entityType);
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
const response = await fetch('/api/images/upload', {
|
|
670
|
+
method: 'POST',
|
|
671
|
+
body: formData,
|
|
672
|
+
headers: {
|
|
673
|
+
'Authorization': `Bearer ${authToken}`,
|
|
674
|
+
// Don't set Content-Type, let browser set it with boundary
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const result = await response.json();
|
|
679
|
+
|
|
680
|
+
if (!response.ok) {
|
|
681
|
+
throw new Error(result.message || 'Upload failed');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
success: true,
|
|
686
|
+
data: result.images,
|
|
687
|
+
message: result.message
|
|
688
|
+
};
|
|
689
|
+
} catch (error) {
|
|
690
|
+
return {
|
|
691
|
+
success: false,
|
|
692
|
+
message: error.message
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
#### 2. GraphQL Integration
|
|
699
|
+
|
|
700
|
+
```javascript
|
|
701
|
+
import { useMutation } from '@apollo/client';
|
|
702
|
+
|
|
703
|
+
const UPLOAD_IMAGES = gql`
|
|
704
|
+
mutation UploadImages($files: [Upload!]!, $input: ImageUploadInput!) {
|
|
705
|
+
uploadImages(files: $files, input: $input) {
|
|
706
|
+
success
|
|
707
|
+
message
|
|
708
|
+
images {
|
|
709
|
+
id
|
|
710
|
+
code
|
|
711
|
+
url
|
|
712
|
+
thumbnailUrl
|
|
713
|
+
filename
|
|
714
|
+
size
|
|
715
|
+
width
|
|
716
|
+
height
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
`;
|
|
721
|
+
|
|
722
|
+
const GraphQLImageGallery = ({ ownerEntity, ...props }) => {
|
|
723
|
+
const [uploadImages] = useMutation(UPLOAD_IMAGES);
|
|
724
|
+
|
|
725
|
+
const handleImageUpload = async (imageData) => {
|
|
726
|
+
try {
|
|
727
|
+
const { data } = await uploadImages({
|
|
728
|
+
variables: {
|
|
729
|
+
files: imageData.files,
|
|
730
|
+
input: {
|
|
731
|
+
companyId: imageData.metadata.companyId,
|
|
732
|
+
entityId: imageData.metadata.entityId,
|
|
733
|
+
entityType: imageData.metadata.entityType
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
success: data.uploadImages.success,
|
|
740
|
+
data: data.uploadImages.images,
|
|
741
|
+
message: data.uploadImages.message
|
|
742
|
+
};
|
|
743
|
+
} catch (error) {
|
|
744
|
+
return {
|
|
745
|
+
success: false,
|
|
746
|
+
message: error.message
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
return (
|
|
752
|
+
<GoalImageGallery
|
|
753
|
+
{...props}
|
|
754
|
+
ownerEntity={ownerEntity}
|
|
755
|
+
imageHandlerApi={handleImageUpload}
|
|
756
|
+
/>
|
|
757
|
+
);
|
|
758
|
+
};
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
#### 3. Firebase Storage Integration
|
|
762
|
+
|
|
763
|
+
```javascript
|
|
764
|
+
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
|
765
|
+
import { storage } from './firebaseConfig';
|
|
766
|
+
|
|
767
|
+
const firebaseHandler = async (imageData) => {
|
|
768
|
+
try {
|
|
769
|
+
const uploadPromises = imageData.files.map(async (file) => {
|
|
770
|
+
const storageRef = ref(storage, `images/${Date.now()}_${file.name}`);
|
|
771
|
+
const snapshot = await uploadBytes(storageRef, file);
|
|
772
|
+
const downloadURL = await getDownloadURL(snapshot.ref);
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
id: snapshot.ref.fullPath,
|
|
776
|
+
code: snapshot.ref.name,
|
|
777
|
+
url: downloadURL,
|
|
778
|
+
filename: file.name,
|
|
779
|
+
size: file.size,
|
|
780
|
+
mimeType: file.type
|
|
781
|
+
};
|
|
153
782
|
});
|
|
154
783
|
|
|
784
|
+
const results = await Promise.all(uploadPromises);
|
|
785
|
+
|
|
155
786
|
return {
|
|
156
787
|
success: true,
|
|
157
|
-
data:
|
|
158
|
-
message: 'Images uploaded
|
|
788
|
+
data: results,
|
|
789
|
+
message: 'Images uploaded to Firebase'
|
|
159
790
|
};
|
|
160
791
|
} catch (error) {
|
|
161
792
|
return {
|
|
@@ -166,6 +797,140 @@ const handleImageUpload = async (imageData) => {
|
|
|
166
797
|
};
|
|
167
798
|
```
|
|
168
799
|
|
|
800
|
+
### Error Handling Best Practices
|
|
801
|
+
|
|
802
|
+
```javascript
|
|
803
|
+
const robustApiHandler = async (imageData) => {
|
|
804
|
+
try {
|
|
805
|
+
// Validate files before upload
|
|
806
|
+
const validFiles = imageData.files.filter(file => {
|
|
807
|
+
if (file.size > 25 * 1024 * 1024) {
|
|
808
|
+
console.warn(`File ${file.name} is too large`);
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
if (!file.type.startsWith('image/')) {
|
|
812
|
+
console.warn(`File ${file.name} is not an image`);
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
return true;
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
if (validFiles.length === 0) {
|
|
819
|
+
return {
|
|
820
|
+
success: false,
|
|
821
|
+
message: 'No valid files to upload'
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Attempt upload with retry logic
|
|
826
|
+
let attempts = 0;
|
|
827
|
+
const maxAttempts = 3;
|
|
828
|
+
|
|
829
|
+
while (attempts < maxAttempts) {
|
|
830
|
+
try {
|
|
831
|
+
const response = await uploadFiles(validFiles);
|
|
832
|
+
return {
|
|
833
|
+
success: true,
|
|
834
|
+
data: response.data,
|
|
835
|
+
message: 'Upload successful'
|
|
836
|
+
};
|
|
837
|
+
} catch (error) {
|
|
838
|
+
attempts++;
|
|
839
|
+
if (attempts >= maxAttempts) {
|
|
840
|
+
throw error;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Wait before retry
|
|
844
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} catch (error) {
|
|
848
|
+
// Log error for debugging
|
|
849
|
+
console.error('Image upload failed:', error);
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
success: false,
|
|
853
|
+
message: error.message || 'Upload failed. Please try again.',
|
|
854
|
+
error: error
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
```
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
## Customization Options
|
|
865
|
+
|
|
866
|
+
### Slot System
|
|
867
|
+
|
|
868
|
+
The GoalImageGallery supports a flexible slot system for customization:
|
|
869
|
+
|
|
870
|
+
#### Custom Image Selector
|
|
871
|
+
|
|
872
|
+
You can provide your own image selector component:
|
|
873
|
+
|
|
874
|
+
```javascript
|
|
875
|
+
// Custom selector component
|
|
876
|
+
const CustomImageSelector = ({
|
|
877
|
+
afterUpload,
|
|
878
|
+
activate,
|
|
879
|
+
onCancel,
|
|
880
|
+
multiple,
|
|
881
|
+
title,
|
|
882
|
+
uploadMessage,
|
|
883
|
+
maxFileSize,
|
|
884
|
+
acceptedFormats,
|
|
885
|
+
customStyles
|
|
886
|
+
}) => {
|
|
887
|
+
const handleFileSelect = async (files) => {
|
|
888
|
+
// Your custom upload logic
|
|
889
|
+
const result = await uploadFiles(files);
|
|
890
|
+
afterUpload(result);
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
return (
|
|
894
|
+
<div style={{ ...customStyles, padding: '20px' }}>
|
|
895
|
+
<h3>{title}</h3>
|
|
896
|
+
<p>{uploadMessage}</p>
|
|
897
|
+
<input
|
|
898
|
+
type="file"
|
|
899
|
+
multiple={multiple}
|
|
900
|
+
accept={acceptedFormats.join(',')}
|
|
901
|
+
onChange={(e) => handleFileSelect(e.target.files)}
|
|
902
|
+
/>
|
|
903
|
+
<button onClick={onCancel}>Cancel</button>
|
|
904
|
+
</div>
|
|
905
|
+
);
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// Usage
|
|
909
|
+
<GoalImageGallery
|
|
910
|
+
// ... other props
|
|
911
|
+
slot={{
|
|
912
|
+
selector: CustomImageSelector
|
|
913
|
+
}}
|
|
914
|
+
slotProps={{
|
|
915
|
+
selector: {
|
|
916
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
917
|
+
acceptedFormats: ['image/jpeg', 'image/png'],
|
|
918
|
+
customStyles: {
|
|
919
|
+
border: '2px dashed #007bff',
|
|
920
|
+
borderRadius: '8px',
|
|
921
|
+
backgroundColor: '#f8f9fa'
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}}
|
|
925
|
+
/>
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### Available Slots
|
|
929
|
+
|
|
930
|
+
| Slot | Description | Props Passed |
|
|
931
|
+
|------|-------------|--------------|
|
|
932
|
+
| `selector` | Custom image selector/uploader component | `afterUpload`, `activate`, `onCancel`, `multiple`, `title`, `uploadMessage`, and custom props from `slotProps.selector` |
|
|
933
|
+
|
|
169
934
|
## Styling and Theming
|
|
170
935
|
|
|
171
936
|
The component uses Material-UI's theming system and can be customized:
|