@qwickapps/react-framework 1.5.12 → 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 (38) 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.esm.js +1192 -265
  9. package/dist/index.js +1194 -263
  10. package/dist/palettes/manifest.json +19 -19
  11. package/dist/schemas/ImageGallerySchema.d.ts +27 -0
  12. package/dist/schemas/ImageGallerySchema.d.ts.map +1 -0
  13. package/dist/schemas/OptionSelectorSchema.d.ts +34 -0
  14. package/dist/schemas/OptionSelectorSchema.d.ts.map +1 -0
  15. package/dist/schemas/index.d.ts +2 -0
  16. package/dist/schemas/index.d.ts.map +1 -1
  17. package/package.json +1 -1
  18. package/src/components/blocks/Article.tsx +1 -1
  19. package/src/components/blocks/ImageGallery.tsx +464 -0
  20. package/src/components/blocks/OptionSelector.tsx +459 -0
  21. package/src/components/blocks/index.ts +4 -0
  22. package/src/schemas/ImageGallerySchema.ts +148 -0
  23. package/src/schemas/OptionSelectorSchema.ts +216 -0
  24. package/src/schemas/index.ts +2 -0
  25. package/src/stories/ImageGallery.stories.tsx +497 -0
  26. package/src/stories/OptionSelector.stories.tsx +506 -0
  27. /package/dist/palettes/{palette-autumn.1.5.12.css → palette-autumn.1.5.13.css} +0 -0
  28. /package/dist/palettes/{palette-autumn.1.5.12.min.css → palette-autumn.1.5.13.min.css} +0 -0
  29. /package/dist/palettes/{palette-cosmic.1.5.12.css → palette-cosmic.1.5.13.css} +0 -0
  30. /package/dist/palettes/{palette-cosmic.1.5.12.min.css → palette-cosmic.1.5.13.min.css} +0 -0
  31. /package/dist/palettes/{palette-default.1.5.12.css → palette-default.1.5.13.css} +0 -0
  32. /package/dist/palettes/{palette-default.1.5.12.min.css → palette-default.1.5.13.min.css} +0 -0
  33. /package/dist/palettes/{palette-ocean.1.5.12.css → palette-ocean.1.5.13.css} +0 -0
  34. /package/dist/palettes/{palette-ocean.1.5.12.min.css → palette-ocean.1.5.13.min.css} +0 -0
  35. /package/dist/palettes/{palette-spring.1.5.12.css → palette-spring.1.5.13.css} +0 -0
  36. /package/dist/palettes/{palette-spring.1.5.12.min.css → palette-spring.1.5.13.min.css} +0 -0
  37. /package/dist/palettes/{palette-winter.1.5.12.css → palette-winter.1.5.13.css} +0 -0
  38. /package/dist/palettes/{palette-winter.1.5.12.min.css → palette-winter.1.5.13.min.css} +0 -0
