@qwickapps/react-framework 1.5.11 → 1.5.13

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 (41) 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/index.css +1 -1
  9. package/dist/index.esm.css +1 -1
  10. package/dist/index.esm.js +1192 -265
  11. package/dist/index.js +1194 -263
  12. package/dist/palettes/manifest.json +19 -19
  13. package/dist/schemas/ImageGallerySchema.d.ts +27 -0
  14. package/dist/schemas/ImageGallerySchema.d.ts.map +1 -0
  15. package/dist/schemas/OptionSelectorSchema.d.ts +34 -0
  16. package/dist/schemas/OptionSelectorSchema.d.ts.map +1 -0
  17. package/dist/schemas/index.d.ts +2 -0
  18. package/dist/schemas/index.d.ts.map +1 -1
  19. package/package.json +1 -1
  20. package/src/components/QwickApp.css +8 -0
  21. package/src/components/blocks/Article.tsx +1 -1
  22. package/src/components/blocks/ImageGallery.tsx +464 -0
  23. package/src/components/blocks/OptionSelector.tsx +459 -0
  24. package/src/components/blocks/index.ts +4 -0
  25. package/src/schemas/ImageGallerySchema.ts +148 -0
  26. package/src/schemas/OptionSelectorSchema.ts +216 -0
  27. package/src/schemas/index.ts +2 -0
  28. package/src/stories/ImageGallery.stories.tsx +497 -0
  29. package/src/stories/OptionSelector.stories.tsx +506 -0
  30. /package/dist/palettes/{palette-autumn.1.5.11.css → palette-autumn.1.5.13.css} +0 -0
  31. /package/dist/palettes/{palette-autumn.1.5.11.min.css → palette-autumn.1.5.13.min.css} +0 -0
  32. /package/dist/palettes/{palette-cosmic.1.5.11.css → palette-cosmic.1.5.13.css} +0 -0
  33. /package/dist/palettes/{palette-cosmic.1.5.11.min.css → palette-cosmic.1.5.13.min.css} +0 -0
  34. /package/dist/palettes/{palette-default.1.5.11.css → palette-default.1.5.13.css} +0 -0
  35. /package/dist/palettes/{palette-default.1.5.11.min.css → palette-default.1.5.13.min.css} +0 -0
  36. /package/dist/palettes/{palette-ocean.1.5.11.css → palette-ocean.1.5.13.css} +0 -0
  37. /package/dist/palettes/{palette-ocean.1.5.11.min.css → palette-ocean.1.5.13.min.css} +0 -0
  38. /package/dist/palettes/{palette-spring.1.5.11.css → palette-spring.1.5.13.css} +0 -0
  39. /package/dist/palettes/{palette-spring.1.5.11.min.css → palette-spring.1.5.13.min.css} +0 -0
  40. /package/dist/palettes/{palette-winter.1.5.11.css → palette-winter.1.5.13.css} +0 -0
  41. /package/dist/palettes/{palette-winter.1.5.11.min.css → palette-winter.1.5.13.min.css} +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./manifest.schema.json",
3
- "version": "1.5.11",
3
+ "version": "1.5.13",
4
4
  "palettes": [
5
5
  {
6
6
  "id": "default",
@@ -8,11 +8,11 @@
8
8
  "description": "Classic blue and neutral color scheme - the original QwickApps palette",
9
9
  "author": "QwickApps",
10
10
  "license": "PolyForm-Shield-1.0.0",
11
- "version": "1.5.11",
12
- "file": "palette-default.1.5.11.css",
11
+ "version": "1.5.13",
12
+ "file": "palette-default.1.5.13.css",
13
13
  "primaryColor": "#007bff",
14
14
  "inlined": true,
15
- "fileMinified": "palette-default.1.5.11.min.css",
15
+ "fileMinified": "palette-default.1.5.13.min.css",
16
16
  "fileLatest": "palette-default.latest.css",
17
17
  "fileLatestMinified": "palette-default.latest.min.css"
18
18
  },
@@ -22,11 +22,11 @@
22
22
  "description": "Warm oranges, golden yellows, and earthy browns - inspired by fall foliage",
23
23
  "author": "QwickApps",
24
24
  "license": "PolyForm-Shield-1.0.0",
25
- "version": "1.5.11",
26
- "file": "palette-autumn.1.5.11.css",
25
+ "version": "1.5.13",
26
+ "file": "palette-autumn.1.5.13.css",
27
27
  "primaryColor": "#ea580c",
28
28
  "inlined": false,
29
- "fileMinified": "palette-autumn.1.5.11.min.css",
29
+ "fileMinified": "palette-autumn.1.5.13.min.css",
30
30
  "fileLatest": "palette-autumn.latest.css",
31
31
  "fileLatestMinified": "palette-autumn.latest.min.css"
32
32
  },
