@qwickapps/react-framework 1.5.12 → 1.6.0

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.
Files changed (65) hide show
  1. package/README.md +23 -0
  2. package/dist/components/blocks/ImageGallery.d.ts +30 -0
  3. package/dist/components/blocks/ImageGallery.d.ts.map +1 -0
  4. package/dist/components/blocks/OptionSelector.d.ts +45 -0
  5. package/dist/components/blocks/OptionSelector.d.ts.map +1 -0
  6. package/dist/components/blocks/index.d.ts +4 -0
  7. package/dist/components/blocks/index.d.ts.map +1 -1
  8. package/dist/components/forms/Captcha.d.ts +33 -28
  9. package/dist/components/forms/Captcha.d.ts.map +1 -1
  10. package/dist/components/forms/FormCheckbox.d.ts +15 -12
  11. package/dist/components/forms/FormCheckbox.d.ts.map +1 -1
  12. package/dist/components/forms/FormField.d.ts +20 -23
  13. package/dist/components/forms/FormField.d.ts.map +1 -1
  14. package/dist/components/forms/FormSelect.d.ts +16 -15
  15. package/dist/components/forms/FormSelect.d.ts.map +1 -1
  16. package/dist/hooks/useBaseProps.d.ts +27 -1172
  17. package/dist/hooks/useBaseProps.d.ts.map +1 -1
  18. package/dist/index.esm.js +1674 -554
  19. package/dist/index.js +1676 -552
  20. package/dist/palettes/manifest.json +19 -19
  21. package/dist/schemas/CaptchaSchema.d.ts +16 -0
  22. package/dist/schemas/CaptchaSchema.d.ts.map +1 -0
  23. package/dist/schemas/FormCheckboxSchema.d.ts +16 -0
  24. package/dist/schemas/FormCheckboxSchema.d.ts.map +1 -0
  25. package/dist/schemas/FormFieldSchema.d.ts +23 -0
  26. package/dist/schemas/FormFieldSchema.d.ts.map +1 -0
  27. package/dist/schemas/FormSelectSchema.d.ts +20 -0
  28. package/dist/schemas/FormSelectSchema.d.ts.map +1 -0
  29. package/dist/schemas/ImageGallerySchema.d.ts +27 -0
  30. package/dist/schemas/ImageGallerySchema.d.ts.map +1 -0
  31. package/dist/schemas/OptionSelectorSchema.d.ts +34 -0
  32. package/dist/schemas/OptionSelectorSchema.d.ts.map +1 -0
  33. package/dist/schemas/index.d.ts +6 -0
  34. package/dist/schemas/index.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/components/blocks/Article.tsx +1 -1
  37. package/src/components/blocks/ImageGallery.tsx +464 -0
  38. package/src/components/blocks/OptionSelector.tsx +459 -0
  39. package/src/components/blocks/index.ts +4 -0
  40. package/src/components/forms/Captcha.tsx +57 -63
  41. package/src/components/forms/FormCheckbox.tsx +35 -43
  42. package/src/components/forms/FormField.tsx +50 -66
  43. package/src/components/forms/FormSelect.tsx +41 -49
  44. package/src/hooks/useBaseProps.ts +34 -1
  45. package/src/schemas/CaptchaSchema.ts +65 -0
  46. package/src/schemas/FormCheckboxSchema.ts +65 -0
  47. package/src/schemas/FormFieldSchema.ts +140 -0
  48. package/src/schemas/FormSelectSchema.ts +108 -0
  49. package/src/schemas/ImageGallerySchema.ts +148 -0
  50. package/src/schemas/OptionSelectorSchema.ts +216 -0
  51. package/src/schemas/index.ts +6 -0
  52. package/src/stories/ImageGallery.stories.tsx +497 -0
  53. package/src/stories/OptionSelector.stories.tsx +506 -0
  54. /package/dist/palettes/{palette-autumn.1.5.12.css → palette-autumn.1.6.0.css} +0 -0
  55. /package/dist/palettes/{palette-autumn.1.5.12.min.css → palette-autumn.1.6.0.min.css} +0 -0
  56. /package/dist/palettes/{palette-cosmic.1.5.12.css → palette-cosmic.1.6.0.css} +0 -0
  57. /package/dist/palettes/{palette-cosmic.1.5.12.min.css → palette-cosmic.1.6.0.min.css} +0 -0
  58. /package/dist/palettes/{palette-default.1.5.12.css → palette-default.1.6.0.css} +0 -0
  59. /package/dist/palettes/{palette-default.1.5.12.min.css → palette-default.1.6.0.min.css} +0 -0
  60. /package/dist/palettes/{palette-ocean.1.5.12.css → palette-ocean.1.6.0.css} +0 -0
  61. /package/dist/palettes/{palette-ocean.1.5.12.min.css → palette-ocean.1.6.0.min.css} +0 -0
  62. /package/dist/palettes/{palette-spring.1.5.12.css → palette-spring.1.6.0.css} +0 -0
  63. /package/dist/palettes/{palette-spring.1.5.12.min.css → palette-spring.1.6.0.min.css} +0 -0
  64. /package/dist/palettes/{palette-winter.1.5.12.css → palette-winter.1.6.0.css} +0 -0
  65. /package/dist/palettes/{palette-winter.1.5.12.min.css → palette-winter.1.6.0.min.css} +0 -0