@@ -0,0 +1,459 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * OptionSelector - Universal option selection component with visual modes
5
+ *
6
+ * Features:
7
+ * - Multiple display modes (text, color swatches, images)
8
+ * - Multiple variants (buttons, dropdown, grid)
9
+ * - Visual disabled state for unavailable options
10
+ * - Tooltips for unavailable items
11
+ * - Selected state highlighting
12
+ * - Responsive layouts (horizontal, vertical, wrap)
13
+ * - Theme-compliant styling with CSS custom properties
14
+ * - Full serialization support
15
+ *
16
+ * @example
17
+ * // Text mode (sizes, quantities, etc.)
18
+ * <OptionSelector
19
+ * options={[
20
+ * { id: 's', label: 'S', available: true },
21
+ * { id: 'm', label: 'M', available: true },
22
+ * ]}
23
+ * displayMode="text"
24
+ * />
25
+ *
26
+ * @example
27
+ * // Color mode (color selection)
28
+ * <OptionSelector
29
+ * options={[
30
+ * { id: 'red', label: 'Red', hexValue: '#FF0000', available: true },
31
+ * { id: 'blue', label: 'Blue', hexValue: '#0000FF', available: true },
32
+ * ]}
33
+ * displayMode="color"
34
+ * />
35
+ *
36
+ * @example
37
+ * // Image mode (pattern selection)
38
+ * <OptionSelector
39
+ * options={[
40
+ * { id: 'pattern1', label: 'Stripes', imageUrl: '/patterns/stripes.jpg', available: true },
41
+ * ]}
42
+ * displayMode="image"
43
+ * />
44
+ *
45
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
46
+ */
47
+
48
+ import React, { useCallback } from 'react';
49
+ import { Box, Button, Select, MenuItem, FormControl, InputLabel, Tooltip } from '@mui/material';
50
+ import CheckIcon from '@mui/icons-material/Check';
51
+ import { createSerializableView, SerializableComponent } from '../shared/createSerializableView';
52
+ import { ViewProps } from '../shared/viewProps';
53
+
54
+ export interface SelectOption {
55
+ /** Unique identifier */
56
+ id: string;
57
+
58
+ /** Display label */
59
+ label: string;
60
+
61
+ /** Whether this option is available for selection */
62
+ available: boolean;
63
+
64
+ /** Optional price adjustment */
65
+ price?: number;
66
+
67
+ /** Hex color value (for color display mode) */
68
+ hexValue?: string;
69
+
70
+ /** Image URL (for image or color display mode) */
71
+ imageUrl?: string;
72
+ }
73
+
74
+ export interface OptionSelectorProps extends ViewProps {
75
+ /** Array of available options */
76
+ options: SelectOption[];
77
+
78
+ /** Currently selected option ID */
79
+ selectedOption?: string;
80
+
81
+ /** Callback when option is selected */
82
+ onOptionSelect?: (optionId: string) => void;
83
+
84
+ /** Display mode */
85
+ displayMode?: 'text' | 'color' | 'image';
86
+
87
+ /** Display variant */
88
+ variant?: 'buttons' | 'dropdown' | 'grid';
89
+
90
+ /** Layout direction (for buttons variant) */
91
+ layout?: 'horizontal' | 'vertical' | 'wrap';
92
+
93
+ /** Visual size (for color/image modes) */
94
+ visualSize?: 'small' | 'medium' | 'large';
95
+
96
+ /** Show label below visual (for color/image modes) */
97
+ showLabel?: boolean;
98
+
99
+ /** Disable all selections */
100
+ disabled?: boolean;
101
+
102
+ /** Label for the selector */
103
+ label?: string;
104
+
105
+ /** Data source for dynamic loading */
106
+ dataSource?: string;
107
+
108
+ /** Data binding configuration */
109
+ bindingOptions?: Record<string, unknown>;
110
+ }
111
+
112
+ // View component
113
+ function OptionSelectorView({
114
+ options = [],
115
+ selectedOption,
116
+ onOptionSelect,
117
+ displayMode = 'text',
118
+ variant = 'grid',
119
+ layout = 'wrap',
120
+ visualSize = 'medium',
121
+ showLabel = false,
122
+ disabled = false,
123
+ label = 'Select Option',
124
+ dataSource,
125
+ bindingOptions,
126
+ ...restProps
127
+ }: OptionSelectorProps) {
128
+ const handleOptionClick = useCallback((optionId: string, available: boolean) => {
129
+ if (!disabled && available && onOptionSelect) {
130
+ onOptionSelect(optionId);
131
+ }
132
+ }, [disabled, onOptionSelect]);
133
+
134
+ const handleDropdownChange = useCallback((event: any) => {
135
+ if (onOptionSelect) {
136
+ onOptionSelect(event.target.value);
137
+ }
138
+ }, [onOptionSelect]);
139
+
140
+ // Get visual size in pixels
141
+ const getSizePixels = () => {
142
+ if (displayMode === 'text') return 48;
143
+ switch (visualSize) {
144
+ case 'small': return 32;
145
+ case 'large': return 56;
146
+ case 'medium':
147
+ default: return 44;
148
+ }
149
+ };
150
+
151
+ const sizeInPx = getSizePixels();
152
+
153
+ // Render nothing if no options
154
+ if (!options || options.length === 0) {
155
+ return null;
156
+ }
157
+
158
+ // Dropdown variant
159
+ if (variant === 'dropdown') {
160
+ return (
161
+ <FormControl
162
+ fullWidth
163
+ disabled={disabled}
164
+ {...restProps}
165
+ sx={{
166
+ '& .MuiOutlinedInput-root': {
167
+ '& fieldset': {
168
+ borderColor: 'var(--theme-border-main)',
169
+ },
170
+ '&:hover fieldset': {
171
+ borderColor: 'var(--theme-border-emphasis)',
172
+ },
173
+ '&.Mui-focused fieldset': {
174
+ borderColor: 'var(--theme-primary)',
175
+ },
176
+ },
177
+ '& .MuiInputLabel-root': {
178
+ color: 'var(--theme-text-secondary)',
179
+ '&.Mui-focused': {
180
+ color: 'var(--theme-primary)',
181
+ },
182
+ },
183
+ }}
184
+ >
185
+ <InputLabel>{label}</InputLabel>
186
+ <Select
187
+ value={selectedOption || ''}
188
+ onChange={handleDropdownChange}
189
+ label={label}
190
+ sx={{
191
+ backgroundColor: 'var(--theme-surface)',
192
+ color: 'var(--theme-text-primary)',
193
+ }}
194
+ >
195
+ {options.map((option) => (
196
+ <MenuItem
197
+ key={option.id}
198
+ value={option.id}
199
+ disabled={!option.available}
200
+ sx={{
201
+ color: option.available ? 'var(--theme-text-primary)' : 'var(--theme-text-disabled)',
202
+ }}
203
+ >
204
+ {displayMode === 'color' && option.hexValue && (
205
+ <Box
206
+ sx={{
207
+ width: 20,
208
+ height: 20,
209
+ mr: 1,
210
+ borderRadius: '50%',
211
+ backgroundColor: option.hexValue,
212
+ border: '1px solid var(--theme-border-main)',
213
+ backgroundImage: option.imageUrl ? `url(${option.imageUrl})` : undefined,
214
+ backgroundSize: 'cover',
215
+ }}
216
+ />
217
+ )}
218
+ {option.label}
219
+ {!option.available && ' (Out of stock)'}
220
+ {option.price && option.price !== 0 && ` (+$${(option.price / 100).toFixed(2)})`}
221
+ </MenuItem>
222
+ ))}
223
+ </Select>
224
+ </FormControl>
225
+ );
226
+ }
227
+
228
+ // Buttons/Grid variant with visual modes
229
+ const getLayoutStyles = () => {
230
+ if (variant === 'grid') {
231
+ const minWidth = displayMode === 'text' ? 60 : sizeInPx + 16;
232
+ return {
233
+ display: 'grid',
234
+ gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}px, 1fr))`,
235
+ gap: displayMode === 'text' ? 1 : 2,
236
+ };
237
+ }
238
+
239
+ return {
240
+ display: 'flex',
241
+ flexDirection: layout === 'vertical' ? 'column' : 'row',
242
+ flexWrap: layout === 'wrap' ? 'wrap' : 'nowrap',
243
+ gap: 1,
244
+ };
245
+ };
246
+
247
+ return (
248
+ <Box {...restProps}>
249
+ {label && (
250
+ <Box
251
+ component="label"
252
+ sx={{
253
+ display: 'block',
254
+ mb: 1,
255
+ fontSize: '0.875rem',
256
+ fontWeight: 500,
257
+ color: 'var(--theme-text-primary)',
258
+ }}
259
+ >
260
+ {label}
261
+ </Box>
262
+ )}
263
+
264
+ <Box sx={getLayoutStyles()}>
265
+ {options.map((option) => {
266
+ const isSelected = selectedOption === option.id;
267
+ const isAvailable = option.available;
268
+
269
+ // Text mode - render as buttons
270
+ if (displayMode === 'text') {
271
+ const button = (
272
+ <Button
273
+ key={option.id}
274
+ onClick={() => handleOptionClick(option.id, isAvailable)}
275
+ disabled={disabled || !isAvailable}
276
+ variant={isSelected ? 'contained' : 'outlined'}
277
+ sx={{
278
+ minWidth: variant === 'grid' ? '60px' : '80px',
279
+ height: `${sizeInPx}px`,
280
+ borderRadius: 'var(--theme-border-radius-small)',
281
+ textTransform: 'uppercase',
282
+ fontWeight: 600,
283
+ fontSize: '0.875rem',
284
+ backgroundColor: isSelected ? 'var(--theme-primary)' : 'var(--theme-surface)',
285
+ color: isSelected ? 'var(--theme-text-on-primary)' : 'var(--theme-text-primary)',
286
+ borderColor: isSelected ? 'var(--theme-primary)' : 'var(--theme-border-main)',
287
+ borderWidth: '2px',
288
+ borderStyle: 'solid',
289
+ '&.Mui-disabled': {
290
+ backgroundColor: 'var(--theme-surface-variant)',
291
+ color: 'var(--theme-text-disabled)',
292
+ borderColor: 'var(--theme-border-light)',
293
+ opacity: 0.5,
294
+ textDecoration: 'line-through',
295
+ },
296
+ '&:hover:not(.Mui-disabled)': {
297
+ backgroundColor: !isSelected ? 'var(--theme-surface-variant)' : undefined,
298
+ borderColor: !isSelected ? 'var(--theme-border-emphasis)' : undefined,
299
+ boxShadow: 'var(--theme-elevation-1)',
300
+ },
301
+ transition: 'all 0.2s ease-in-out',
302
+ }}
303
+ >
304
+ {option.label}
305
+ </Button>
306
+ );
307
+
308
+ return !isAvailable ? (
309
+ <Tooltip
310
+ key={option.id}
311
+ title="Not available"
312
+ arrow
313
+ sx={{
314
+ '& .MuiTooltip-tooltip': {
315
+ backgroundColor: 'var(--theme-surface)',
316
+ color: 'var(--theme-text-primary)',
317
+ border: '1px solid var(--theme-border-main)',
318
+ boxShadow: 'var(--theme-elevation-2)',
319
+ },
320
+ '& .MuiTooltip-arrow': {
321
+ color: 'var(--theme-surface)',
322
+ },
323
+ }}
324
+ >
325
+ <span>{button}</span>
326
+ </Tooltip>
327
+ ) : button;
328
+ }
329
+
330
+ // Color/Image mode - render as visual swatches
331
+ const swatchContent = (
332
+ <Box
333
+ key={option.id}
334
+ onClick={() => handleOptionClick(option.id, isAvailable)}
335
+ sx={{
336
+ display: 'flex',
337
+ flexDirection: 'column',
338
+ alignItems: 'center',
339
+ gap: 0.5,
340
+ cursor: disabled || !isAvailable ? 'not-allowed' : 'pointer',
341
+ opacity: disabled || !isAvailable ? 0.5 : 1,
342
+ }}
343
+ >
344
+ <Box
345
+ sx={{
346
+ position: 'relative',
347
+ width: sizeInPx,
348
+ height: sizeInPx,
349
+ borderRadius: displayMode === 'color' ? '50%' : 'var(--theme-border-radius-small)',
350
+ backgroundColor: option.hexValue || 'var(--theme-surface-variant)',
351
+ backgroundImage: option.imageUrl ? `url(${option.imageUrl})` : undefined,
352
+ backgroundSize: 'cover',
353
+ backgroundPosition: 'center',
354
+ border: '2px solid',
355
+ borderColor: isSelected ? 'var(--theme-primary)' : 'var(--theme-border-main)',
356
+ boxShadow: isSelected ? 'var(--theme-elevation-2)' : 'none',
357
+ transition: 'all 0.2s ease-in-out',
358
+ ...(isAvailable && !disabled && !isSelected && {
359
+ '&:hover': {
360
+ borderColor: 'var(--theme-border-emphasis)',
361
+ boxShadow: 'var(--theme-elevation-1)',
362
+ transform: 'scale(1.05)',
363
+ },
364
+ }),
365
+ ...(!isAvailable && {
366
+ '&::after': {
367
+ content: '""',
368
+ position: 'absolute',
369
+ top: '50%',
370
+ left: '10%',
371
+ right: '10%',
372
+ height: '2px',
373
+ backgroundColor: 'var(--theme-border-emphasis)',
374
+ transform: 'translateY(-50%) rotate(-45deg)',
375
+ },
376
+ }),
377
+ }}
378
+ >
379
+ {isSelected && (
380
+ <Box
381
+ sx={{
382
+ position: 'absolute',
383
+ top: '50%',
384
+ left: '50%',
385
+ transform: 'translate(-50%, -50%)',
386
+ display: 'flex',
387
+ alignItems: 'center',
388
+ justifyContent: 'center',
389
+ width: '100%',
390
+ height: '100%',
391
+ borderRadius: 'inherit',
392
+ backgroundColor: 'rgba(0, 0, 0, 0.3)',
393
+ }}
394
+ >
395
+ <CheckIcon
396
+ sx={{
397
+ color: 'white',
398
+ fontSize: sizeInPx * 0.5,
399
+ filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.5))',
400
+ }}
401
+ />
402
+ </Box>
403
+ )}
404
+ </Box>
405
+
406
+ {showLabel && (
407
+ <Box
408
+ sx={{
409
+ fontSize: '0.75rem',
410
+ color: 'var(--theme-text-secondary)',
411
+ textAlign: 'center',
412
+ maxWidth: sizeInPx + 16,
413
+ overflow: 'hidden',
414
+ textOverflow: 'ellipsis',
415
+ whiteSpace: 'nowrap',
416
+ }}
417
+ >
418
+ {option.label}
419
+ </Box>
420
+ )}
421
+ </Box>
422
+ );
423
+
424
+ return (
425
+ <Tooltip
426
+ key={option.id}
427
+ title={!isAvailable ? 'Not available' : option.label}
428
+ arrow
429
+ sx={{
430
+ '& .MuiTooltip-tooltip': {
431
+ backgroundColor: 'var(--theme-surface)',
432
+ color: 'var(--theme-text-primary)',
433
+ border: '1px solid var(--theme-border-main)',
434
+ boxShadow: 'var(--theme-elevation-2)',
435
+ },
436
+ '& .MuiTooltip-arrow': {
437
+ color: 'var(--theme-surface)',
438
+ },
439
+ }}
440
+ >
441
+ {swatchContent}
442
+ </Tooltip>
443
+ );
444
+ })}
445
+ </Box>
446
+ </Box>
447
+ );
448
+ }
449
+
450
+ // Create the serializable component
451
+ export const OptionSelector: SerializableComponent<OptionSelectorProps> =
452
+ createSerializableView<OptionSelectorProps>({
453
+ tagName: 'OptionSelector',
454
+ version: '1.0.0',
455
+ role: 'view',
456
+ View: OptionSelectorView,
457
+ });
458
+
459
+ export default OptionSelector;
@@ -22,6 +22,8 @@ export { default as Section } from './Section';
22
22
  export { default as Image } from './Image';
23
23
  export { default as Text } from './Text';
24
24
  export { default as ProductCard } from './ProductCard';
25
+ export { default as ImageGallery } from './ImageGallery';
26
+ export { default as OptionSelector } from './OptionSelector';
25
27
  export { default as FeatureCard } from './FeatureCard';
26
28
  export { default as CardListGrid } from './CardListGrid';
27
29
 
@@ -37,5 +39,7 @@ export type { SectionProps } from './Section';
37
39
  export type { ImageProps } from './Image';
38
40
  export type { TextProps } from './Text';
39
41
  export type { ProductCardProps, Product, ProductCardAction } from './ProductCard';
42
+ export type { ImageGalleryProps, GalleryImage } from './ImageGallery';
43
+ export type { OptionSelectorProps, SelectOption } from './OptionSelector';
40
44
  export type { FeatureCardProps, FeatureItem, FeatureCardAction } from './FeatureCard';
41
45
  export type { CardListGridProps } from './CardListGrid';
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Schema for ImageGallery component - Image gallery with multiple view variants
3
+ *
4
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
5
+ */
6
+
7
+ import { IsBoolean, IsOptional, IsString, IsNumber, IsIn, IsArray, ValidateNested } from 'class-validator';
8
+ import { Type } from 'class-transformer';
9
+ import 'reflect-metadata';
10
+ import { Editor, Field, Schema, FieldType, DataType } from '@qwickapps/schema';
11
+ import { ViewSchema } from './ViewSchema';
12
+
13
+ // Product image interface
14
+ export class GalleryImageModel {
15
+ @Field({ dataType: DataType.STRING })
16
+ @IsString()
17
+ url!: string;
18
+
19
+ @Field({ dataType: DataType.STRING })
20
+ @IsString()
21
+ alt!: string;
22
+
23
+ @Field({ dataType: DataType.STRING })
24
+ @IsOptional()
25
+ @IsString()
26
+ thumbnail?: string;
27
+ }
28
+
29
+ // Gallery variants
30
+ export type GalleryVariant = 'thumbnails' | 'carousel' | 'grid';
31
+
32
+ // Thumbnail positions
33
+ export type ThumbnailPosition = 'left' | 'bottom' | 'right';
34
+
35
+ @Schema('ImageGallery', '1.0.0')
36
+ export class ImageGalleryModel extends ViewSchema {
37
+ @Field({ dataType: DataType.ARRAY })
38
+ @Editor({
39
+ field_type: FieldType.ARRAY,
40
+ label: 'Product Images',
41
+ description: 'Array of product images to display in the gallery',
42
+ })
43
+ @IsArray()
44
+ @ValidateNested({ each: true })
45
+ @Type(() => GalleryImageModel)
46
+ images!: GalleryImageModel[];
47
+
48
+ @Field({ dataType: DataType.STRING })
49
+ @Editor({
50
+ field_type: FieldType.TEXT,
51
+ label: 'Product Name',
52
+ description: 'Product name for accessibility',
53
+ placeholder: 'Premium Cotton T-Shirt'
54
+ })
55
+ @IsString()
56
+ productName!: string;
57
+
58
+ @Field({ defaultValue: 'thumbnails', dataType: DataType.STRING })
59
+ @Editor({
60
+ field_type: FieldType.SELECT,
61
+ label: 'Gallery Variant',
62
+ description: 'Display variant for the gallery',
63
+ validation: {
64
+ options: [
65
+ { label: 'Thumbnails', value: 'thumbnails' },
66
+ { label: 'Carousel', value: 'carousel' },
67
+ { label: 'Grid', value: 'grid' }
68
+ ]
69
+ }
70
+ })
71
+ @IsOptional()
72
+ @IsString()
73
+ @IsIn(['thumbnails', 'carousel', 'grid'])
74
+ variant?: GalleryVariant;
75
+
76
+ @Field({ defaultValue: 'left', dataType: DataType.STRING })
77
+ @Editor({
78
+ field_type: FieldType.SELECT,
79
+ label: 'Thumbnail Position',
80
+ description: 'Position of thumbnails (only for thumbnails variant)',
81
+ validation: {
82
+ options: [
83
+ { label: 'Left', value: 'left' },
84
+ { label: 'Bottom', value: 'bottom' },
85
+ { label: 'Right', value: 'right' }
86
+ ]
87
+ }
88
+ })
89
+ @IsOptional()
90
+ @IsString()
91
+ @IsIn(['left', 'bottom', 'right'])
92
+ thumbnailPosition?: ThumbnailPosition;
93
+
94
+ @Field({ defaultValue: '1', dataType: DataType.STRING })
95
+ @Editor({
96
+ field_type: FieldType.TEXT,
97
+ label: 'Aspect Ratio',
98
+ description: 'Aspect ratio for main image (e.g., "1", "4/3", "16/9")',
99
+ placeholder: '1'
100
+ })
101
+ @IsOptional()
102
+ @IsString()
103
+ aspectRatio?: string;
104
+
105
+ @Field({ defaultValue: true, dataType: DataType.BOOLEAN })
106
+ @Editor({
107
+ field_type: FieldType.BOOLEAN,
108
+ label: 'Show Zoom',
109
+ description: 'Enable zoom functionality for images'
110
+ })
111
+ @IsOptional()
112
+ @IsBoolean()
113
+ showZoom?: boolean;
114
+
115
+ @Field({ dataType: DataType.NUMBER })
116
+ @Editor({
117
+ field_type: FieldType.NUMBER,
118
+ label: 'Max Images',
119
+ description: 'Maximum number of images to display (leave empty for all)',
120
+ placeholder: '8'
121
+ })
122
+ @IsOptional()
123
+ @IsNumber()
124
+ maxImages?: number;
125
+
126
+ @Field({ dataType: DataType.STRING })
127
+ @Editor({
128
+ field_type: FieldType.TEXT,
129
+ label: 'Data Source',
130
+ description: 'Data source for dynamic image loading',
131
+ placeholder: 'product-images'
132
+ })
133
+ @IsOptional()
134
+ @IsString()
135
+ dataSource?: string;
136
+
137
+ @Field({ dataType: DataType.OBJECT })
138
+ @Editor({
139
+ field_type: FieldType.TEXTAREA,
140
+ label: 'Binding Options',
141
+ description: 'Data binding configuration (JSON format)',
142
+ placeholder: '{ "filter": {}, "sort": {} }'
143
+ })
144
+ @IsOptional()
145
+ bindingOptions?: Record<string, unknown>;
146
+ }
147
+
148
+ export default ImageGalleryModel;