@@ -36,11 +36,11 @@
36
36
  "description": "Modern purple gradient for creative and tech brands - inspired by cosmic nebulae",
37
37
  "author": "QwickApps",
38
38
  "license": "PolyForm-Shield-1.0.0",
39
- "version": "1.5.11",
40
- "file": "palette-cosmic.1.5.11.css",
39
+ "version": "1.5.13",
40
+ "file": "palette-cosmic.1.5.13.css",
41
41
  "primaryColor": "#8b5cf6",
42
42
  "inlined": false,
43
- "fileMinified": "palette-cosmic.1.5.11.min.css",
43
+ "fileMinified": "palette-cosmic.1.5.13.min.css",
44
44
  "fileLatest": "palette-cosmic.latest.css",
45
45
  "fileLatestMinified": "palette-cosmic.latest.min.css"
46
46
  },
@@ -50,11 +50,11 @@
50
50
  "description": "Deep blues, aqua teals, and seafoam greens - inspired by ocean depths",
51
51
  "author": "QwickApps",
52
52
  "license": "PolyForm-Shield-1.0.0",
53
- "version": "1.5.11",
54
- "file": "palette-ocean.1.5.11.css",
53
+ "version": "1.5.13",
54
+ "file": "palette-ocean.1.5.13.css",
55
55
  "primaryColor": "#0891b2",
56
56
  "inlined": false,
57
- "fileMinified": "palette-ocean.1.5.11.min.css",
57
+ "fileMinified": "palette-ocean.1.5.13.min.css",
58
58
  "fileLatest": "palette-ocean.latest.css",
59
59
  "fileLatestMinified": "palette-ocean.latest.min.css"
60
60
  },
@@ -64,11 +64,11 @@
64
64
  "description": "Fresh greens, soft pinks, and bright yellows - inspired by spring blooms",
65
65
  "author": "QwickApps",
66
66
  "license": "PolyForm-Shield-1.0.0",
67
- "version": "1.5.11",
68
- "file": "palette-spring.1.5.11.css",
67
+ "version": "1.5.13",
68
+ "file": "palette-spring.1.5.13.css",
69
69
  "primaryColor": "#16a34a",
70
70
  "inlined": false,
71
- "fileMinified": "palette-spring.1.5.11.min.css",
71
+ "fileMinified": "palette-spring.1.5.13.min.css",
72
72
  "fileLatest": "palette-spring.latest.css",
73
73
  "fileLatestMinified": "palette-spring.latest.min.css"
74
74
  },
@@ -78,11 +78,11 @@
78
78
  "description": "Cool blues, icy whites, and frosty grays - inspired by winter landscapes",
79
79
  "author": "QwickApps",
80
80
  "license": "PolyForm-Shield-1.0.0",
81
- "version": "1.5.11",
82
- "file": "palette-winter.1.5.11.css",
81
+ "version": "1.5.13",
82
+ "file": "palette-winter.1.5.13.css",
83
83
  "primaryColor": "#0077be",
84
84
  "inlined": false,
85
- "fileMinified": "palette-winter.1.5.11.min.css",
85
+ "fileMinified": "palette-winter.1.5.13.min.css",
86
86
  "fileLatest": "palette-winter.latest.css",
87
87
  "fileLatestMinified": "palette-winter.latest.min.css"
