@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.
- 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/components/forms/Captcha.d.ts +33 -28
- package/dist/components/forms/Captcha.d.ts.map +1 -1
- package/dist/components/forms/FormCheckbox.d.ts +15 -12
- package/dist/components/forms/FormCheckbox.d.ts.map +1 -1
- package/dist/components/forms/FormField.d.ts +20 -23
- package/dist/components/forms/FormField.d.ts.map +1 -1
- package/dist/components/forms/FormSelect.d.ts +16 -15
- package/dist/components/forms/FormSelect.d.ts.map +1 -1
- package/dist/hooks/useBaseProps.d.ts +27 -1172
- package/dist/hooks/useBaseProps.d.ts.map +1 -1
- package/dist/index.esm.js +1674 -554
- package/dist/index.js +1676 -552
- package/dist/palettes/manifest.json +19 -19
- package/dist/schemas/CaptchaSchema.d.ts +16 -0
- package/dist/schemas/CaptchaSchema.d.ts.map +1 -0
- package/dist/schemas/FormCheckboxSchema.d.ts +16 -0
- package/dist/schemas/FormCheckboxSchema.d.ts.map +1 -0
- package/dist/schemas/FormFieldSchema.d.ts +23 -0
- package/dist/schemas/FormFieldSchema.d.ts.map +1 -0
- package/dist/schemas/FormSelectSchema.d.ts +20 -0
- package/dist/schemas/FormSelectSchema.d.ts.map +1 -0
- 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 +6 -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/components/forms/Captcha.tsx +57 -63
- package/src/components/forms/FormCheckbox.tsx +35 -43
- package/src/components/forms/FormField.tsx +50 -66
- package/src/components/forms/FormSelect.tsx +41 -49
- package/src/hooks/useBaseProps.ts +34 -1
- package/src/schemas/CaptchaSchema.ts +65 -0
- package/src/schemas/FormCheckboxSchema.ts +65 -0
- package/src/schemas/FormFieldSchema.ts +140 -0
- package/src/schemas/FormSelectSchema.ts +108 -0
- package/src/schemas/ImageGallerySchema.ts +148 -0
- package/src/schemas/OptionSelectorSchema.ts +216 -0
- package/src/schemas/index.ts +6 -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.6.0.css} +0 -0
- /package/dist/palettes/{palette-autumn.1.5.12.min.css → palette-autumn.1.6.0.min.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.12.css → palette-cosmic.1.6.0.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.12.min.css → palette-cosmic.1.6.0.min.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.12.css → palette-default.1.6.0.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.12.min.css → palette-default.1.6.0.min.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.12.css → palette-ocean.1.6.0.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.12.min.css → palette-ocean.1.6.0.min.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.12.css → palette-spring.1.6.0.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.12.min.css → palette-spring.1.6.0.min.css} +0 -0
- /package/dist/palettes/{palette-winter.1.5.12.css → palette-winter.1.6.0.css} +0 -0
- /package/dist/palettes/{palette-winter.1.5.12.min.css → palette-winter.1.6.0.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';
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Provider-agnostic API
|
|
12
12
|
* - Automatic script loading
|
|
13
13
|
* - TypeScript support
|
|
14
|
-
* - Themed styling with
|
|
14
|
+
* - Themed styling with schema-driven architecture
|
|
15
15
|
* - Grid behavior support
|
|
16
16
|
* - Error handling
|
|
17
17
|
*
|
|
@@ -20,57 +20,57 @@
|
|
|
20
20
|
|
|
21
21
|
import React, { useEffect, useRef, useState } from 'react';
|
|
22
22
|
import { Box, Alert } from '@mui/material';
|
|
23
|
-
import {
|
|
23
|
+
import type { SchemaProps } from '@qwickapps/schema';
|
|
24
|
+
import CaptchaModel from '../../schemas/CaptchaSchema';
|
|
25
|
+
import { ViewProps } from '../shared/viewProps';
|
|
26
|
+
import { createSerializableView, SerializableComponent } from '../shared/createSerializableView';
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
// Declare global interfaces for CAPTCHA providers
|
|
29
|
+
declare global {
|
|
30
|
+
interface Window {
|
|
31
|
+
grecaptcha?: {
|
|
32
|
+
render?: (container: HTMLElement | null, params: Record<string, unknown>) => string | number;
|
|
33
|
+
execute?: (siteKey: string, options: { action: string }) => Promise<string>;
|
|
34
|
+
reset?: (widgetId: string | number) => void;
|
|
35
|
+
};
|
|
36
|
+
hcaptcha?: {
|
|
37
|
+
render?: (container: HTMLElement | null, params: Record<string, unknown>) => string | number;
|
|
38
|
+
remove?: (widgetId: string | number) => void;
|
|
39
|
+
};
|
|
40
|
+
turnstile?: {
|
|
41
|
+
render?: (container: HTMLElement | null, params: Record<string, unknown>) => string | number;
|
|
42
|
+
remove?: (widgetId: string | number) => void;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
26
46
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Props interface for Captcha component
|
|
49
|
+
* Combines schema props with callback handlers
|
|
50
|
+
*/
|
|
51
|
+
export interface CaptchaProps extends ViewProps, SchemaProps<typeof CaptchaModel> {
|
|
32
52
|
/** Callback when CAPTCHA is successfully completed */
|
|
33
53
|
onVerify: (token: string) => void;
|
|
34
54
|
/** Callback when CAPTCHA expires or fails */
|
|
35
55
|
onExpire?: () => void;
|
|
36
56
|
/** Callback when CAPTCHA encounters an error */
|
|
37
57
|
onError?: (error: Error) => void;
|
|
38
|
-
/** Theme for the widget (light or dark) */
|
|
39
|
-
theme?: 'light' | 'dark';
|
|
40
|
-
/** Size of the widget */
|
|
41
|
-
size?: 'normal' | 'compact' | 'invisible';
|
|
42
|
-
/** reCAPTCHA v3 action name */
|
|
43
|
-
action?: string;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
export const Captcha = React.forwardRef<HTMLDivElement, CaptchaProps>((props, ref) => {
|
|
61
|
-
const { gridProps, styleProps, htmlProps, restProps } = useBaseProps(props);
|
|
62
|
-
|
|
63
|
-
const {
|
|
64
|
-
provider,
|
|
65
|
-
siteKey,
|
|
66
|
-
onVerify,
|
|
67
|
-
onExpire,
|
|
68
|
-
onError,
|
|
69
|
-
theme = 'light',
|
|
70
|
-
size = 'normal',
|
|
71
|
-
action = 'submit',
|
|
72
|
-
} = restProps as CaptchaBaseProps;
|
|
73
|
-
|
|
60
|
+
/**
|
|
61
|
+
* CaptchaView - Pure view component that renders the CAPTCHA widget
|
|
62
|
+
*/
|
|
63
|
+
function CaptchaView({
|
|
64
|
+
provider,
|
|
65
|
+
siteKey,
|
|
66
|
+
onVerify,
|
|
67
|
+
onExpire,
|
|
68
|
+
onError,
|
|
69
|
+
theme = 'light',
|
|
70
|
+
size = 'normal',
|
|
71
|
+
action = 'submit',
|
|
72
|
+
...restProps
|
|
73
|
+
}: CaptchaProps) {
|
|
74
74
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
75
75
|
const widgetIdRef = useRef<string | number | null>(null);
|
|
76
76
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
@@ -155,7 +155,7 @@ export const Captcha = React.forwardRef<HTMLDivElement, CaptchaProps>((props, re
|
|
|
155
155
|
try {
|
|
156
156
|
switch (provider) {
|
|
157
157
|
case 'recaptcha-v2':
|
|
158
|
-
if (window.grecaptcha
|
|
158
|
+
if (window.grecaptcha?.render) {
|
|
159
159
|
widgetIdRef.current = window.grecaptcha.render(containerRef.current, {
|
|
160
160
|
sitekey: siteKey,
|
|
161
161
|
callback: onVerify,
|
|
@@ -173,7 +173,7 @@ export const Captcha = React.forwardRef<HTMLDivElement, CaptchaProps>((props, re
|
|
|
173
173
|
|
|
174
174
|
case 'recaptcha-v3':
|
|
175
175
|
// reCAPTCHA v3 is invisible and executes programmatically
|
|
176
|
-
if (window.grecaptcha
|
|
176
|
+
if (window.grecaptcha?.execute) {
|
|
177
177
|
window.grecaptcha.execute(siteKey, { action }).then((token: string) => {
|
|
178
178
|
onVerify(token);
|
|
179
179
|
}).catch((err: Error) => {
|
|
@@ -184,7 +184,7 @@ export const Captcha = React.forwardRef<HTMLDivElement, CaptchaProps>((props, re
|
|
|
184
184
|
break;
|
|
185
185
|
|
|
186
186
|
case 'hcaptcha':
|
|
187
|
-
if (window.hcaptcha
|
|
187
|
+
if (window.hcaptcha?.render) {
|
|
188
188
|
widgetIdRef.current = window.hcaptcha.render(containerRef.current, {
|
|
189
189
|
sitekey: siteKey,
|
|
190
190
|
callback: onVerify,
|
|
@@ -201,7 +201,7 @@ export const Captcha = React.forwardRef<HTMLDivElement, CaptchaProps>((props, re
|
|
|
201
201
|
break;
|
|
202
202
|
|
|
203
203
|
case 'turnstile':
|
|
204
|
-
if (window.turnstile
|
|
204
|
+
if (window.turnstile?.render) {
|
|
205
205
|
widgetIdRef.current = window.turnstile.render(containerRef.current, {
|
|
206
206
|
sitekey: siteKey,
|
|
207
207
|
callback: onVerify,
|
|
@@ -257,21 +257,10 @@ export const Captcha = React.forwardRef<HTMLDivElement, CaptchaProps>((props, re
|
|
|
257
257
|
|
|
258
258
|
return (
|
|
259
259
|
<Box
|
|
260
|
-
|
|
261
|
-
{...htmlProps}
|
|
260
|
+
{...restProps}
|
|
262
261
|
sx={{
|
|
263
262
|
my: 2,
|
|
264
|
-
...styleProps.sx,
|
|
265
263
|
}}
|
|
266
|
-
// Store grid props as data attributes for ColumnLayout to pick up
|
|
267
|
-
{...(gridProps && {
|
|
268
|
-
'data-grid-span': gridProps.span,
|
|
269
|
-
'data-grid-xs': gridProps.xs,
|
|
270
|
-
'data-grid-sm': gridProps.sm,
|
|
271
|
-
'data-grid-md': gridProps.md,
|
|
272
|
-
'data-grid-lg': gridProps.lg,
|
|
273
|
-
'data-grid-xl': gridProps.xl,
|
|
274
|
-
})}
|
|
275
264
|
>
|
|
276
265
|
{error && (
|
|
277
266
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
@@ -281,11 +270,16 @@ export const Captcha = React.forwardRef<HTMLDivElement, CaptchaProps>((props, re
|
|
|
281
270
|
<div ref={containerRef} />
|
|
282
271
|
</Box>
|
|
283
272
|
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
Captcha.displayName = 'Captcha';
|
|
273
|
+
}
|
|
287
274
|
|
|
288
|
-
|
|
289
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Create Captcha component using the factory pattern
|
|
277
|
+
*/
|
|
278
|
+
export const Captcha: SerializableComponent<CaptchaProps> = createSerializableView<CaptchaProps>({
|
|
279
|
+
tagName: 'Captcha',
|
|
280
|
+
version: '1.0.0',
|
|
281
|
+
role: 'input',
|
|
282
|
+
View: CaptchaView,
|
|
283
|
+
});
|
|
290
284
|
|
|
291
285
|
export default Captcha;
|