@qwickapps/react-framework 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +23 -0
  2. package/dist/components/blocks/ImageGallery.d.ts.map +1 -1
  3. package/dist/components/blocks/OptionSelector.d.ts.map +1 -1
  4. package/dist/components/blocks/ProductCard.d.ts +10 -2
  5. package/dist/components/blocks/ProductCard.d.ts.map +1 -1
  6. package/dist/components/forms/FormCheckbox.d.ts.map +1 -1
  7. package/dist/components/forms/FormField.d.ts.map +1 -1
  8. package/dist/components/forms/FormSelect.d.ts.map +1 -1
  9. package/dist/index.esm.js +257 -31
  10. package/dist/index.js +256 -30
  11. package/dist/palettes/manifest.json +22 -22
  12. package/package.json +1 -1
  13. package/src/components/blocks/ImageGallery.tsx +6 -5
  14. package/src/components/blocks/OptionSelector.tsx +18 -3
  15. package/src/components/blocks/ProductCard.tsx +283 -84
  16. package/src/components/forms/FormCheckbox.tsx +15 -0
  17. package/src/components/forms/FormField.tsx +15 -0
  18. package/src/components/forms/FormSelect.tsx +15 -0
  19. package/src/stories/ProductCard.stories.tsx +151 -1
  20. /package/dist/palettes/{palette-autumn.1.7.0.css → palette-autumn.1.8.0.css} +0 -0
  21. /package/dist/palettes/{palette-autumn.1.7.0.min.css → palette-autumn.1.8.0.min.css} +0 -0
  22. /package/dist/palettes/{palette-boutique.1.7.0.css → palette-boutique.1.8.0.css} +0 -0
  23. /package/dist/palettes/{palette-boutique.1.7.0.min.css → palette-boutique.1.8.0.min.css} +0 -0
  24. /package/dist/palettes/{palette-cosmic.1.7.0.css → palette-cosmic.1.8.0.css} +0 -0
  25. /package/dist/palettes/{palette-cosmic.1.7.0.min.css → palette-cosmic.1.8.0.min.css} +0 -0
  26. /package/dist/palettes/{palette-default.1.7.0.css → palette-default.1.8.0.css} +0 -0
  27. /package/dist/palettes/{palette-default.1.7.0.min.css → palette-default.1.8.0.min.css} +0 -0
  28. /package/dist/palettes/{palette-ocean.1.7.0.css → palette-ocean.1.8.0.css} +0 -0
  29. /package/dist/palettes/{palette-ocean.1.7.0.min.css → palette-ocean.1.8.0.min.css} +0 -0
  30. /package/dist/palettes/{palette-spring.1.7.0.css → palette-spring.1.8.0.css} +0 -0
  31. /package/dist/palettes/{palette-spring.1.7.0.min.css → palette-spring.1.8.0.min.css} +0 -0
  32. /package/dist/palettes/{palette-winter.1.7.0.css → palette-winter.1.8.0.css} +0 -0
  33. /package/dist/palettes/{palette-winter.1.7.0.min.css → palette-winter.1.8.0.min.css} +0 -0
@@ -13,12 +13,14 @@
13
13
  import Schedule from '@mui/icons-material/Schedule';
14
14
  import Launch from '@mui/icons-material/Launch';
15
15
  import Visibility from '@mui/icons-material/Visibility';
16
+ import StarIcon from '@mui/icons-material/Star';
16
17
  const ComingSoonIcon = Schedule;
17
18
  const LaunchIcon = Launch;
18
19
  const PreviewIcon = Visibility;
19
20
  import {
20
21
  Box,
21
22
  Chip,
23
+ Rating,
22
24
  Typography,
23
25
  useTheme
24
26
  } from '@mui/material';
@@ -33,11 +35,18 @@ export interface Product {
33
35
  category: string;
34
36
  description: string;
35
37
  shortDescription?: string;
36
- features: string[];
37
- technologies: string[];
38
+ features?: string[];
39
+ technologies?: string[];
38
40
  status: string;
39
41
  image?: string;
40
42
  url?: string;
43
+ // E-commerce fields (optional)
44
+ price?: number;
45
+ salePrice?: number;
46
+ rating?: number;
47
+ reviewCount?: number;
48
+ isNew?: boolean;
49
+ featured?: boolean;
41
50
  }
42
51
 
