@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.
- package/README.md +23 -0
- package/dist/components/blocks/ImageGallery.d.ts +30 -0
- package/dist/components/blocks/ImageGallery.d.ts.map +1 -0
- package/dist/components/blocks/OptionSelector.d.ts +45 -0
- package/dist/components/blocks/OptionSelector.d.ts.map +1 -0
- package/dist/components/blocks/index.d.ts +4 -0
- package/dist/components/blocks/index.d.ts.map +1 -1
- package/dist/index.esm.js +1192 -265
- package/dist/index.js +1194 -263
- package/dist/palettes/manifest.json +19 -19
- package/dist/schemas/ImageGallerySchema.d.ts +27 -0
- package/dist/schemas/ImageGallerySchema.d.ts.map +1 -0
- package/dist/schemas/OptionSelectorSchema.d.ts +34 -0
- package/dist/schemas/OptionSelectorSchema.d.ts.map +1 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/blocks/Article.tsx +1 -1
- package/src/components/blocks/ImageGallery.tsx +464 -0
- package/src/components/blocks/OptionSelector.tsx +459 -0
- package/src/components/blocks/index.ts +4 -0
- package/src/schemas/ImageGallerySchema.ts +148 -0
- package/src/schemas/OptionSelectorSchema.ts +216 -0
- package/src/schemas/index.ts +2 -0
- package/src/stories/ImageGallery.stories.tsx +497 -0
- package/src/stories/OptionSelector.stories.tsx +506 -0
- /package/dist/palettes/{palette-autumn.1.5.12.css → palette-autumn.1.5.13.css} +0 -0
- /package/dist/palettes/{palette-autumn.1.5.12.min.css → palette-autumn.1.5.13.min.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.12.css → palette-cosmic.1.5.13.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.12.min.css → palette-cosmic.1.5.13.min.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.12.css → palette-default.1.5.13.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.12.min.css → palette-default.1.5.13.min.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.12.css → palette-ocean.1.5.13.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.12.min.css → palette-ocean.1.5.13.min.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.12.css → palette-spring.1.5.13.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.12.min.css → palette-spring.1.5.13.min.css} +0 -0
- /package/dist/palettes/{palette-winter.1.5.12.css → palette-winter.1.5.13.css} +0 -0
- /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;
|