@@ -0,0 +1,464 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ImageGallery - Comprehensive product image gallery with multiple view variants
5
+ *
6
+ * Features:
7
+ * - Multiple view variants (thumbnails, carousel, grid)
8
+ * - Configurable thumbnail position (left, bottom, right)
9
+ * - Image zoom modal
10
+ * - Video support
11
+ * - Responsive design
12
+ * - Accessibility support
13
+ * - Full serialization support via factory pattern
14
+ * - Theme-compliant styling with CSS custom properties
15
+ *
16
+ * @example
17
+ * <ImageGallery
18
+ * images={[
19
+ * { url: '/image1.jpg', alt: 'Product view 1' },
20
+ * { url: '/image2.jpg', alt: 'Product view 2' }
21
+ * ]}
22
+ * productName="Premium Cotton T-Shirt"
23
+ * variant="thumbnails"
24
+ * thumbnailPosition="left"
25
+ * showZoom={true}
26
+ * />
27
+ *
28
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
29
+ */
30
+
31
+ import React, { useState, useCallback } from 'react';
32
+ import { Box, IconButton, Modal, Grid, Skeleton } from '@mui/material';
33
+ import CloseIcon from '@mui/icons-material/Close';
34
+ import ZoomInIcon from '@mui/icons-material/ZoomIn';
35
+ import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
36
+ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
37
+ import { createSerializableView, SerializableComponent } from '../shared/createSerializableView';
38
+ import { ViewProps } from '../shared/viewProps';
39
+
40
+ export interface GalleryImage {
41
+ url: string;
42
+ alt: string;
43
+ thumbnail?: string;
44
+ }
45
+
46
+ export interface ImageGalleryProps extends ViewProps {
47
+ /** Array of product images */
48
+ images: GalleryImage[];
49
+
50
+ /** Product name for accessibility */
51
+ productName: string;
52
+
53
+ /** Gallery display variant */
54
+ variant?: 'thumbnails' | 'carousel' | 'grid';
55
+
56
+ /** Position of thumbnails (only for thumbnails variant) */
57
+ thumbnailPosition?: 'left' | 'bottom' | 'right';
58
+
59
+ /** Aspect ratio for main image */
60
+ aspectRatio?: string;
61
+
62
+ /** Enable zoom functionality */
63
+ showZoom?: boolean;
64
+
65
+ /** Maximum number of images to display */
66
+ maxImages?: number;
67
+
68
+ /** Data source for dynamic loading */
69
+ dataSource?: string;
70
+
71
+ /** Data binding configuration */
72
+ bindingOptions?: Record<string, unknown>;
73
+ }
74
+
75
+ // View component - handles the actual rendering
76
+ function ImageGalleryView({
77
+ images = [],
78
+ productName,
79
+ variant = 'thumbnails',
80
+ thumbnailPosition = 'left',
81
+ aspectRatio = '1',
82
+ showZoom = true,
83
+ maxImages,
84
+ dataSource,
85
+ bindingOptions,
86
+ ...restProps
87
+ }: ImageGalleryProps) {
88
+ const [selectedIndex, setSelectedIndex] = useState(0);
89
+ const [zoomOpen, setZoomOpen] = useState(false);
90
+
91
+ // Limit images if maxImages is specified
92
+ const displayImages = maxImages ? images.slice(0, maxImages) : images;
93
+
94
+ const handlePrevious = useCallback(() => {
95
+ setSelectedIndex((prev) => (prev === 0 ? displayImages.length - 1 : prev - 1));
96
+ }, [displayImages.length]);
97
+
98
+ const handleNext = useCallback(() => {
99
+ setSelectedIndex((prev) => (prev === displayImages.length - 1 ? 0 : prev + 1));
100
+ }, [displayImages.length]);
101
+
102
+ const handleThumbnailClick = useCallback((index: number) => {
103
+ setSelectedIndex(index);
104
+ }, []);
105
+
106
+ const handleZoomOpen = useCallback(() => {
107
+ if (showZoom) {
108
+ setZoomOpen(true);
109
+ }
110
+ }, [showZoom]);
111
+
112
+ const handleZoomClose = useCallback(() => {
113
+ setZoomOpen(false);
114
+ }, []);
115
+
116
+ // Handle empty images
117
+ if (!displayImages || displayImages.length === 0) {
118
+ return (
119
+ <Box
120
+ {...restProps}
121
+ sx={{
122
+ backgroundColor: 'var(--theme-surface-variant)',
123
+ borderRadius: 'var(--theme-border-radius)',
124
+ display: 'flex',
125
+ alignItems: 'center',
126
+ justifyContent: 'center',
127
+ aspectRatio,
128
+ minHeight: 400,
129
+ }}
130
+ >
131
+ <Skeleton
132
+ variant="rectangular"
133
+ width="100%"
134
+ height="100%"
135
+ sx={{
136
+ borderRadius: 'var(--theme-border-radius)',
137
+ }}
138
+ />
139
+ </Box>
140
+ );
141
+ }
142
+
143
+ const currentImage = displayImages[selectedIndex];
144
+
145
+ // Render thumbnails
146
+ const renderThumbnails = () => (
147
+ <Box
148
+ sx={{
149
+ display: 'flex',
150
+ flexDirection: thumbnailPosition === 'left' || thumbnailPosition === 'right' ? 'column' : 'row',
151
+ gap: 1,
152
+ overflow: 'auto',
153
+ maxHeight: thumbnailPosition === 'left' || thumbnailPosition === 'right' ? 500 : 'auto',
154
+ maxWidth: thumbnailPosition === 'bottom' ? '100%' : 'auto',
155
+ }}
156
+ >
157
+ {displayImages.map((image, index) => (
158
+ <Box
159
+ key={index}
160
+ onClick={() => handleThumbnailClick(index)}
161
+ sx={{
162
+ width: 80,
163
+ height: 80,
164
+ flexShrink: 0,
165
+ cursor: 'pointer',
166
+ border: '2px solid',
167
+ borderColor: index === selectedIndex ? 'var(--theme-primary)' : 'var(--theme-border-main)',
168
+ borderRadius: 'var(--theme-border-radius-small)',
169
+ overflow: 'hidden',
170
+ transition: 'all 0.2s ease-in-out',
171
+ '&:hover': {
172
+ borderColor: 'var(--theme-border-emphasis)',
173
+ },
174
+ }}
175
+ >
176
+ <img
177
+ src={image.thumbnail || image.url}
178
+ alt={`${productName} thumbnail ${index + 1}`}
179
+ style={{
180
+ width: '100%',
181
+ height: '100%',
182
+ objectFit: 'cover',
183
+ }}
184
+ />
185
+ </Box>
186
+ ))}
187
+ </Box>
188
+ );
189
+
190
+ // Render main image container
191
+ const renderMainImage = () => (
192
+ <Box
193
+ sx={{
194
+ position: 'relative',
195
+ width: '100%',
196
+ backgroundColor: 'var(--theme-surface)',
197
+ borderRadius: 'var(--theme-border-radius)',
198
+ border: '1px solid var(--theme-border-main)',
199
+ overflow: 'hidden',
200
+ aspectRatio,
201
+ }}
202
+ >
203
+ <img
204
+ src={currentImage.url}
205
+ alt={currentImage.alt || `${productName} - Image ${selectedIndex + 1}`}
206
+ style={{
207
+ width: '100%',
208
+ height: '100%',
209
+ objectFit: 'contain',
210
+ display: 'block',
211
+ }}
212
+ />
213
+
214
+ {/* Zoom button */}
215
+ {showZoom && (
216
+ <IconButton
217
+ onClick={handleZoomOpen}
218
+ sx={{
219
+ position: 'absolute',
220
+ top: 8,
221
+ right: 8,
222
+ backgroundColor: 'var(--theme-surface)',
223
+ color: 'var(--theme-text-primary)',
224
+ '&:hover': {
225
+ backgroundColor: 'var(--theme-surface-variant)',
226
+ },
227
+ }}
228
+ aria-label="Zoom image"
229
+ >
230
+ <ZoomInIcon />
231
+ </IconButton>
232
+ )}
233
+
234
+ {/* Navigation arrows for carousel */}
235
+ {variant === 'carousel' && displayImages.length > 1 && (
236
+ <>
237
+ <IconButton
238
+ onClick={handlePrevious}
239
+ sx={{
240
+ position: 'absolute',
241
+ left: 8,
242
+ top: '50%',
243
+ transform: 'translateY(-50%)',
244
+ backgroundColor: 'var(--theme-surface)',
245
+ color: 'var(--theme-text-primary)',
246
+ '&:hover': {
247
+ backgroundColor: 'var(--theme-surface-variant)',
248
+ },
249
+ }}
250
+ aria-label="Previous image"
251
+ >
252
+ <ChevronLeftIcon />
253
+ </IconButton>
254
+ <IconButton
255
+ onClick={handleNext}
256
+ sx={{
257
+ position: 'absolute',
258
+ right: 8,
259
+ top: '50%',
260
+ transform: 'translateY(-50%)',
261
+ backgroundColor: 'var(--theme-surface)',
262
+ color: 'var(--theme-text-primary)',
263
+ '&:hover': {
264
+ backgroundColor: 'var(--theme-surface-variant)',
265
+ },
266
+ }}
267
+ aria-label="Next image"
268
+ >
269
+ <ChevronRightIcon />
270
+ </IconButton>
271
+ </>
272
+ )}
273
+ </Box>
274
+ );
275
+
276
+ // Render zoom modal
277
+ const renderZoomModal = () => (
278
+ <Modal
279
+ open={zoomOpen}
280
+ onClose={handleZoomClose}
281
+ sx={{
282
+ display: 'flex',
283
+ alignItems: 'center',
284
+ justifyContent: 'center',
285
+ backgroundColor: 'rgba(0, 0, 0, 0.9)',
286
+ }}
287
+ >
288
+ <Box
289
+ sx={{
290
+ position: 'relative',
291
+ maxWidth: '90vw',
292
+ maxHeight: '90vh',
293
+ outline: 'none',
294
+ }}
295
+ >
296
+ <IconButton
297
+ onClick={handleZoomClose}
298
+ sx={{
299
+ position: 'absolute',
300
+ top: -40,
301
+ right: 0,
302
+ color: 'white',
303
+ '&:hover': {
304
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
305
+ },
306
+ }}
307
+ aria-label="Close zoom"
308
+ >
309
+ <CloseIcon />
310
+ </IconButton>
311
+ <img
312
+ src={currentImage.url}
313
+ alt={currentImage.alt || `${productName} - Zoomed view`}
314
+ style={{
315
+ maxWidth: '100%',
316
+ maxHeight: '90vh',
317
+ objectFit: 'contain',
318
+ }}
319
+ />
320
+ </Box>
321
+ </Modal>
322
+ );
323
+
324
+ // Render thumbnails variant
325
+ if (variant === 'thumbnails') {
326
+ return (
327
+ <Box {...restProps}>
328
+ <Box
329
+ sx={{
330
+ display: 'flex',
331
+ flexDirection: {
332
+ xs: 'column',
333
+ sm: thumbnailPosition === 'bottom' ? 'column' : 'row',
334
+ },
335
+ gap: 2,
336
+ }}
337
+ >
338
+ {/* Thumbnails on left */}
339
+ {thumbnailPosition === 'left' && (
340
+ <Box sx={{ order: { xs: 2, sm: 1 } }}>
341
+ {renderThumbnails()}
342
+ </Box>
343
+ )}
344
+
345
+ {/* Main image */}
346
+ <Box sx={{ flex: 1, order: { xs: 1, sm: thumbnailPosition === 'left' ? 2 : 1 } }}>
347
+ {renderMainImage()}
348
+ </Box>
349
+
350
+ {/* Thumbnails on right */}
351
+ {thumbnailPosition === 'right' && (
352
+ <Box sx={{ order: { xs: 2, sm: 2 } }}>
353
+ {renderThumbnails()}
354
+ </Box>
355
+ )}
356
+
357
+ {/* Thumbnails on bottom */}
358
+ {thumbnailPosition === 'bottom' && (
359
+ <Box sx={{ order: 2 }}>
360
+ {renderThumbnails()}
361
+ </Box>
362
+ )}
363
+ </Box>
364
+ {renderZoomModal()}
365
+ </Box>
366
+ );
367
+ }
368
+
369
+ // Render carousel variant
370
+ if (variant === 'carousel') {
371
+ return (
372
+ <Box {...restProps}>
373
+ {renderMainImage()}
374
+ {/* Dot indicators */}
375
+ {displayImages.length > 1 && (
376
+ <Box
377
+ sx={{
378
+ display: 'flex',
379
+ justifyContent: 'center',
380
+ gap: 1,
381
+ mt: 2,
382
+ }}
383
+ >
384
+ {displayImages.map((_, index) => (
385
+ <Box
386
+ key={index}
387
+ onClick={() => handleThumbnailClick(index)}
388
+ sx={{
389
+ width: 10,
390
+ height: 10,
391
+ borderRadius: '50%',
392
+ backgroundColor: index === selectedIndex ? 'var(--theme-primary)' : 'var(--theme-border-main)',
393
+ cursor: 'pointer',
394
+ transition: 'all 0.2s ease-in-out',
395
+ '&:hover': {
396
+ backgroundColor: index === selectedIndex ? 'var(--theme-primary)' : 'var(--theme-border-emphasis)',
397
+ },
398
+ }}
399
+ />
400
+ ))}
401
+ </Box>
402
+ )}
403
+ {renderZoomModal()}
404
+ </Box>
405
+ );
406
+ }
407
+
408
+ // Render grid variant
409
+ if (variant === 'grid') {
410
+ return (
411
+ <Box {...restProps}>
412
+ <Grid container spacing={2}>
413
+ {displayImages.map((image, index) => (
414
+ <Grid item xs={6} sm={4} md={3} key={index}>
415
+ <Box
416
+ onClick={() => {
417
+ setSelectedIndex(index);
418
+ handleZoomOpen();
419
+ }}
420
+ sx={{
421
+ width: '100%',
422
+ aspectRatio: '1',
423
+ borderRadius: 'var(--theme-border-radius)',
424
+ border: '1px solid var(--theme-border-main)',
425
+ overflow: 'hidden',
426
+ cursor: showZoom ? 'pointer' : 'default',
427
+ transition: 'all 0.2s ease-in-out',
428
+ '&:hover': {
429
+ borderColor: 'var(--theme-border-emphasis)',
430
+ boxShadow: 'var(--theme-elevation-1)',
431
+ },
432
+ }}
433
+ >
434
+ <img
435
+ src={image.url}
436
+ alt={image.alt || `${productName} - Image ${index + 1}`}
437
+ style={{
438
+ width: '100%',
439
+ height: '100%',
440
+ objectFit: 'cover',
441
+ }}
442
+ />
443
+ </Box>
444
+ </Grid>
445
+ ))}
446
+ </Grid>
447
+ {renderZoomModal()}
448
+ </Box>
449
+ );
450
+ }
451
+
452
+ return null;
453
+ }
454
+
455
+ // Create the serializable ImageGallery component using the factory
456
+ export const ImageGallery: SerializableComponent<ImageGalleryProps> =
457
+ createSerializableView<ImageGalleryProps>({
458
+ tagName: 'ImageGallery',
459
+ version: '1.0.0',
460
+ role: 'view',
461
+ View: ImageGalleryView,
462
+ });
463
+
464
+ export default ImageGallery;