43
52
  export interface ProductCardAction {
@@ -64,6 +73,8 @@ interface ProductCardViewProps extends WithBaseProps {
64
73
  showTechnologies?: boolean;
65
74
  /** Maximum features to show in compact mode */
66
75
  maxFeaturesCompact?: number;
76
+ /** Handler for adding product to cart (e-commerce products only) */
77
+ onAddToCart?: (product: Product) => void;
67
78
  }
68
79
 
69
80
  export interface ProductCardProps extends ProductCardViewProps, WithDataBinding {}
@@ -79,6 +90,7 @@ function ProductCardView({
79
90
  showImage = true,
80
91
  showTechnologies = true,
81
92
  maxFeaturesCompact = 3,
93
+ onAddToCart,
82
94
  ...restProps
83
95
  }: ProductCardViewProps) {
84
96
  const { styleProps, htmlProps } = useBaseProps(restProps);
@@ -87,6 +99,19 @@ function ProductCardView({
87
99
  // Return null if no product data
88
100
  if (!product) return null;
89
101
 
102
+ // Detect product type: e-commerce products have price field
103
+ const isEcommerce = product.price !== undefined;
104
+
105
+ // E-commerce helpers
106
+ const formatPrice = (price: number) => {
107
+ return `$${price.toFixed(2)}`;
108
+ };
109
+
110
+ const calculateDiscount = () => {
111
+ if (!product.price || !product.salePrice) return 0;
112
+ return Math.round(((product.price - product.salePrice) / product.price) * 100);
113
+ };
114
+
90
115
  const getStatusIcon = (status: Product['status']) => {
91
116
  switch (status) {
92
117
  case 'launched':
@@ -113,12 +138,30 @@ function ProductCardView({
113
138
  const handleProductClick = () => {
114
139
  if (onClick) {
115
140
  onClick();
116
- } else if (product.status === 'launched' && product.url?.startsWith('http')) {
117
- window.open(product.url, '_blank', 'noopener,noreferrer');
118
141
  }
142
+ // Note: Navigation is handled by parent wrapper (e.g., Next.js Link in BlockRenderer)
143
+ // For standalone usage, provide onClick prop with navigation logic
119
144
  };
120
145
 
121
146
  const getDefaultActions = (): ProductCardAction[] => {
147
+ // E-commerce products get "Add to Cart" action
148
+ if (isEcommerce) {
149
+ return [
150
+ {
151
+ id: 'add-to-cart',
152
+ label: 'Add to Cart',
153
+ variant: 'contained',
154
+ color: 'primary',
155
+ onClick: () => {
156
+ if (onAddToCart && product) {
157
+ onAddToCart(product);
158
+ }
159
+ }
160
+ }
161
+ ];
162
+ }
163
+
164
+ // Software products get status-based actions
122
165
  const actions: ProductCardAction[] = [
123
166
  {
124
167
  id: 'primary',
@@ -225,7 +268,7 @@ function ProductCardView({
225
268
  );
226
269
  })();
227
270
 
228
- const technologiesSectionElement = (!showTechnologies || variant === 'compact') ? null : (
271
+ const technologiesSectionElement = (!showTechnologies || variant === 'compact' || !product.technologies) ? null : (
229
272
  <Box sx={{ mb: 3 }}>
230
273
  <Typography
231
274
  variant="h6"
@@ -260,57 +303,57 @@ function ProductCardView({
260
303
  className={styleProps.className || "product-card"}
261
304
  onClick={htmlProps.onClick || (variant === 'compact' ? handleProductClick : undefined)}
262
305
  sx={{
263
- p: 3, // padding="large" equivalent
264
- borderRadius: 3,
265
- border: '1px solid',
266
- borderColor: 'divider',
267
- cursor: variant === 'compact' ? 'pointer' : 'default',
268
- position: 'relative',
269
306
  height: '100%',
270
307
  display: 'flex',
271
308
  flexDirection: 'column',
272
309
  backgroundColor: 'background.paper',
273
- transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
310
+ borderRadius: isEcommerce ? 2 : 3,
311
+ border: '1px solid',
312
+ borderColor: 'divider',
313
+ overflow: 'hidden',
314
+ position: 'relative',
315
+ cursor: variant === 'compact' ? 'pointer' : 'default',
316
+ transition: 'transform 0.2s ease, box-shadow 0.2s ease',
274
317
  '&:hover': variant === 'compact' ? {
275
318
  transform: 'translateY(-4px)',
276
- boxShadow: 8
319
+ boxShadow: 3
277
320
  } : {},
278
321
  ...(styleProps.sx || {})
279
322
  }}
280
323
  style={styleProps.style}
281
324
  >
282
- {/* Status Badge */}
283
- <Chip
284
- icon={getStatusIcon(product.status)}
285
- label={product.status.replace('-', ' ')}
286
- sx={{
287
- position: 'absolute',
288
- top: 16,
289
- right: 16,
290
- backgroundColor: getStatusColor(product.status),
291
- color: 'white',
292
- fontSize: '0.8rem',
293
- fontWeight: 500,
294
- textTransform: 'capitalize',
295
- zIndex: 2,
296
- height: '28px',
297
- px: 1.5,
298
- '& .MuiChip-icon': {
299
- color: 'white'
300
- }
301
- }}
302
- />
325
+ {/* Status Badge - only for software products */}
326
+ {!isEcommerce && (
327
+ <Chip
328
+ icon={getStatusIcon(product.status)}
329
+ label={product.status.replace('-', ' ')}
330
+ sx={{
331
+ position: 'absolute',
332
+ top: 16,
333
+ right: 16,
334
+ backgroundColor: getStatusColor(product.status),
335
+ color: 'white',
336
+ fontSize: '0.8rem',
337
+ fontWeight: 500,
338
+ textTransform: 'capitalize',
339
+ zIndex: 2,
340
+ height: '28px',
341
+ px: 1.5,
342
+ '& .MuiChip-icon': {
343
+ color: 'white'
344
+ }
345
+ }}
346
+ />
347
+ )}
303
348
 
304
349
  {/* Product Image */}
305
350
  {showImage && product.image && (
306
351
  <Box
307
- sx={{
308
- width: '100%',
309
- height: variant === 'detailed' ? 240 : 200,
310
- mb: 2.5,
311
- borderRadius: 1,
312
- overflow: 'hidden',
313
- backgroundColor: 'divider'
352
+ sx={{
353
+ width: '100%',
354
+ height: isEcommerce ? 280 : (variant === 'detailed' ? 240 : 200),
355
+ backgroundColor: 'divider',
356
+ position: 'relative'
314
357
  }}
315
358
  >
316
359
  <Box
@@ -331,56 +374,207 @@ function ProductCardView({
331
374
  }
332
375
  }}
333
376
  />
377
+
378
+ {/* E-commerce badges */}
379
+ {isEcommerce && (
380
+ <>
381
+ {/* Left side badges */}
382
+ <Box sx={{ position: 'absolute', top: 12, left: 12, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
383
+ {product.isNew && (
384
+ <Chip
385
+ label="NEW"
386
+ sx={{
387
+ backgroundColor: 'primary.main',
388
+ color: 'white',
389
+ fontSize: '0.75rem',
390
+ fontWeight: 600,
391
+ height: '24px',
392
+ px: 1
393
+ }}
394
+ />
395
+ )}
396
+ {product.featured && (
397
+ <Chip
398
+ label="POPULAR"
399
+ sx={{
400
+ backgroundColor: 'warning.main',
401
+ color: 'white',
402
+ fontSize: '0.75rem',
403
+ fontWeight: 600,
404
+ height: '24px',
405
+ px: 1
406
+ }}
407
+ />
408
+ )}
409
+ </Box>
410
+
411
+ {/* Right side badges */}
412
+ <Box sx={{ position: 'absolute', top: 12, right: 12, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
413
+ {product.salePrice && product.price && (
414
+ <>
415
+ <Chip
416
+ label="ON SALE"
417
+ sx={{
418
+ backgroundColor: 'error.main',
419
+ color: 'white',
420
+ fontSize: '0.75rem',
421
+ fontWeight: 600,
422
+ height: '24px',
423
+ px: 1
424
+ }}
425
+ />
426
+ <Chip
427
+ label={`-${calculateDiscount()}%`}
428
+ sx={{
429
+ backgroundColor: 'error.dark',
430
+ color: 'white',
431
+ fontSize: '0.75rem',
432
+ fontWeight: 600,
433
+ height: '24px',
434
+ px: 1
435
+ }}
436
+ />
437
+ </>
438
+ )}
439
+ </Box>
440
+ </>
441
+ )}
334
442
  </Box>
335
443
  )}
336
444
 
337
445
  {/* Product Info */}
338
- <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
339
- <Box sx={{ mb: 3 }}>
340
- <Typography
341
- variant="h4"
342
- component="h3"
343
- sx={{
344
- mb: 1,
345
- fontSize: variant === 'detailed' ? '1.75rem' : '1.5rem',
346
- fontWeight: 600
347
- }}
348
- >
349
- {product.name}
350
- </Typography>
351
- <Typography
352
- variant="overline"
353
- sx={{
354
- mb: 1.5,
355
- color: 'primary.main',
356
- fontSize: '0.9rem',
357
- fontWeight: 500,
358
- letterSpacing: '0.5px',
359
- display: 'block'
360
- }}
361
- >
362
- {product.category}
363
- </Typography>
364
- <Typography
365
- variant="body1"
366
- sx={{
367
- opacity: 0.8,
368
- lineHeight: 1.6
369
- }}
370
- >
371
- {variant === 'detailed' ? product.description : (product.shortDescription || product.description)}
372
- </Typography>
373
- </Box>
446
+ <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', p: isEcommerce ? 2 : 3 }}>
447
+ {isEcommerce ? (
448
+ /* E-commerce product info */
449
+ <Box>
450
+ {/* Category */}
451
+ {product.category && (
452
+ <Typography
453
+ variant="caption"
454
+ sx={{
455
+ mb: 0.5,
456
+ color: 'text.secondary',
457
+ display: 'block'
458
+ }}
459
+ >
460
+ {product.category}
461
+ </Typography>
462
+ )}
463
+
464
+ {/* Product name */}
465
+ <Typography
466
+ variant="h6"
467
+ component="h3"
468
+ sx={{
469
+ mb: 1,
470
+ fontSize: '1rem',
471
+ fontWeight: 500,
472
+ lineHeight: 1.3,
473
+ overflow: 'hidden',
474
+ textOverflow: 'ellipsis',
475
+ display: '-webkit-box',
476
+ WebkitLineClamp: 2,
477
+ WebkitBoxOrient: 'vertical'
478
+ }}
479
+ >
480
+ {product.name}
481
+ </Typography>
482
+
483
+ {/* Rating */}
484
+ {product.rating !== undefined && product.rating > 0 && (
485
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 1 }}>
486
+ <Rating
487
+ value={product.rating}
488
+ readOnly
489
+ size="small"
490
+ precision={0.5}
491
+ emptyIcon={<StarIcon style={{ opacity: 0.3 }} fontSize="inherit" />}
492
+ />
493
+ {product.reviewCount !== undefined && product.reviewCount > 0 && (
494
+ <Typography
495
+ variant="caption"
496
+ sx={{
497
+ color: 'text.secondary'
498
+ }}
499
+ >
500
+ ({product.reviewCount})
501
+ </Typography>
502
+ )}
503
+ </Box>
504
+ )}
505
+
506
+ {/* Price */}
507
+ <Box sx={{ mt: 'auto', display: 'flex', alignItems: 'center', gap: 1 }}>
508
+ <Typography
509
+ variant="h6"
510
+ sx={{
511
+ fontWeight: 600,
512
+ color: product.salePrice ? 'primary.main' : 'inherit'
513
+ }}
514
+ >
515
+ {formatPrice(product.salePrice || product.price!)}
516
+ </Typography>
517
+ {product.salePrice && (
518
+ <Typography
519
+ variant="body2"
520
+ sx={{
521
+ color: 'text.secondary',
522
+ textDecoration: 'line-through'
523
+ }}
524
+ >
525
+ {formatPrice(product.price!)}
526
+ </Typography>
527
+ )}
528
+ </Box>
529
+ </Box>
530
+ ) : (
531
+ /* Software product info */
532
+ <Box sx={{ mb: 3 }}>
533
+ <Typography
534
+ variant="h4"
535
+ component="h3"
536
+ sx={{
537
+ mb: 1,
538
+ fontSize: variant === 'detailed' ? '1.75rem' : '1.5rem',
539
+ fontWeight: 600
540
+ }}
541
+ >
542
+ {product.name}
543
+ </Typography>
544
+ <Typography
545
+ variant="overline"
546
+ sx={{
547
+ mb: 1.5,
548
+ color: 'primary.main',
549
+ fontSize: '0.9rem',
550
+ fontWeight: 500,
551
+ letterSpacing: '0.5px',
552
+ display: 'block'
553
+ }}
554
+ >
555
+ {product.category}
556
+ </Typography>
557
+ <Typography
558
+ variant="body1"
559
+ sx={{
560
+ opacity: 0.8,
561
+ lineHeight: 1.6
562
+ }}
563
+ >
564
+ {variant === 'detailed' ? product.description : (product.shortDescription || product.description)}
565
+ </Typography>
566
+ </Box>
567
+ )}
374
568
 
375
- {/* Features - only show if features exist */}
376
- {product.features && product.features.length > 0 && featuresListElement}
569
+ {/* Features - only show for software products */}
570
+ {!isEcommerce && product.features && product.features.length > 0 && featuresListElement}
377
571
 
378
- {/* Technologies - only show if technologies exist */}
379
- {product.technologies && product.technologies.length > 0 && technologiesSectionElement}
572
+ {/* Technologies - only show for software products */}
573
+ {!isEcommerce && product.technologies && product.technologies.length > 0 && technologiesSectionElement}
380
574
 
381
575
  {/* Action Buttons */}
382
- <Box sx={{
383
- display: 'flex',
576
+ <Box sx={{
577
+ display: 'flex',
384
578
  gap: 1.5,
385
579
  mt: 'auto',
386
580
  ...(variant === 'compact' && { justifyContent: 'center' })
@@ -391,7 +585,12 @@ function ProductCardView({
391
585
  variant={action.variant || 'contained'}
392
586
  // color={action.color || 'primary'}
393
587
  disabled={action.disabled}
394
- onClick={action.onClick}
588
+ onClick={(e) => {
589
+ // Prevent Link navigation when clicking button (e.g., Add to Cart)
590
+ e.stopPropagation();
591
+ e.preventDefault();
592
+ action.onClick();
593
+ }}
395
594
  {...(variant === 'compact' && { fullWidth: true })}
396
595
  >
397
596
  {action.label}
@@ -42,6 +42,21 @@ function FormCheckboxView({
42
42
  helperText,
43
43
  required = false,
44
44
  disabled = false,
45
+ // Exclude ViewProps that conflict with MUI FormControl types
46
+ margin: _margin,
47
+ marginTop: _marginTop,
48
+ marginRight: _marginRight,
49
+ marginBottom: _marginBottom,
50
+ marginLeft: _marginLeft,
51
+ marginX: _marginX,
52
+ marginY: _marginY,
53
+ padding: _padding,
54
+ paddingTop: _paddingTop,
55
+ paddingRight: _paddingRight,
56
+ paddingBottom: _paddingBottom,
57
+ paddingLeft: _paddingLeft,
58
+ paddingX: _paddingX,
59
+ paddingY: _paddingY,
45
60
  ...restProps
46
61
  }: FormCheckboxProps) {
47
62
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -63,6 +63,21 @@ function FormFieldView({
63
63
  startAdornment,
64
64
  endAdornment,
65
65
  inputProps,
66
+ // Exclude ViewProps that conflict with MUI FormControl types
67
+ margin: _margin,
68
+ marginTop: _marginTop,
69
+ marginRight: _marginRight,
70
+ marginBottom: _marginBottom,
71
+ marginLeft: _marginLeft,
72
+ marginX: _marginX,
73
+ marginY: _marginY,
74
+ padding: _padding,
75
+ paddingTop: _paddingTop,
76
+ paddingRight: _paddingRight,
77
+ paddingBottom: _paddingBottom,
78
+ paddingLeft: _paddingLeft,
79
+ paddingX: _paddingX,
80
+ paddingY: _paddingY,
66
81
  ...restProps
67
82
  }: FormFieldProps) {
68
83
  const fieldId = React.useId();
@@ -54,6 +54,21 @@ function FormSelectView({
54
54
  fullWidth = true,
55
55
  size = 'small',
56
56
  placeholder,
57
+ // Exclude ViewProps that conflict with MUI FormControl types
58
+ margin: _margin,
59
+ marginTop: _marginTop,
60
+ marginRight: _marginRight,
61
+ marginBottom: _marginBottom,
62
+ marginLeft: _marginLeft,
63
+ marginX: _marginX,
64
+ marginY: _marginY,
65
+ padding: _padding,
66
+ paddingTop: _paddingTop,
67
+ paddingRight: _paddingRight,
68
+ paddingBottom: _paddingBottom,
69
+ paddingLeft: _paddingLeft,
70
+ paddingX: _paddingX,
71
+ paddingY: _paddingY,
57
72
  ...restProps
58
73
  }: FormSelectProps) {
59
74
  const handleChange = (e: { target: { value: unknown } }) => {