88
88
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Schema for ImageGallery component - Image gallery with multiple view variants
3
+ *
4
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
5
+ */
6
+ import 'reflect-metadata';
7
+ import { ViewSchema } from './ViewSchema';
8
+ export declare class GalleryImageModel {
9
+ url: string;
10
+ alt: string;
11
+ thumbnail?: string;
12
+ }
13
+ export type GalleryVariant = 'thumbnails' | 'carousel' | 'grid';
14
+ export type ThumbnailPosition = 'left' | 'bottom' | 'right';
15
+ export declare class ImageGalleryModel extends ViewSchema {
16
+ images: GalleryImageModel[];
17
+ productName: string;
18
+ variant?: GalleryVariant;
19
+ thumbnailPosition?: ThumbnailPosition;
20
+ aspectRatio?: string;
21
+ showZoom?: boolean;
22
+ maxImages?: number;
23
+ dataSource?: string;
24
+ bindingOptions?: Record<string, unknown>;
25
+ }
26
+ export default ImageGalleryModel;
27
+ //# sourceMappingURL=ImageGallerySchema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageGallerySchema.d.ts","sourceRoot":"","sources":["../../src/schemas/ImageGallerySchema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,qBAAa,iBAAiB;IAG5B,GAAG,EAAG,MAAM,CAAC;IAIb,GAAG,EAAG,MAAM,CAAC;IAKb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAGD,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;AAGhE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE5D,qBACa,iBAAkB,SAAQ,UAAU;IAU/C,MAAM,EAAG,iBAAiB,EAAE,CAAC;IAU7B,WAAW,EAAG,MAAM,CAAC;IAkBrB,OAAO,CAAC,EAAE,cAAc,CAAC;IAkBzB,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAWtC,WAAW,CAAC,EAAE,MAAM,CAAC;IAUrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAWnB,SAAS,CAAC,EAAE,MAAM,CAAC;IAWnB,UAAU,CAAC,EAAE,MAAM,CAAC;IAUpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Schema for OptionSelector component - Universal option selection with visual modes
3
+ *
4
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
5
+ */
6
+ import 'reflect-metadata';
7
+ import { ViewSchema } from './ViewSchema';
8
+ export declare class SelectOptionModel {
9
+ id: string;
10
+ label: string;
11
+ available: boolean;
12
+ price?: number;
13
+ hexValue?: string;
14
+ imageUrl?: string;
15
+ }
16
+ export type DisplayMode = 'text' | 'color' | 'image';
17
+ export type OptionVariant = 'buttons' | 'dropdown' | 'grid';
18
+ export type OptionLayout = 'horizontal' | 'vertical' | 'wrap';
19
+ export type VisualSize = 'small' | 'medium' | 'large';
20
+ export declare class OptionSelectorModel extends ViewSchema {
21
+ options: SelectOptionModel[];
22
+ selectedOption?: string;
23
+ displayMode?: DisplayMode;
24
+ variant?: OptionVariant;
25
+ layout?: OptionLayout;
26
+ visualSize?: VisualSize;
27
+ showLabel?: boolean;
28
+ disabled?: boolean;
29
+ label?: string;
30
+ dataSource?: string;
31
+ bindingOptions?: Record<string, unknown>;
32
+ }
33
+ export default OptionSelectorModel;
34
+ //# sourceMappingURL=OptionSelectorSchema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OptionSelectorSchema.d.ts","sourceRoot":"","sources":["../../src/schemas/OptionSelectorSchema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,qBAAa,iBAAiB;IAG5B,EAAE,EAAG,MAAM,CAAC;IAIZ,KAAK,EAAG,MAAM,CAAC;IAIf,SAAS,EAAG,OAAO,CAAC;IAKpB,KAAK,CAAC,EAAE,MAAM,CAAC;IAWf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAWlB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAGrD,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;AAG5D,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;AAG9D,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtD,qBACa,mBAAoB,SAAQ,UAAU;IAUjD,OAAO,EAAG,iBAAiB,EAAE,CAAC;IAW9B,cAAc,CAAC,EAAE,MAAM,CAAC;IAkBxB,WAAW,CAAC,EAAE,WAAW,CAAC;IAkB1B,OAAO,CAAC,EAAE,aAAa,CAAC;IAkBxB,MAAM,CAAC,EAAE,YAAY,CAAC;IAkBtB,UAAU,CAAC,EAAE,UAAU,CAAC;IAUxB,SAAS,CAAC,EAAE,OAAO,CAAC;IAUpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAWnB,KAAK,CAAC,EAAE,MAAM,CAAC;IAWf,UAAU,CAAC,EAAE,MAAM,CAAC;IAUpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,eAAe,mBAAmB,CAAC"}
@@ -36,6 +36,8 @@ export * from './MetadataItemSchema';
36
36
  export * from './PageBannerHeaderSchema';
37
37
  export * from './PaletteSwitcherSchema';
38
38
  export * from './ProductCardSchema';
39
+ export * from './ImageGallerySchema';
40
+ export * from './OptionSelectorSchema';
39
41
  export * from './SafeSpanSchema';
40
42
  export * from './SectionSchema';
41
43
  export * from './TextInputFieldSchema';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,YAAY,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAE3E,OAAO,yBAAyB,CAAC;AAGjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AAGtC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,YAAY,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAE3E,OAAO,yBAAyB,CAAC;AAGjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AAGtC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qwickapps/react-framework",
3
- "version": "1.5.11",
3
+ "version": "1.5.13",
4
4
  "description": "Complete React framework with responsive navigation, flexible layouts, theming system, and reusable components for building modern applications.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -451,3 +451,11 @@ code, code.inline-code, pre.code-block {
451
451
  padding: 2px 4px;
452
452
  border-radius: 4px;
453
453
  }
454
+
455
+ /* Override inline code styles for code blocks */
456
+ pre code {
457
+ background-color: transparent !important;
458
+ color: inherit !important;
459
+ padding: 0;
460
+ border-radius: 0;
461
+ }
@@ -75,7 +75,7 @@ function ArticleView({
75
75
  {...htmlProps}
76
76
  {...otherProps}
77
77
  {...styleProps}
78
- {...{
78
+ sx={{
79
79
  // Modern article layout
80
80
  maxWidth: '900px',
81
81
  mx: 'auto',
@@ -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;